プログラミング

C言語超入門⑦ ポインタの基本を理解しよう

普通の変数とポインタは何が違う?値のコピーと元の値の変更を同じ例で比べ、&と*、関数への渡し方、NULLまで順に整理します。

ポインタは、名前だけで「難しそう」と感じやすいところです。でも、最初から複雑な計算やメモリ確保をする必要はありません。

まずは「値そのもの」と「値が置かれた場所」を分けるところから始めます。最後に、関数から2つの変数の値を入れ替えるプログラムを作りましょう。

関数と配列を読んだ後を想定しています。

#includeから始まる例は、そのまま1本のプログラムです。短い変数操作の例はmain内、関数の例はmainの外へ置きます。同じ名前を使う別の例を一度に貼り合わせないようにしてください。

1. 値とアドレスは別の情報

#include <stdio.h>

int main(void) {
    int score = 80;

    printf("値: %d\n", score);
    printf("場所: %p\n", (void *)&score);
    return 0;
}

1行目には80、2行目には環境に応じたアドレス表現が表示されます。アドレスの数字は実行ごとに変わることがあるため、決まった値を期待しません。

メモリは、プログラムが実行中のデータを置く場所です。その中の場所を表す情報がアドレス。ロッカーにたとえるなら、80という値が荷物で、アドレスがロッカーの位置です。

&scoreはscoreのアドレスを得る式です。%pで表示する際にはvoid*型のポインタを渡すため、(void *)で変換しています。変換の細かな書式は後からでも構いません。

2. ポインタへ「場所」を保存する

int score = 80;
int *p = &score;

pがポインタ変数です。ポインタも変数の一種ですが、今回のscoreが整数を保存するのに対し、pはint型のオブジェクトを指すポインタ値を保存します。「特別な別世界の変数」というより、入れておく値の種類が違うんですね。

書き方 今、表しているもの
score 元の変数の値80
&score 元の変数のアドレス
p 保存したアドレス
*p そのアドレスにあるintの値80

pに80がコピーされたわけではありません。pをたどるとscoreへ到達する、という関係です。

p自体にも置き場所はありますが、pに保存しているのは自分の場所ではなく、指す相手であるscoreの場所です。&pはポインタ変数自身のアドレスなので、ここで使う&scoreとは別です。

宣言の*と、式の*を分ける

int *pの*は「ポインタ型の変数を宣言する」記号です。後で書く*pは「指している先を使う」操作です。同じ記号でも出てくる場所で役割が変わります。

int *p, number;では、ポインタはpだけで、numberは普通のintになります。初心者のうちは宣言を1行ずつ分けると読み違いを防げます。

3. 値のコピーと、ポインタ経由の変更を比べる

「どちらも80を読めるのに、何が違うの?」というところを、同じプログラムで確かめます。表示の順番はscore、copy、*pです。

#include <stdio.h>

int main(void) {
    int score = 80;
    int copy = score;
    int *p = &score;

    printf("%d %d %d\n", score, copy, *p);
    copy = 90;
    printf("%d %d %d\n", score, copy, *p);
    *p = 95;
    printf("%d %d %d\n", score, copy, *p);
    return 0;
}
80 80 80
80 90 80
95 90 95
操作した直後 score copy pが指す相手 *p
3つを用意 80 80 score 80
copy = 90 80 90 score 80
*p = 95 95 90 score 95
  1. int copy = scoreは、その時点の80を別の変数へコピーします。以後の変更まで連動するわけではありません。
  2. copy = 90で変わるのはcopyだけです。元のscoreは80のまま。
  3. *p = 95は、pが指すscoreへ95を書きます。だからscoreと*pを読むと同じ95になります。

pはこの間ずっと同じ相手を指しています。95という点数がpへ入ったわけではありません。

*でポインタの指す先を使う操作は間接参照と呼ばれます。Microsoftのアドレス演算子と間接参照の説明も確認できます。

確認問題:指す相手をcopyに変えたら?

最後の*p = 95;の直前に、p = &copy;を追加すると最後の表示はどうなるでしょう。

答えは80 95 95です。pの新しい相手はcopyなので、95になるのはcopy。scoreは80のままです。「左側がpならアドレス、*pなら指す先の値」と読むと区別できます。

pを変えるのと、*pを変えるのは別

int first = 10;
int second = 20;
int *p = &first;

p = &second;
*p = 30;

printf("%d, %d\n", first, second);

結果は10, 30です。p = &secondは「次から別の場所を指す」、*p = 30は「今指している場所の値を変える」。この2つを区別できると、急に読みやすくなります。

4. 関数へ渡して、元の変数を変更する

前回の普通のint引数は、値のコピーでした。ポインタを渡す場合もポインタ値がコピーされますが、コピーしたアドレスから同じ元の変数へアクセスできます。

#include <stdio.h>

void addOne(int *number) {
    if (number == NULL) {
        return;
    }
    (*number)++;
}

int main(void) {
    int count = 3;
    addOne(&count);
    printf("%d\n", count);
    return 0;
}

結果は4です。この例は、渡す値がintの最大値ではないことを前提にしています。最大値へさらに1を加えてはいけません。

(*number)++のかっこも重要です。*number++と書くと、指す先の値ではなくポインタ側を進める式になります。まずは「指す先をかっこで取り出し、それを1増やす」と読んでください。

scanfの&にもつながる

scanf("%d", &age)は、ageへ結果を書き込んでもらうためにアドレスを渡しています。表示用のprintf("%d", age)には値を渡すので&を付けません。

同じ数を扱うのに引数が違う理由は、「読むだけなのか、元の場所へ書くのか」という役割の違いです。

5. NULLなら使わない。でも、それだけでは十分ではない

int *p = NULL;

if (p != NULL) {
    printf("%d\n", *p);
}

NULLは、どのオブジェクトも指さないヌルポインタを表すためのマクロです。stdio.hなどで使えるようになります。

NULLを間接参照するのは未定義動作です。「必ず安全なエラーで止まる」保証はないため、読み書きしてはいけません。

ただし、p != NULLだから必ず有効、とは言えません。次のような問題もあります。

  • 値を初期化していないポインタ
  • すでに寿命が終わった変数を指すポインタ
  • 配列の範囲外を指し、その先を読むポインタ

変数の寿命が終わった場所は使わない

例えば、関数内の普通のローカル変数のアドレスを返しても、その変数は関数終了時に寿命が終わります。返されたポインタを使って読み書きしてはいけません。

これはダングリングポインタ、つまり有効な相手を失ったポインタの問題です。アドレスが0以外でも、その場所を使える約束がなくなっています。

今回の例では、ポインタを使う間ずっとmainの変数が生きているので、この条件を満たせています。変数の寿命はC11公開委員会草案の6.2.4で定義されています。

6. 配列とポインタは、関係はあるが同じものではない

int numbers[3] = {10, 20, 30};
int *p = numbers;

printf("%d\n", p[1]);
printf("%d\n", *(p + 1));

どちらも20です。多くの式では、配列名が先頭要素を指すポインタへ変換されます。p + 1は、アドレスの数字を単純に1増やすというより、int要素1個分先を表します。

ただし、ポインタには配列の個数が自動で付いてくるわけではありません。p[3]を読めば範囲外です。

また、sizeof(numbers)は配列全体のサイズですが、sizeof(p)はポインタ自体のサイズです。「配列名がポインタへ変換される場面がある」と「配列がポインタそのもの」は分けて考えます。

7. 練習:2つの変数を入れ替えよう

first = 10、second = 20を、swap関数を使って20と10に入れ替えてください。

模範解答

#include <stdio.h>

void swap(int *a, int *b) {
    if (a == NULL || b == NULL) {
        return;
    }

    int temporary = *a;
    *a = *b;
    *b = temporary;
}

int main(void) {
    int first = 10;
    int second = 20;

    swap(&first, &second);
    printf("first = %d, second = %d\n", first, second);
    return 0;
}

解説:なぜ一時保存が必要?

  1. temporaryへfirstの10を避難させます。
  2. *a = *bでfirstを20へ変えます。
  3. *b = temporaryでsecondへ元の10を戻します。

一時保存せず*a = *b; *b = *a;とすると、2行目で読む*aはすでに20です。両方20になり、元の10を失います。

ヌルの確認はしていますが、寿命切れのアドレスを修復するものではありません。呼び出し側が有効なintの場所を渡すことも、この関数を使う条件です。

次の一歩

ポインタで迷ったら、「今はアドレスを変えている? それとも、その先の値を変えている?」と読み直しましょう。

mallocやfreeによる動的メモリ管理は、今回の範囲外です。まず有効な変数へのポインタを追えるようになってから、所有や解放の話へ進めば大丈夫です。

前の回 / 次の回:構造体と列挙型

← 記事一覧へ戻る