C言語のポインタの構文でつまづきやすいポイントについて紹介し、このように考えるとわかりやすいという私なりの方法も書かせていただきます。規格書の内容を解説しているわけではないのでご承知おきください。
#ポインタとは
ポインタ (pointer)とは、あるオブジェクトがなんらかの論理的位置情報でアクセスできるとき、それを参照するものである。有名な例としてはC/C++でのメモリアドレスを表すポインタが挙げられる。(ja.wikipedia.org)
ふむふむ。ポインタ=メモリアドレスだな。
注)コメントにて指摘あり。
#ポインタ型変数
int *pointer;
はい、これがポインタです。って書いても、何を指しているのかよく分からん。*
がポインタなのか!と思うひともいれば、*pointer
がポインタなのか!と思う人もいるだろう。いや、そもそもポインタってメモリアドレスってさっき言うたやん?これのどこにメモリアドレスがあるんだ?ポインタって何だ!?
とならないためにここでは、「ポインタ型」、「ポインタ型変数」の2つを分けて記す。ちなみに、英文のwikipediaだと、ポインタはオブジェクトと書かれているので、単にポインタと表すときは「ポインタ型変数」のことと考えて良いだろう。
In computer science, a pointer is a programming language object, whose value refers to (or "points to") another value stored elsewhere in the computer memory using its address.(en.wikipedia.org)
C言語の宣言は、変数の型 変数名
なので、変数の型はint *
、変数名はpointer
になる。
pointer
が ポインタ型変数 。*pointer
じゃなくて。そしてint *
が ポインタ型 になる。ところでこの時の*
ってなんて呼べばいいんだろう。ポインタ型修飾子?
型と変数名を明確に分けるために、ポインタ型の宣言を下のように書くとより明確になる。
int* pointer;
でも、
int* pointer1, other;
って書くとotherはint型になる。気をつけよう。
#間接参照
int val;
int* pointer_of_val = &val;
と書くと、pointer_of_valがvalのアドレスで初期化される。
pointer_of_valを使ってvalのメモリにアクセスするには、
*pointer_of_val = 10;
と書く。この時の*
は間接演算子と呼ぶ。そう、**ポインタ型宣言時の*
と間接参照時の*
は違う。**同じ*
であっても役割は逆なのだ。逆なので「int *p;
の時に*p
はint型」とうまいこと覚えられるようになっている。うまく出来てるんだけどそのせいでこの2つが別という意識が働かなくなるのだ。ぐぬぬ。
#引数の参照渡し
関数の引数に値渡しと参照渡しがあると言われるが、関数の引数は値渡ししかできない。
void func(int* pointer){
*pointer = 0;
}
この時、pointerというポインタ型変数が値渡しされたint型へのアドレスを受け取っている。*pointer=0
は値渡しされたアドレスを間接参照して値を書き換えている。このことを便宜的に参照渡しと呼んでいるだけ。参照渡しは文法ではなくて使い方の一種なのだ。
さらに、C++には参照型があるがそれは別のお話。ややこしや。
#配列のポインタ型
int *array_of_pointer[10];
これはintポインタ型の配列なのか。int配列のポインタ型なのか。どっちやねん。
これは配列記号が変数の右側につくせいで生まれる戸惑いだ。
私はこんな風にして読んでいる。我流なので注意。
int *array_of_pointer[10];
↓
int* array_of_pointer[10];
↓
int*[10] array_of_pointer; /* あくまで読み方ね */
実際にこう置き換えられるわけではないが、配列を型の右端に持って行くと、intポインタ型の要素が10個の配列と理解しやすいのではないだろうか。
さて、じゃあこれはどうなるだろう。
int (*pointer_of_array)[10];
これは脳内でこんな風に置き換える
int (*pointer_of_array)[10];
↓
int[10] (*pointer_of_array);
↓
int[10]* pointer_of_array;
()のほうが型定義で優先されるので、先に[10]が左側にやってきてint[10] (*pointer_of_array)
となる。その次に()が解かれて上記のようになる。
これなら、int型の10個の配列へのポインタ型と解釈できるだろう。
このことの改善のためか、C#では
int[] array = new int[5];
というように配列記号を左側に書くようになっている。これでC#好きになった。
#const型へのポインタ型
(この章は分かりにくいので修正しました)
constは右側でも左側でも修飾できる。
/* constが左側を修飾する例 */
int const constant_int1;
/* constが右側を修飾する例(上記と同じ意味) */
const int constant_int2;
ポインタ型と組み合わさると下記のようになる。
/* (constant int)型へのポインタ型 */
int const *pointer_to_constant_int1;
const int *pointer_to_constant_int2;
/* int型へのconstantポインタ型 */
int * const constant_pointer_to_int = NULL;
見慣れてないとわかりにくいことだろう。修飾子は*
の左側か右側かで修飾する対象が異なる。
*
の左側にconstがあれば、ポインタの指す先の型に対して修飾するが、*
の右側にconstがついた時はポインタ型そのものを修飾する。
*
の右側にconstがついた時は、〜(左側のポインタ型)〜 is constで読みかえよう。
/* (constant int)型へのポインタ型 */
(const int)* pointer_to_constant_int1;
(int const)* pointer_to_constant_int2;
/* int型へのconstantポインタ型 */
((int *) is const) constant_pointer_to_int = NULL;
少しはわかりやすくなっただろうか。こうすると、pointer_to_constant_int1(2)は、const int型へのポインタ型なので、*pointer_to_constant_int1=0
としてもコンパイルエラーになる。また、pointer_to_constant_int1=NULL
はエラーにならない。対して、constant_pointer_to_intはint型へのconstポインタ型なので、constant_pointer_to_int=&some_int;
がコンパイルエラーとなる。
constポインタ型は関数の引数で有用なので超基本テクニックである。読み取り専用の参照ができるので、関数を使用する側が渡した値を変更される心配をしなくてよくなるし、constポインタ型でないとconst型のアドレスは渡せなくなってしまう。
ただ、入力専用の参照をconstポインタ型にせずとも、プログラムは動いてしまうのでconst教一族は今日も他人のconstついてないポインタ型でイラつきを覚えるのだ。
#文字列リテラル
構文ではないですがご紹介。
"hello"等の文字列リテラルは、その先頭文字へのアドレスを返す。なので、下記のように受け取れる。
const char* str = "hello";
"hello"のh,e,l,l,oが入ったメモリそのものは、コンパイラがどっかに用意しておいてくれるので、ポインタ変数で先頭アドレスを受け取る。ただし、その"hello"が入ったどっかのメモリは、書き換え禁止の領域なのでconst char*型((constant char)へのポインタ型)で受け取るようにする。
他のリテラルはその値を返すのに、文字列だけアドレスを返すわけだ。特に、文字列型をimmutableなオブジェクトとして扱える言語に先に触ってしまった人は戸惑うんじゃないだろうか。
#おわりに
最後まで読んでくれてありがとうございます。
ポインタを完璧に扱えているプログラムってなかなかないです(ポインタを完璧に扱えるプログラマって少ないです)。T *
引数を読み取り専用にするためにconst T *
にしたりと、C言語は習熟度が上がっていくほど書き方がシンプルじゃなくなっていくせいで、同じ仕様でも習熟度によって出来上がるプログラムに差ができるんですよね。その場は動くのですが、この違いは後々バグの原因になったり、別のプログラマが利用時に戸惑ったりして負の遺産となっていきます。
Rustはmutable指定しない限り読み取り専用の参照になるようになってます。この方針はいいですね。Rust流行ってほしいなぁ。
#追記
ポインタの型の解釈方法を我流で書いちゃいましたが、これは良くなかったかもしれません。
JISのC99規格書を見つけたので、近々真正面から仕様通りの解釈を試みたいと思います。その際はここにリンクを貼ります。
→投稿しました。
const int *p と int * const p の違いを構文規則から理解する