Menu

C のポインタ:例で学ぶ完全ガイド

ポインタとはメモリアドレスを格納する変数です。& と * の演算子、ポインタの宣言と参照外し、ポインタの型が重要な理由、そしてポインタが存在する理由そのものを示す swap() 関数まで、土台から組み立てます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

プログラム中のすべての変数はメモリのどこかに存在し、その「どこか」にはすべて番号、すなわちアドレスが付いています。ポインタとは、その番号を 1 つ格納する変数にすぎません。アイデアはそれだけです。このページの残りは構文とその帰結です。

C でポインタがこれほど重要なのは、C が関数にすべての引数のコピーを渡すからです。数値 7 を渡された関数は自分のコピーを好きなだけ変えられますが、呼び出し側の変数は微動だにしません。代わりにアドレスを渡せば、関数は手を伸ばして元の値を変えられます。配列、文字列、動的メモリ、そしてこの先 C で組み立てるあらゆるデータ構造は、この 1 つの能力の上に成り立っています。

メモリ、アドレス、そしてなぜ何にでもアドレスがあるのか

int score = 42; と書くと、コンパイラは数バイトのメモリを予約し、あなたが使える名前を付けます。そのバイト列には数値のアドレスもあります。メモリを、番号の付いた家が並ぶとても長い通りだと考えてください。score0x7ffd4c2a 番地に住む一家です。名前はあなたのため、番号は機械のためのものです。

& 演算子はその番号を尋ねます。

実行してみてください。3 つのアドレスが異なるのは 3 つの変数が別々のバイトを占めているからで、実行のたびに変わります。それが普通であり、アドレスを決して直書きしない理由でもあります。(void *) のキャストにも注目してください。%pvoid * を取ると規定されており、ほかのポインタ型を渡すのは厳密には未定義動作です。キャストを習慣にしましょう。

アドレスが予測できないことは問題になりません。アドレスがいくつかを気にすることはまずないからです。大事なのは、それを手にしていることです。

ポインタを宣言する

ポインタの宣言は、指す先の型を名指しします。

int    *p;      // p は int のアドレスを保持できる
double *q;      // q は double のアドレスを保持できる
char   *name;   // name は char のアドレスを保持できる

int *p; は「*pint である」と読んでください。p を参照外しすると int が得られる、という意味です。この読み方は、この先 C が突き付けてくるどんな複雑な宣言でも通用します。

* をどこに置くかはコンパイラが無視するスタイルの選択です。int* pint *pint * p はどれも同じものを宣言します。ほとんどの C コードは int *p と書きますが、それには実用上の理由があります。

int* a, b;   // a は int へのポインタ。b はポインタではなく「ただの」int
int *a, *b;  // 両方ともポインタ - * は型ではなく名前に結び付く

* は型ではなく宣言子に付きます。名前の隣に書けば、それが目に見えるままになります。

& と *:2 つの演算子

すべての仕事をするのは 2 つの演算子で、両者はちょうど反対です。

  • &x - アドレス演算子x が存在するアドレスを生み出します。
  • *p - 参照外しp の中のアドレスまで行き、そこに存在するオブジェクトを返します。

最後の 2 行こそがポインタの眼目です。age に直接何も代入していないのに age が変わりました。*p = 31 は「p が保持するアドレスに 31 を格納せよ」という意味で、そのアドレスが age のアドレスだからです。

メモリ上の図はこうなります。

      p                        age
 +------------+           +----------+
 | 0x7ffd1c40 | --------> |    31    |
 +------------+           +----------+
 0x7ffd1c38 にある        0x7ffd1c40 にある

 p    -> アドレス 0x7ffd1c40
 *p   -> そこに格納された int、31
 &age -> 0x7ffd1c40、p が保持しているのと同じアドレス
 &p   -> 0x7ffd1c38、p 自身が存在する場所

最後の行に注目してください。ポインタ自身もメモリのどこかにある変数なので、それ自身のアドレスも持っています。これはひっかけではなく、単なる一貫性であり、ポインタへのポインタ(int **)が成り立つ仕組みでもあります。

&* は互いを打ち消すので、*&age は単に age であり、&*p は単に p です。

ポインタの型が重要な理由

ポインタはアドレスを格納し、アドレスはただの数値です。ではなぜコンパイラは、それが int を指すのか double を指すのかを知りたがるのでしょうか。理由は 2 つです。

  1. 何バイト読むか。 int * を参照外しすると(典型的には)4 バイト、double * を参照外しすると 8 バイト読みます。型がなければ *p は無意味です。
  2. そのバイト列をどう解釈するか。 同じビット列でも、int としては 1 つの数値、float としてはまったく別の数値になります。

一方、ポインタ自身のサイズは、何を指しているかに依存しません。アドレスはアドレスです。

64 ビットマシンでは 3 つのポインタはどれも 8 バイトですが、指している先は 4、8、1 バイトです。*p と ポインタ演算 に意味を与えているのは、この指し先の型です。

ポインタ型をまたいだ代入はコンパイラが文句を言うエラーで、それにはもっともな理由があります。pi = &d; は、double の 4 バイトを読んでその結果を整数と呼ぶ準備をしているようなものです。

定番の動機:swap()

どの C 講座でも持ち出される関数がこれです。まさにポインタを説明してくれる形で失敗するからです。

関数は入れ替えたと報告し、main は何も起きていないと報告します。broken_swap が受け取ったのは xyコピーでした。自分のコピーを完璧に入れ替え、そのコピーは関数が戻った時点で消滅したのです。

代わりにアドレスを渡しましょう。

今でも ab はコピーです。ただし 2 つのアドレスのコピーであり、アドレスのコピーは元と同じ変数を指します。関数がどこから呼ばれようと、*ax なのです。

これは呼び出し側のデータを変更する C の関数すべての背後にあるパターンであり、関数の引数 全般の背後にもあるパターンです。C には参照渡しがないので、ポインタを渡して参照外しするのです。 scanf& が必要なのもこのためです。

int n;
scanf("%d", &n);   // scanf は n に書き込めなければならないので、n のアドレスが要る

2 つ以上の値を返す

C の関数は値を 1 つ返します。それを回避する手段がポインタです。追加の結果のためにアドレスを渡し込みます。

戻り値が状態を運び、ポインタが結果を運びます。この分担は C 自身の標準ライブラリにも OS の API にも、いたるところにあります。

ポインタへのポインタ

ポインタも変数なので、そのアドレスを取れます。そしてそのアドレスの型は「int へのポインタへのポインタ」です。

  pp              p               value
+------+       +------+        +------+
| &p   | ----> | &val | -----> |   8  |
+------+       +------+        +------+

** に本当に出会うのは、呼び出し側が保持するポインタを関数が変更しなければならないとき(たとえば呼び出し側のポインタを新しく確保したメモリに設定するアロケータ)と、コマンドライン引数char *argv[] です。

やらかす前に知っておきたい 4 つの間違い

どこも指していないポインタを使う。 未初期化のポインタは、そこに元からあったバイト列を保持しています。参照外しすると、でたらめなアドレスを読み書きします。

int *p;      // p はゴミを保持している
*p = 10;     // 未定義動作 - おそらくクラッシュ

すべてのポインタを、実在のアドレスか NULL で初期化しましょう。その作法については NULL ポインタ を参照してください。

*pp を混同する。 p = 5; はポインタをアドレス 5 に設定します(無意味です)。*p = 5; は指している先に 5 を格納します。前者にはコンパイラが警告を出しますし、警告は読む価値があります。

scanf& を忘れる。 scanf("%d", n)nをアドレスとして渡します。警告付きでコンパイルされ、その数値が名指すメモリへ書き込んでしまいます。

ローカル変数のアドレスを返す。 関数が戻った瞬間にローカル変数は消えます。そのアドレスは、取り壊された建物の領収書です。

int *bad(void) {
    int local = 42;
    return &local;   // 呼び出し側はダングリングポインタを受け取る
}

呼び出しより長生きするメモリが必要なら、確保しましょう。そのための 動的メモリ です。

次はどこへ

ポインタは「アドレスを 1 つ保持する」で終わりではありません。ポインタへの加算はその型の単位でメモリを歩き進み、それが ポインタ演算 の主題です。そして C の配列名は先頭要素へのポインタに減衰するので、両方を知ってしまえば ポインタと配列 は事実上ひとつの話題になります。次はその 2 つを読みましょう。そうすれば C の残りの部分は、もう魔法には見えなくなります。

よくある質問

C のポインタとは何ですか?

ポインタとは、値がメモリアドレス、つまりメモリ上にある別のオブジェクトの位置である変数のことです。int *p = &age;page のアドレスを保持させ、以後 *p はそのアドレスを通じて age 変数を読み書きします。

C の * と & の違いは何ですか?

&x はアドレス演算子で、x が存在するアドレスを生み出します。*p は参照外し演算子で、p に入っているアドレスまで行き、そこにあるオブジェクトを返します。互いに打ち消し合うので、*&x は単に x です。

なぜ * は宣言時とポインタの使用時の 2 回現れるのですか?

同じ記号の異なる 2 つの用法だからです。int *p;* は型の一部で、「p は int へのポインタである」と述べています。*p = 5;* は参照外し演算子で、「p が保持するアドレスに 5 を格納せよ」と述べています。宣言を「*p は int である」と読めば、どちらも筋が通ります。

なぜ C ではポインタが必要なのですか?

C はすべての引数を値渡しするので、アドレスを渡されない限り関数が呼び出し側の変数を変えることはできません。ポインタはまた、配列を効率よく歩き、2 つ以上の結果を返し、連結構造を組み立て、malloc で実行時に確保したメモリを使うことも可能にします。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める