2009年2月2日月曜日

第二回:ポインタ(1)ポインタを理解する

ソフトウェア開発者コラム、第二回はポインタに関するあれこれを扱っていこうと思う。

ポインタと聞いて苦手意識を持つ人は少なくない。このコラムが苦手意識を払拭するための一助となれば幸いであるが、もしかしたら苦手意識を増してしまう人もいると思われるので、読むかどうかは自己責任でお願いしたい。

ポインタを概念的に理解したい人と言うよりはポインタとはこういうもので有るというしっかりとした定義を知りたい人向けの説明を行っていこうと思う。

1.序章



「ポインタ」の仕組みを考えた人間は天才だとは思うが、天才の考えが一般人に分かりにくいこともまた真理である。

 実際、ポインタの神髄を理解できるかどうかがプログラマの資質に関連してくると思う。また、最近のJAVAやVB等でも表面的にはその姿を隠しながらもポインタの概念が根強く残ってしまっているため、理解しづらい「規則」が出来上がってしまっているのも確かである。

 「ポインタ」を分かりやすく説明するため、様々な試みがなされてきた。箱にものを入れてその番号を渡すとか、ロッカーにものを入れてその鍵を渡す。等の表現で、現実世界の事柄での説明を通してポインタを理解しようとする試みが多くなされている。

ただ、ポインタの概念ではなく、幅や深さを解説する上ではそう言った例えでの説明では破綻を起こす場合が少なくないため、今回はあえて直接的な表現での説明を試み、その後、実際にポインタを使用した様々な話題に触れていこうと思う。


2.変数とポインタに関する直接的説明



プログラム上で値を格納するために使用される識別子を「変数」と呼ぶ。一般的なプログラム言語の場合、変数は「型」を持つ。

 では、変数は実際にどのように格納されているのだろうか?

 ここで、情報処理の初期で勉強した、2進数や16進数の知識が必要になってくる。記憶に無い人は勉強し直して貰いたい。

 コンピュータが数値を保存する場合、データは全てビット単位で保存される。つまり、2進数で保存される。例えば、10進数の123は2進数で表すと


1111011



となる。

これは、コンピュータには判断しやすいかも知れないが、人間が見てもよく分からないので、通常はこれを16進数で表現する。2進数の4桁(4ビット)を16進数だと1桁として扱うことから、2進数から直接変換するのにやりやすいものとしてよく使われる。

16進数で10進数の123を表すと


7B



となる。

 この16進数で2桁(つまり8ビット)を1バイトと呼ぶ。通常はデータはバイト単位で扱うのが容易であるので、一般的な説明はバイト単位で行うことにする。

 この前提条件を元に変数がどのようにコンピュータ上で管理されているかを見ていこう。一番分かりやすいところで、「整数」について扱う。

 1バイトの領域を使って整数を扱うとしたら、どの位の範囲の整数を扱うことが出来るだろうか?

 16進数の00~16進数のFFまでが扱えることになるので、単純に考えて10進数で0~255までが扱えることになる。整数は正の整数だけでなく負の整数も扱わなくてはならないため、負号付き整数の場合には頭のビットが1のものは負の数と見なし、1バイト整数では10進数で-128~127が扱えるようになっている。

日常生活で使う上で-128~127の範囲の整数だけでは子供のお小遣いすら計算できないため、通常は整数を扱う場合には2バイト整数、4バイト整数とバイトを増やして扱える範囲を広げて使うことになるだろう。
 
2バイト整数では-32,768~32,768までが
 4バイト整数では-2,147,483,648~2,147,483,647までが扱える。
 最近はニーズに合わせて8バイト整数などを用意しているコンパイラもある。

 10進数の1,234,567,890を16進数で表現すると


49 96 02 D2   (便宜上1バイト毎に区切ってある)



となる。

 C言語でのint型が何バイトかはコンパイラによって決定されている。DOS時代ではMS-CやBorlandCのint型は2バイトだったが、Windows時代になってからはintで変数を宣言すると4バイトが確保されているように記憶している(恐らくOSが32ビットとなり、CPUも32ビットCPUとなったためと思われる)。今後、int型について触れられる時には4バイトの整数型であるものとする。

 さて、main関数内で以下のように定義したとしよう。


int main()
{
    int a = 1234567890;

    return 0;
}



main関数が実行される際、main関数が使用する領域がメモリ内に作成される。main関数に限らず他の関数が実行される際にも、実行されるたびにその関数用の領域がメモリ内に確保されるが、この領域のことを「スタック領域」とか「コールスタック」と呼ぶ。

 関数内で定義された変数(上記の場合は「a」)はこのスタック領域内にその変数用の格納領域を確保する。先ほどの前提条件で述べたとおり、intは4バイトで有るので、スタック領域内のどこかに4バイト分のメモリが確保され、その中に10進数の1234567890が格納されることになる。

 イメージとしては以下のようになるであろう。



上記の図はスタック領域のイメージである。小さな枠1つが1バイト分のメモリ領域を表している。
 
メモリ内の場所を表現するために、メモリには1バイト毎にメモリ番地(アドレスとも呼ぶ)と言う通し番号がついている。上記の図で左側に書いている数字がメモリ番地の2桁目以降を表しており、上側に書いている数字がメモリ番地の1桁目を表している。

 上記の例では、変数aはメモリ番地??????33~??????36に確保されたと言うことが出来る。

 10進数1234567890を16進数に変換すると49 96 02 D2 となることは前に述べた。上記の例では、下位バイトから順にメモリ内に格納してあり、見た目逆転しているように見える。

Intel系のCPUでは「リトルエンディアン」を採用しているため、このように下位バイトからの書き込みが行われているのであるが、この部分は深く触れないようにしておく。ネットワーク関係の話題でいずれお話しすることになると思う。

 さて、話題を元に戻そう。上記のように変数aがメモリの??????33番地を先頭に4バイトで格納されることが分かった。このメモリ番地「??????33」が実はポインタそのものとなる。

つまり、変数を使用するには、変数が使用する領域をメモリ内に確保する必要が出てくるが、この確保されたメモリの先頭のメモリ番地の事をポインタと呼んでいるのである。

 さて、続いて、以下のソースを参照していただきたい。


int main()
{
    int a = 1234567890;
    int *p = &a;

    return 0;
}



上記の例では、int型のポインタ変数pを定義し、ここに&aを代入している。(ここで「int *p」と書いているのが、しばしば勘違いと複雑さを増す原因になることもある。と言うのはポインタの中身を表示する場合にも「*p」と言う記号を使うからだ。ポインタの中身を参照するための「*p」と変数宣言に出てくる「*」は別物であると考えた方がよい。「int *p」はあくまで変数pをint型のポインタを格納するための変数として定義すると言う記号なのである)

 変数の前に&(アンパサンド)をつけると、その変数のメモリ番地を参照できる。ここでは、メモリ番地を格納するためのポインタ変数pを定義し、そこに変数aのメモリ番地を格納している。
 
このとき、メモリ内は以下のようになるであろう。



変数aの格納領域が??????33~??????36に確保され、変数pの格納領域が??????3C~??????3Fに確保されたものとする。

 変数pは&aの値になる。つまり、変数aが格納した領域の先頭番地となるので、この部分には??????33が入ることになる。ポインタと言えどただの変数である。代入すればその値が入るのは当然のことで、何か特別なものではない。

 さて、上記の様に見ていくと、ポインタ変数pも領域を確保しているわけだから、そのメモリ番地も取得できるのでは?と言う気になる。確かに出来る。ソースで表すと以下のようになる。


int main()
{
    int a = 1234567890;
    int *p = &a;
    int **pp = &p;

    return 0;
}



ここで「int **pp」と変数ppを定義しているが、これは、int型のポインタ変数(int*)のポインタを格納するための変数で有ると言う意味になる。

 このように定義した場合、メモリ内は以下のようになるであろう。



となると、変数ppのポインタも取得できるのではないだろうか...きりがないので、この辺りで止めておこう。

 ここで扱いたいのは、「ポインタ」と難しく言っているが、実は値を格納するための変数に過ぎない。と言うことだ。

 一応、ここまでの説明を終えるに当たり、動作するソースを紹介しておこう。


/* PointerTest.c */

#include <stdio.h>

main()
{
    int a = 1234567890;
    int *p = &a;
    int **pp = &p;

    printf( "a = 0x%08x¥n" , a );
    printf( "&a = p = %p¥n" , p );
    printf( "&p = pp = %p¥n" , pp );
    printf( "&pp = %p¥n" , &pp );

    return 0;
}



実行した結果は以下の通りとなるが、これは各コンピュータによって表示される値はまちまちとなると思われる。


a = 0x499602d2
&a = p = 0xbfbfeccc
&p = pp = 0xbfbfecc8
&pp = 0xbfbfecc4




3.ポインタを利用した簡単な例



ポインタを利用してどのようなことが出来るか、そうした場合の内部的な動きはどうなっているのかを説明していこうと思う。ポインタの実用的な使い方については、次回に回すとして、以下のような単純な例について考えてみよう。


#include <stdio.h>

int main()
{
    int a = 100;
    int *p = &a;

    printf( "a = %d¥n" , a );
    *p += 10;
    printf( "a = %d¥n" , a );

    return 0;
}



上記プログラムの実行結果は以下の通りとなる。


a = 100
a = 110



プログラムを詳しく見てみよう。


int a=100;



と言う文で、変数aを定義し、その中身を100にしている。
 
次に


int *p = &a;



と言う文で、変数aのメモリ番地をポインタ変数pに格納している。

 printfを使ってaを表示し、*pの値を増分してから、再びprintfでaの値を表示している。このプログラムは変数aを直接操作していなくても、変数aの値が変動することを確認することを目的としている。

 そこで、注目できるのは


*p += 10;



と言う部分である。

 pがポインタ変数である場合、*pは「ポインタ変数pが指し示すアドレスにある変数の中身」と言う意味になる。それで、この例においては、*pを操作することは変数aを直接操作することと同じ意味になる。

4.まとめ



今回はポインタの基本に関する説明を、メモリ内の話も含め、直接的に説明した。ポインタとはこういうものと言う程度の説明であるので、これがどのように役立つかは今回の説明だけではピンと来ないと思う。

 次回はポインタを実用的に使用する方法と、ポインタのこの仕組みでなぜそのような動きが可能なのかを述べていく。

 なお、多少の資料は調べているものの、内容に関しては当方の見識を超えないものであるため、技術的に間違った情報がある場合はご容赦及びご指導いただきたいと思う。

0 件のコメント:

コメントを投稿

コメントを承認してから公開しますので即時には反映されません。