Cには「ポインタ(pointer)」という便利で厄介な機能がある。Cの学習者が最初に突き当たる壁だとよく言われる。百戦錬磨のベテランでさえ,ポインタがらみのバグは少なからず経験しているだろう。今回はCのポインタについて調べてみた。
アドレスを保持して何になる?
ポインタは“point + er”→「ポイントするもの」→「指し示すもの」という意味で,メモリー上のデータを間接的に指し示すことが大きな役割だ…ってことは,ご存じだろう。
どのような変数も,使用する前に初期化しなければならない。ポインタ変数*1ももちろん変数のアドレスで初期化する必要がある。例えば,int型の変数をポインタを介して扱うなら,以下のようになる。
int *p; ―― int型のポインタ変数pを宣言
int num; ―― int型の変数numを宣言
num = 123; ―― numに値(123)を代入
p = # ―― pに値(numのアドレス)を代入
ポインタ変数は,変数名の前にアスタリスク記号(*)を付けて宣言する(1行目)。変数のアドレスはアドレス演算子(&)で知ることができるので,4行目の文によってポインタ変数pにint型変数numのアドレスが渡される。ここではじめてポインタpは初期化され,意味のある値を持つことになる。変数numの値を保持する領域が,例えばメモリー上のアドレス1000番地に取られたとすれば,ポインタ変数pにはその1000という値が代入され,pを参照することで「アドレス1000番地に保持されている値=numの保持している値」にアクセスできるようになる。
このように,ポインタ変数はアドレスを代入することで初期化してから利用する。初期化されていないポインタにはゴミ(不定な値)が入っており,それは多くの場合メモリー上の「どこかとんでもない場所」を指している。現在の処理系では,初期化されていないポインタを参照しようとすると,コンパイル時に警告メッセージが出るのが普通だが,昔は何も教えてくれない処理系が多く,ひどい目に遭ったものだ。
高級言語は“アドレス”なんていうほとんど機械語レベルのデータを覆い隠してくれるからこそ「高級=人間に近い」わけであって,そんなややこしいものは見えないほうが幸せなはずだ。わざわざポインタを使う必要はないんじゃないの? という気がする。それに,アドレスがわかったところで,それが何の役に立つの? と思う人もいるだろう。一体どのような用途に使われるのかをもう少し詳しく見ることにしよう。
ポインタで「引数の参照渡し」をする
ポインタの利用例としてよく挙げられるのが,引数の「参照渡し」による関数呼び出しだ。引数に与えた整数を10倍する関数を例に説明しておこう*2。
リスト1のtentimes1は,ポインタを使わず,単に引数の値のコピーを受け取る「値渡し」の関数だ。関数の中で,引数numに自身の10倍を代入している(1)。そして,tentimes2が引数をポインタとして受け取る「参照渡し」の関数だ。こちらも引数numが指し示す場所にある値(Cの構文で“*num”)に自身の10倍を代入している(2)。ポインタ変数は変数名だけだとアドレスを示すが,先頭に*記号を付けると「アドレスの示す場所にある値」を参照できる。
main関数*3ではそれぞれの関数を呼び出した後で戻り値と引数の値を表示させている(図1)。値渡しのtentimes1では,呼び出し元の引数aには何の変化もない。tentimes1には渡されるのは変数aの持つ値のコピーで,関数内で10倍されるのはそのコピーのほうだからである(図2(a))。
一方,参照渡しのtentimes2に渡されるのは,式“&b”によって求められた「変数bのアドレス」である(リスト1の(3))。関数内で10倍しているのは,そのアドレスが指し示す場所にある値,つまり呼び出し側の変数bの値そのものである。結果,呼び出し元の変数bの値が直接書き換わることになる(図2(b))。