# C言語のポインタの説明 C言語の壁といわれるポインタについてコードをまじえて説明していきます。 何か一つでも得られるものがあれば幸いです。 <h3>ポインタとは</h3> ポインタとはアドレスを格納する変数です。 <h3>ポインタの使い方</h3> 「`*`」を使いますが、ややこしいことに宣言時と文中で「`*`」の意味が変わります。 ポインタは次のように宣言します。 ``` int *p; char *q; void *r; ``` 文中でポインタを扱うときは、2つの方法があります。 ``` p = 何らかのアドレス *p = 何らかの値 ``` `*p`とすると、アドレスpに格納された変数を指すことになります。 ``` #include <stdio.h> int main() { int a; int *p; a = 123; p = &a; printf("a = %d, p = %p\n", *p, p); *p = 456; printf("a = %d, p = %p\n", *p, p); return 0; } ``` ``` a = 123, p = 0x7fffc9e8aecc a = 456, p = 0x7fffc9e8aecc ``` <h3>void型ポインタ</h3> 次のコードはvoid型のポインタを使った例です。 例えば、mallocの戻り値がvoid型のポインタであったり、よく使われる印象があります。 ``` #include <stdio.h> int main() { void *p; int a; a = 123; p = &a; printf("1: a = %d, p = %p\n", a, p); printf("2: a = %d, p = %p\n", *(int *)p, p); return 0; } ``` ``` 1: a = 123, p = 0x7fffcc25affc 2: a = 123, p = 0x7fffcc25affc ``` `%d`で出力されるのは整数です。そのため、pを整数の型にキャストしています。 もう少し細かく説明します。 まず、`(int *)p`でvoid型のポインタpからint型のポインタpに変換します。その後は、先ほどと同じように`*`を使って同じように変数aにアクセスしているだけです。 <h3>すきまについて</h3> ポインタ宣言時の`void`と`*`の間に隙間がある理由を説明します。 端的に言うと連続したポインタの宣言時のややこしさ回避のための慣習です。ようはは見た目の問題です。 すきまなしで考えてみると、すぐにわかります。 int型を2つ宣言したいとき、次のようにします。 ``` int a, b; ``` 同じようにint型のポインタをすきまなしで宣言すると、次のようになります。 ``` int* p, q; ``` しかし、これは次のように解釈されます。 ``` int *p; int q; ``` これではポインタを2つ宣言したつもりなのにポインタはpしか宣言されていません。 次のようにすると、2つのポインタが宣言されます。 ``` int *p, *q; ``` すきまがあるほうがわかりやすいです。 <h3>ポインタと文字列</h3> 文字列はchar型のポインタみたいなものです。 逆にchar型のポインタは文字列みたいに扱えます。 ``` #include <stdio.h> int main() { char str1[] = "abcd"; char *str2 = "efgh"; printf("%s\n", str1); printf("%s\n", str2); return 0; } ``` ``` abcd efgh ``` ただし、全く同じように扱えるかというと、そうではないようです。 例えば、次のようにすると私の環境ではうまく実行されませんでした。 ``` #include <stdio.h> int main() { char str1[] = "abcd"; char *str2 = "efgh"; printf("%s\n", str1); printf("%s\n", str2); int i; for (i = 0; i < 4; i++) { str1[i] = 'x'; } printf("%s\n", str1); for (i = 0; i < 4; i++) { str2[i] = 'y'; } printf("%s\n", str2); return 0; } ``` ``` abcd efgh xxxx Segmentation fault (core dumped) ``` デバッガで調べてみると、プロセスのアクセス保護属性の問題でした。 文字列`efgt`はプロセスメモリの書き込みできない領域に生成され、ここに書き込もうとするとセグメンテーションフォールトが起こります。~~なぜ書き込みできない領域に文字列が生成されるのかはわかりませんでした。~~`char *str2 = "efgh";`という書き方は古いコンパイラに対しては使われていたようです。しかし、コンパイラの仕様変更によりこれは違反コードになりました。この文字列リテラルに対する代入文の結果は未定義みたいです。 参考 https://docs.oracle.com/cd/E19957-01/805-7888/z4000220f2c4c/index.html https://www.jpcert.or.jp/m/sc-rules/c-str05-c.html 文字列`abcd`はスタック領域という書き込み可能なメモリ領域に生成されているので、次のようにすることはできました。 ``` #include <stdio.h> int main() { char str1[] = "abcd"; char *str2 = str1; /*変更箇所*/ printf("%s\n", str1); printf("%s\n", str2); int i; for (i = 0; i < 4; i++) { str1[i] = 'x'; } printf("%s\n", str1); for (i = 0; i < 4; i++) { str2[i] = 'y'; } printf("%s\n", str2); return 0; } ``` ``` abcd abcd xxxx yyyy ``` とにかく、`char *str2 = "efgh";`みたいなことをするのはしない方がいいようです。 <h3>ポインタと構造体</h3> 今までポインタの型はintやcharでしたが、構造体型のポインタを使うこともできます。 ``` #include <stdio.h> #include <stdlib.h> int main() { struct human { char *name; int height; int weight; }; printf("human size:%d\n", (int) sizeof(struct human)); struct human ichiro; ichiro.name = "ichiro"; ichiro.height = 170; ichiro.weight = 60; printf("name:%s\theight:%d\tweight:%d\n", ichiro.name, ichiro.height, ichiro.weight); struct human *jiro; jiro = (struct human *) malloc(sizeof(struct human)); jiro->name = "jiro"; jiro->height = 160; jiro->weight = 50; printf("name:%s\theight:%d\tweight:%d\n", jiro->name, jiro->height, jiro->weight); return 0; } ``` ``` human size:16 name:ichiro height:170 weight:60 name:jiro height:160 weight:50 ``` jiroはhuman構造体のポインタです。jiroポインタの宣言の直後に、mallocという関数を実行しています。これはヒープメモリという領域から使用可能な領域をもらう関数です。 なぜこのような関数が必要なのでしょうか。 ichiro宣言のときには必要ないことを考えます。普通に構造体を宣言したときは簡潔に言うと勝手にメモリが割り当てられています。コンパイラが構造体に必要なサイズ(この場合16バイト)を計算し、実行時に使用可能にしています。これまでのコードのintやcharも、必要なサイズを計算して、実行時に確保しています。いうなれば、「宣言」というのは、コンパイラに必要なアドレスのサイズを教えてあげているといった感じです。(まちがってたらすいません) 対して構造体ポインタを使用した場合、mallocをしなければその構造体に必要な(サイズ16バイトの)メモリ領域は存在していないことになります。宣言しているのはポインタであって構造体ではありません。構造体を指すアドレスを格納するポインタを宣言しています。つまり、コンパイラには8バイト(32ビットシステムの場合4バイト/64ビットシステムの場合8バイト)の領域が必要であることしか伝えていません。だから、自分で領域を採りに行く必要があり、mallocを呼び出しています。 mallocの戻り値はvoid型のポインタなので、human構造体にキャストしています。 構造体ポインタとして宣言された場合、構造体の要素へのアクセスは`.`ではなく、`->`を使用します。 <h3>ポインタのありがたさ</h3> このままではポインタが嫌われそうなので、ここでありがたさを説明します。 さっきのプログラムに、体重を減らす関数としてdiet_struct関数とdiet_pointer関数を加えます。 diet_struct関数: 構造体を引数とする。 diet_pointer関数: ポインタを引数とする。 ``` #include <stdio.h> #include <stdlib.h> struct human { char *name; int height; int weight; }; struct human diet_struct(struct human x_ro); void diet_pointer(struct human *x_ro); int main() { printf("human size:%d\n", (int) sizeof(struct human)); struct human ichiro; ichiro.name = "ichiro"; ichiro.height = 170; ichiro.weight = 60; printf("name:%s\theight:%d\tweight:%d\n", ichiro.name, ichiro.height, ichiro.weight); /* diet */ ichiro = diet_struct(ichiro); printf("name:%s\theight:%d\tweight:%d\n", ichiro.name, ichiro.height, ichiro.weight); struct human *jiro; jiro = (struct human *) malloc(sizeof(struct human)); jiro->name = "jiro"; jiro->height = 160; jiro->weight = 50; printf("name:%s\theight:%d\tweight:%d\n", jiro->name, jiro->height, jiro->weight); /* diet */ diet_pointer(jiro); printf("name:%s\theight:%d\tweight:%d\n", jiro->name, jiro->height, jiro->weight); return 0; } struct human diet_struct(struct human x_ro) { x_ro.weight -= 10; return x_ro; } void diet_pointer(struct human *x_ro) { x_ro->weight -= 10; } ``` ``` human size:16 name:ichiro height:170 weight:60 name:ichiro height:170 weight:50 name:jiro height:160 weight:50 name:jiro height:160 weight:40 ``` 抽象的な表現になりますが、関数の気持ちになって考えるとありがたさがわかります。 diet_struct関数には引数として構造体を渡しています。ここでは大きさが16バイトなので大したことありませんが、もし1000バイトとかもっと大きい構造体の場合すごく効率が悪いです。関数からしてみれば、要素weightのみを編集できればいいのに、その他の要素も一緒にに渡されていい迷惑です。しかもその大荷物を呼び出し元の関数に返さなければいけません。 diet_pointer関数には引数としてアドレスを渡しています。構造体がどんな大きさであれ、関数に渡すデータのサイズは8バイトです。ポインタを介して構造体の要素にアクセスできるため、関数は重たい荷物を受け取ることも返すこともする必要がありません。 よほど大きな構造体を渡すことがない限りプログラムの実行時間に大きく差が出ることはないとは思いますが、ポインタを介することで実行速度が速くなり、実行時に必要なメモリサイズも少なく済み、効率的なプログラムとなります。 構造体として宣言されたichiroをdiet_pointer関数に渡す場合は&を使います。 ``` #include <stdio.h> #include <stdlib.h> struct human { char *name; int height; int weight; }; struct human diet_struct(struct human x_ro); void diet_pointer(struct human *x_ro); int main() { printf("human size:%d\n", (int) sizeof(struct human)); struct human ichiro; ichiro.name = "ichiro"; ichiro.height = 170; ichiro.weight = 60; printf("name:%s\theight:%d\tweight:%d\n", ichiro.name, ichiro.height, ichiro.weight); /* diet */ ichiro = diet_struct(ichiro); printf("name:%s\theight:%d\tweight:%d\n", ichiro.name, ichiro.height, ichiro.weight); /* ここ */ diet_pointer(&ichiro); printf("name:%s\theight:%d\tweight:%d\n", ichiro.name, ichiro.height, ichiro.weight); struct human *jiro; jiro = (struct human *) malloc(sizeof(struct human)); jiro->name = "jiro"; jiro->height = 160; jiro->weight = 50; printf("name:%s\theight:%d\tweight:%d\n", jiro->name, jiro->height, jiro->weight); /* diet */ diet_pointer(jiro); printf("name:%s\theight:%d\tweight:%d\n", jiro->name, jiro->height, jiro->weight); return 0; } struct human diet_struct(struct human x_ro) { x_ro.weight -= 10; return x_ro; } void diet_pointer(struct human *x_ro) { x_ro->weight -= 10; } ``` ``` human size:16 name:ichiro height:170 weight:60 name:ichiro height:170 weight:50 name:ichiro height:170 weight:40 name:jiro height:160 weight:50 name:jiro height:160 weight:40 ``` ichiroがめっちゃやせてしまいました。 <h3>ポインタの配列</h3> ポインタ配列というものがあります。 よく見かけるのは、実行ファイルの引数処理です。 次のプログラムは実行ファイルの引数に入力した文字などを出力するプログラムです。 ``` #include <stdio.h> int main(int argc, char *argv[]) { printf("argc = %d\n", argc); for (int i = 0; i < argc; i++) { printf("argv[%d]: %s\n", i, argv[i]); } return 0; } ``` ``` $ ./a.out hello world argc = 3 argv[0]: ./a.out argv[1]: hello argv[2]: world ``` argc: 引数の数 argv: 引数の文字列指すポインタの配列 main関数の引数はプログラム実行時に自動的に与えられます。 ```int main(int argc, char *argv[])```は次のように書き換え可能です。 ```int main(int argc, char **argv)``` これだと少しわかりずらいので、同じような感じのプログラムを書いてみました。 ``` #include <stdio.h> int main() { char *argv[3]; char str1[] = "./a.out"; char str2[] = "hello"; char str3[] = "world"; argv[0] = str1; argv[1] = str2; argv[2] = str3; for (int i = 0; i < 3; i++) { printf("argv[%d]:%s\n", i, argv[i]); } printf("argv[0]:%p\tstr1:%p\n", argv[0], str1); printf("argv[1]:%p\tstr2:%p\n", argv[1], str2); printf("argv[2]:%p\tstr3:%p\n", argv[2], str3); return 0; } ``` ``` argv[0]:./a.out argv[1]:hello argv[2]:world argv[0]:0x7ffffd3be690 str1:0x7ffffd3be690 argv[1]:0x7ffffd3be684 str2:0x7ffffd3be684 argv[2]:0x7ffffd3be68a str3:0x7ffffd3be68a ``` argvはポインタを格納する配列です。配列の各要素には文字列を指すアドレスが入っています。