I thought what I'd do was. I'd pretend I was one of those deaf-mutes or should I?

ポインタ・コンプレックス

就職活動していて思ったんですが、やっぱりC言語が業界の基本ですね。一部の企業は別ですけど、やっぱりプログラマとして一番需要のあるのは組み込み系なので処理速度重視なわけですから、C言語かアセンブラかってところだけど、最近はC言語、C++が主流のような気がします。Javaは組み込み系で使えないし。Dはまだまだライブラリが貧弱だし。そんなわけで、Cをやるなら絶対ポインタ、メモリ管理なんかを覚えておかないと駄目でしょう、ってことで、つらつら書きます。

型の話

「型」というと、intとかdoubleとかfloatとかのあれです。intは整数値を入れる箱、floatやdoubleは浮動小数点数(それぞれ単精度、倍精度)を入れる箱というイメージをよくします。箱の大きさが「intなら4バイト」(環境による)などと決まっていることも御存知かと思います。

さて、「ポインタ」も「型」のひとつだと考えましょう。「ポインタ型」も箱ですので、何かを入れます。何を入れるか・・・。これを一言で説明してわかるなら、それで良いんですが、それがわからないから、ポインタがわからないんだと思います。わかってる人は読み飛ばし推奨。答えは「アドレス」なのですが、このアドレスを理解せずにポインタはわからないと思うので、それを説明します。

メモリのアドレス

メモリのことは知っていますよね。256MBとか1GBとかの表記があるあれです。ゲームなんかやろうとすると、「推奨:512MB」とか書かれてありますね。さてさて、そんなメモリですが、いったい何に使われているのかというと、ゲームプログラムを例にとって説明していきます。

ハードディスク(HDD)上にあるゲームプログラムを起動しようとすると、まずそのプログラムのコピーがメモリ(主記憶)上にコピーされます。なんでそんなことするのか、プロセッサ(CPU)はプログラム中に書かれた命令を取り出すのにHDDから読み取るよりもメモリから読み取った方が遙かに早い時間で済ませられるからです。

プログラムが実行されると、たとえば「体力」のようなパラメータがint型の変数として宣言されます。「宣言される」というのは、つまりメモリ上に「体力パラメータ」用の記憶域ができる、ということです。メモリのある部分の数値は体力を示している、そういうことです。

さて、このときメモリ上の「どこが」では困るので、その体力が書かれている部分が「どこか」を示すために「アドレス」を用います。アドレスは、ようは住所です(よく聞く話ですね)。「徳川埋蔵金は神奈川県相模原市光が丘五丁目のローソン、通称ゴローソンに隠した」みたいに、「体力を格納しているのは、0x240c91a だ」という感じでアドレスで示します。アドレスが16進数ということ以外は問題ないと思います。

アドレス体験記〜その1〜

さて、アドレスを実際に体験してみます。次のようなプログラムをコンパイルして実行してみましょう。

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   int hoge = 10;
 6 :   printf("値:%d\n", hoge);
 7 :   printf("アドレス:%p\n", &hoge);
 8 : 
 9 :   return 0;
10 : }
11 : 

7行目のprintfに注目します。まず後ろの「&hoge」、これでhogeのアドレスを知ることができます。hogeがメモリ上のどこに格納されているかを知ることができます。書式指定(整数なら%dとかのアレ)はアドレスを表示する場合「%p」です。int型の%dに相当します。これは特に問題ないかと思います。さて、うちのWindowsXPでは次のようになりました。

値:10
アドレス:0x0012FEE4

アドレスの方は、環境によって違う値を示すでしょう。さて、イメージとしては、メモリ上の住所「0x0012FEE4」に「10」というint型の値が入っています、それでいいです。難しくはないですね。

アドレス体験記〜その2〜

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   int hoge = 10;
 6 :   int piyo = 31;
 7 :   printf("アドレス:%p\n", &hoge);
 8 :   printf("アドレス:%p\n", &piyo);
 9 : 
10 :   return 0;
11 : }
12 : 

今度のプログラムは、変数を2つ宣言しました。そしてそれぞれのアドレスを表示させてみます。

アドレス:0x0012FEE0
アドレス:0x0012FEE4

実行させた環境のint型は4バイトです。そして実行結果を見ていただくとわかると思いますが、int型変数2つのそれぞれのアドレスは0x0012FEE0と0x0012FEE4です。hogeはint型ですから、0x0012FEE0〜0x0012FEE3の4バイトに値が記憶されていることになります。piyoもint型なので、0x0012FEE4〜0x0012FEE7に値が記憶されています。わかりますね?

アドレス体験記〜その3〜

double型にするとどうなるでしょう、ってことを確認するためにdouble型の配列にしてみました。配列の初期化をしている部分は、古いコンパイラだと通らないかもしれません。勘違いされてはいけないので解説。{0.0}とやるとすべての要素が0.0に初期化される・・・のではなく、初めの要素、つまり0番要素が指定した0.0で初期化され残りの初期化されなかった部分は0.0で初期化されます。なので{1.0}とやっても、1.0になるのは0番要素だけです。

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   double array[4] = {0.0};
 6 : 
 7 :   int i;
 8 :   for (i = 0; i < 4; i++)
 9 :   {
10 :     printf("アドレス[%d]:%p\n", i, &array[i]);
11 :   }
12 : 
13 :   return 0;
14 : }
15 : 
アドレス[0]:0x0012FEC8
アドレス[1]:0x0012FED0
アドレス[2]:0x0012FED8
アドレス[3]:0x0012FEE0

double型はうちの環境では8バイトのようです。array[0]の値は0x0012FEC8〜0x0012FECFに、array[1]の値は0x0012FED0〜0x0012FED7に、それぞれ8バイトずつ記憶されていることがわかります。

ポインタ型

本題に入ります。次のプログラムを書いてみましょう

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   int hoge = 10;
 6 :   int* p;
 7 : 
 8 :   p = &hoge;
 9 :   printf("hogeのアドレス:%p\n", &hoge);
10 :   printf("pの値:%p\n", p);
11 : 
12 :   return 0;
13 : }
14 : 

何をしようとしているのかを理解してからコーディングしましょう。で理解したら、できるだけ、このコードを見ずにコーディングしましょう。何をやるのかを考えながらコーディングしないと身に着きませんから(体験談)。

まず6行目です。これが「型の話」で書いたように、ポインタ型を宣言したところです。「ポインタ型の変数p」です。「int型の変数hoge」というとの同じですね。さて、このポインタ型変数には、さっき説明した「アドレス」が入ります。なので、8行目でhogeのアドレスを代入しています。そして確認、pの値がhogeのアドレスと同じかどうか?

hogeのアドレス:0x0012FEE4
pの値:0x0012FEE4

一緒ですねー。で、たとえばの話、大好きな人の住所を覚えていても、その住所が好きな人の住所だっていうことを忘れていては意味がありません。電話番号を覚えていても、誰の電話番号なのかを知らなければ意味がありません。そんなわけで、住所に住む人が誰なのか、電話番号は誰のものなのか、アドレスの指すメモリには何が入っているのかを調べたいときがあります。そこで、これ↓

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   int  hoge = 10;
 6 :   int* p;
 7 : 
 8 :   p = &hoge;
 9 :   printf("%p には、%d が入ってる\n", p, *p);
10 : 
11 :   return 0;
12 : }
13 : 

9行目、printfの第二引数が「*p」になっています。これは、宣言の部分を「int *p」と解釈すればわかると思いますが(勘違いしてはいけないが)、「*p」とすることで、pの指すアドレスに記憶されている値を取り出せます。

0x0012FEE4 には、10 が入ってる

小賢しい真似

次は、ちょっと小賢しい真似をしてみます

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   int hoge = 10;
 6 :   int* p;
 7 : 
 8 :   p = &hoge;
 9 :   hoge = 17;
10 :   printf("*p = %d\n", *p);
11 : 
12 :   *p = 33;
13 :   printf("*p = %d\n", *p);
14 :   printf("hoge = %d\n", hoge);
15 : 
16 :   return 0;
17 : }
18 : 

9行目でhogeの値を17にします。*pは10でしょうか、17でしょうか。ちょっと考えてみてください。pはhogeの住所を示すものでした。これを考えれば10行目のprintfでどう出力されるかがわかると思います。次に12行目で*pを33にしています。これも同じことです。pの指す住所に(半ば無理やり)違う人を押し込んだという感じです。13行目、14行目の出力は当然・・・。結果は次のようになります。

*p = 17
*p = 33
hoge = 33

よくある例題〜交換(swap)〜

さて、少し応用です。じっくり読んでください(詰まり所だと思う)。

 1 : #include <stdio.h>
 2 : 
 3 : void swap(int* a, int* b);
 4 : 
 5 : int main(void)
 6 : {
 7 :   int x = 8, y = 29;
 8 :   swap(&x, &y);
 9 : 
10 :   printf("(x,y)=(%d,%d)\n", x, y);
11 : 
12 :   return 0;
13 : }
14 : 
15 : void swap(int* a, int* b)
16 : {
17 :   int temp = *a;
18 :   *a = *b;
19 :   *b = temp;
20 : }
21 : 

普通、関数を呼び出すとき、引数に与えた変数はコピーされて渡されます。たとえば上の例3行目が「void swap(int a, int b);」となっていた場合、「swap(x, y)」とすると第一引数には、変数xの値がコピーされて関数の引数aに代入された上で関数が呼ばれます。あくまでコピーです。コピーなので、オリジナルには影響を与えません。オリジナルというのは、関数の呼び出し元の変数のことで、今回の場合x,yのことです。ちなみに、関数の引数に「値のコピーが渡される」ことを「値渡し」といいます。

今回の例の場合、引数は「int* a」となっています。これはつまり引数にアドレスを受け取る関数だということです。アドレスを受け取るときも例外なくコピーが渡されます。しかし、コピーとはいってもアドレスのコピーなので、アドレスの指す場所にあるモノは、そのままそこにあるわけです。なぜアドレスを渡すのかは、ひとまず横に置いて・・

アドレスがコピーで渡っていることを確認するためのサンプルを作ってみました。

 1 : #include <stdio.h>
 2 : 
 3 : void foo(int* foo_p)
 4 : {
 5 :   printf("  foo_p = %p\n",  foo_p);
 6 :   printf(" &foo_p = %p\n", &foo_p);
 7 : }
 8 : 
 9 : int main(void)
10 : {
11 :   int  main_x = 10;
12 :   int *main_p = &main_x;
13 :   printf(" main_p = %p\n",  main_p);
14 :   printf("&main_p = %p\n", &main_p);
15 : 
16 :   foo(main_p);
17 : 
18 :   return 0;
19 : }
20 : 

まず、14行目。&main_pは、ポインタ型変数main_pが格納されているメモリ上のアドレスを取り出しているだけです、大丈夫ですよね。ポインタ型といえども、メモリ上に記憶されているわけです。メモリ上にあるのだから、当然そのアドレスが存在します、それを取り出しているだけです。

次、16行目で引数にポインタ型のmain_pを関数fooの引数に渡しています。もちろんコピーが渡されます。コピーされるのはアドレスです。main_xに入っている10という値がコピーされたわけじゃありません。そしてfooに処理が移って、foo_pに入っている値は当然、コピーされたアドレスなのでmain_pと同じアドレスです。

ところが&foo_pは、&main_pとは違う値を示します。これはつまり、foo_pとmain_pがメモリ上の別のアドレスに記憶されているということです。それぞれのアドレスには同じ値(main_xのアドレス)が入っています。次の実行結果で確認してください。

 main_p = 0xfefc98f4
&main_p = 0xfefc98f0
  foo_p = 0xfefc98f4
 &foo_p = 0xfefc98d0

ちょっと待った

これまで、ポインタ型として常に「int*」を使ってきましたが、「double*」も「char*」もあります。自分で定義した構造体にもポインタ型を使えます。でも結局、「どうせ値はアドレスなんだろ?区別しなくてもいいじゃん」って思うかもしれません。しかし、区別しなければいけません。

さっきまでの例だと、「int* p」はint型の変数hogeのアドレスでした。これはポインタpが指す先は「int型」だと言っているのです。じゃあ、例えばpにdouble型の変数のアドレスを代入しようとしてみます。

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   double d = 3.14;
 6 :   int* p = &d;
 7 : 
 8 :   return 0;
 9 : }
10 : 

これはコンパイルエラーです。VisualC++.NET2003コンパイラでは次のようなエラーメッセージが出ました。「'初期化中' : 'double *__w64 ' から 'int *' に変換できません。」つまり、「&d」は「double*」なのに、それを「int*」に代入しようとしているもんだからエラーなわけです。では、

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   double d = 3.14;
 6 :   int* p = (int*)&d;
 7 : 
 8 :   printf("p : %d\n", *p);
 9 :   return 0;
10 : }
11 : 

と、無理矢理キャストしてやります。今度はエラーは出ません。ただ、キャストは危険です。これの実行結果が「3」になってくれるならまだしも、

p : 1374389535

となりました。なぜか? 元々doubleの値が入っている領域をint型だと思い込んで数値を取り出したのです。doubleというと浮動小数点数ですので、int型とのbitの並びは全然違います(詳しくは基本情報処理技術者試験などの本を読めばわかるかと)。なので、int型として取り出そうとした時点でビット列の解釈が誤っているのです。それもこれも無理矢理したキャストのせいなんです。上手くいったとしても、CPUによって実行結果が変わってしまうので他所にプログラムを持っていったときにどこにバグがあるかわからずに悶えてしまいます。

試しに、こんなコードを作ってみました。

 1 : #include <stdio.h>
 2 : 
 3 : int main(void)
 4 : {
 5 :   double d = 3.14;
 6 :   int* p = (int*)&d;
 7 : 
 8 :   printf("p : %f\n", *( (double*)p ));
 9 :   return 0;
10 : }
11 : 

まず、pを「double*」にキャストしてやります。そしてこのキャストしたpの指すアドレスの値を取り出します。printfの書式指定は「%f」です。どうなるのでしょう? やはりdouble*でキャストしているから、pの指すアドレスの値はdouble値として解釈されるのでしょうか? 結果は・・・

p : 3.140000

うまくいっていますね。ただ、これは値を一度もいじっていないから上手くいっているだけです。もし、7行目で「*p = 20;」などとした日には、値の保障ができなくなります。ま、実際やってみてください。ひょっとしたら上手くいくかもしれませんけど、ほとんどの場合、おかしな値が出てきます。

これだけ

と、こんなところで基本はすべて終わりです。これだけです。ま、「言うはやすし」ですね。あ、ポインタが何に役立つのかを書いていませんでした。はっきり言って、さっきの例題「よくある例題〜交換〜」のように、変数のアドレスを引数に渡して、渡した先で値を変更してもらう、なんてことで使うことは少ないです。むしろ「変更すんなよ!?」という場面の方が遙かに多い。

そんな場面でなく、サイズの大きい自前の構造体を関数に渡す場面で、よくポインタを使います。というか、使わない人は嫌われ、プログラマとしての腕を疑われ、人間性を疑われ・・(泣)。

覚えておくべきことは、関数の引数には必ずコピーが渡されるということです。たとえ、1GBもあるデカい構造体を関数に渡す場合もコピーです。関数呼び出しのたびに1GBコピーです。「時間かかってしゃあないわ」とつっこむべき所です。しかしポインタを使えば1GBのコピーをとらなくても数バイトのアドレスのコピーで解決します。必要なのは、構造体に含まれているメンバの値であることがほとんどです、わざわざコピーをとる必要がないのが世の常です(ホントか?)

Comments are welcome! We can be reached at whoinside_reshia@hotmail.co.jp
2005/05/06 04:02:06 UTC