akasaki

Sep 06
Permalink
 ポインタが理解できないというのは良く聞く話だ。誰かが、ポインタを理解できたのは、ポインタがメモリアドレスをもつ変数だということを理解できたときだ、といっていた。全くそのとおりだと思う。

 メモリは1バイトごとにアドレスが割り振られている。最初から1バイトめのメモリは番地1、2バイト目のメモリは番地2、………というふうに。

 ちなみに1バイトは、メモリ八個分だ。メモリ一個はON,OFFの二つの状態を持ち、それによって、0,1を現す。メモリ一個で二通りの状態を保持できる。

 メモリ八個では、00000000,00000001,00000010,00000011…11111111、という感じで、2の八乗、256の状態を持つことができる。

 これによって、アルファベッド一文字を保持することができる。アルファベットは大文字、小文字で50文字くらい。さらに、改行とかタブとかスペースとか記号とかを含めて、256文字に収まる。つまり、1バイトはアルファベット1文字分のデータだ。ちなみに、日本語は256以上の文字があるので、2バイトで一文字を表わす。2バイトは2の16乗、65536の状態を持てる。まあこれだけあれば、世界中のほとんどの文字を表わすことができるので、世界共通の文字フォーマット、UNICODEも2バイトで1文字を表わす。その中には日本語も英語もアラビア語もだいたい全部含まれている。

 なんか日本語一つのために2バイトも使うのはもったいない気がするだろうか。本当は、1.5バイトぐらいあれば日本語は表わせる。しかしコンピュータは、1バイト単位で処理する。そういう決まりなのだ。1.5バイトなんて中途半端なデータは処理しづらいことこの上ないのだ。だから日本語にも一文字二バイト使う。

 無駄話が過ぎた。1バイトはメモリ八個分。で、1バイトごとに番地が振られている。番地1を最初のメモリとすると、番地2はそこから8個進んだ、9個目のメモリ、番地3はそこからさらに8個進んで、17個目のメモリ…、というふうになっている。

 で、ポインタ変数とは、番地を記録しておくためのものだ。

char a;

 と書いたとする。

 char型は1バイトだ。C言語は、1バイトのメモリを確保する。つまりメモリ八個だ。メモリを確保するときは、OSとか、他のアプリとかがまだ使っていない、未使用のメモリを探して、割り当てる。

 確保したメモリは、どこの番地にあるのか、これを知りたいときには、変数名の前に&をつける。

 int a_no_Address = (int)&a

 a_no_Addressはaのアドレスという意味だ。ローマ字だ。

 ちなみに、説明のためにアドレスを無理やりint型に変換している。アドレスもただの数字なので、int型に変換しても問題ない。

 で、これをprintfすればaのアドレスが分かる。ちなみにアドレスという用語と番地という用語がごっちゃになってるけど、どっちも同じ意味だ。

 printf(“%d\n”, a_no_Address);

 実行してみた。 aのアドレスは587582だった。これは、最初から数えて58万7582バイト目のメモリが、aを表わしているということを意味している。これはもちろん環境によって変わる。

 で、このアドレスは、本当はint型だ。アドレスをintに変換しているが、これはもともとそうなのだ。int型は32ビットパソコンでは、4バイト、64ビットパソコンでは、8バイトだ。CPUのビット数によって、int型の大きさは変わる。普通に我々が使っているパソコンは、今のところ32ビットが主流だ。しかしもう少しすれば、64ビットが普通になる。今はその過渡期だ。

 CPUのビット数は、一度に処理できるバイト数を表わす。32ビットパソコンは4バイトを一度に処理できる。64ビットだと8バイトだ。つまり、int型は、一度に処理できるバイト数分のデータを持つ。それが一番効率的だから、普通は整数にint型を使う。

 で、アドレスがint型なのは、CPUが処理しやすいようにそうしているのだ。

 4バイトは約四十億の状態を持てる。32ビットパソコンのint型は4バイトだから、アドレスもまた、四十億までの値を持つことができる。逆にいうと、40億以上のアドレスは持つことが出来ない。32ビットパソコンでは、40億バイト以上のメモリを持つことができない。つまり、4ギガバイト以上のメモリがもてない。

 そんな理由もあって、そろそろメモリ4ギガバイトじゃ足りないから、32ビットパソコンはやめて、64ビットにしましょうという話になっている。

 ものすごく関係ない話をしてしまった。ポインタの話をしよう。

 アドレスは&で取り出せる。しかし、アドレスをint型に入れても、あんまり意味がない。さっき書いたこの文は、普通は書かない作為的な文だ。

 int a_no_Address = (int)&a;

 わざわざint型にキャストしている。普通は、アドレスはポインタ変数に入れる。

 char* a_heno_Pointer = &a

 入ってる値は同じだ。587582。char* 型も int型も4バイトだ。だから、本当はどっちでもいいはずだけど、コンパイラがわかりやすいように、char*型を使う。ちなみにa_heno_Pointerは a へのポインターという意味だ。ローマ字だ。

 で、char*型の変数の前に*(こめ)をつける。

 *a_heno_Pointer

 これは、知ってのとおり、aを表わす。aへのポインタにこめをつけるとaになる。

 *(こめ)は、「その番地にある変数」を表わす。a_heno_Pointerは587582という値が入っている。*a_heno_Pointerは、587582番目の番地にある変数、という意味だ。最初にこんな文を書いた。

 a_heno_Pointer = &a;

 &はその変数のアドレスをとりだすことができる。つまり、587582は、変数aがある番地だ。だから、587582番地にある変数はaだ。

 *a_heno_Pointer は、587582番地にある変数、という意味だ。だから、aを表わすことができる。

 たとえば、

 b = a + a;

 これは、

 b = *a_heno_Pointer + *a_heno_Pointer

 とまったく同じ意味だ。*a_heno_Pointerはaそのものを表わすからだ。

 short b;

 こうすると2バイトのメモリが確保される。short型のアドレスは、short*型の変数に入れる。

 short* b_heno_Pointer = &b;

 short*型も、もちろんint型と同じで、4バイトだ。

 char*型とshort*型を使い分ける意味はなんだろうか。

 それは、差している変数の大きさが違うから使い分けなければいけないのだ。

 &b が10000だったとしよう。10000番地に、bという変数を表わすメモリがある。

 bは2バイトなので、10000から10002までがbを表わすメモリだ。

 b_heno_Pointerには10000が入っているので、*b_heno_Pointerは、10000番地にある変数、つまりbを表わす。

 ここでポイントなのは、short*型は、10000番地から2バイト、つまり10000から10002までのメモリを見ようとすることだ。

 これをchar* 型に入れてみよう。

 char* b_heno_Pointer = (char*)&b;

 こうすると、char*型は指している相手がchar型だと仮定するため、10000から10001までの、1バイトのメモリを見て値を判断する。しかしshort型は2バイトなので、これでは正しい値が得られない。本当は10000から10002までがbを表わすメモリなのに、10000から10001までしか見てくれないのだ。

 だから、ポインタ変数には、さしている変数の番地と、その変数の大きさ、二つの情報が必要なのだ。この二つがあれば、正確に、元の値を復元できる。

 だから、ポインタ変数にも、変数の型を与えてやる必要がある。

 これがポインタの仕組みの全てだ。

 で、ポインタは、関数で使うものだ。

void Set10(int* a){

*a = 10;

}

int main(){

int x = 0;

set10(&x);

printf(“%d\n”,x);

}

 驚くほど簡単なプログラムだ。ちゃんとよめば分かる。でもちゃんと説明すると、Set10関数は、引数に10を代入する。Set10にはxを引数で入れているので、xは10になり、printfすると10と表示される。

 で、これになんでポインタが必要なのか。ポインタを使わずに書いてみる。

void Set10(int a){

a = 10;

}

int main(){

int x = 0;

Set10(x);

printf(“%d\n”,x);

}

 これだと、出力結果は10にならない。なんでか。そのためには関数が何をしているかを知る必要がある。

 Set10(x)と呼び出した。で、Set10関数の中では、xはaという名前になっている。このときなにがおきているか。

 Set10(x);を分解すると、

int main(){

int x = 0;

//ここからSet10

int a;

a = x;

a = 10;

//ここまで

printf(“%d\n”,x);

}

 こんなことをしている。いみがわかっただろうか。

 Set10(x)として、Set10関数にxが渡されるとき、Set10関数はaという変数をわざわざ作り、aにxを代入している。xは最初に0を入れているので、それを代入されて、aは0になる。そしてその後、さらに、aに10を代入している。 これによってaは10になる。しかしxは変わらない。こうしてみると当たり前のことだ。

 関数は、変数をわざわざ作り、そこに渡された引数を代入する。で、その変数に対して処理を行う。そういう風になっているのだから仕方ない。

 で、これをポインタにするとどうなるか。

void Set10(int* a){

*a = 10;

}

int main(){

int x = 0;

set10(&x);

printf(“%d\n”,x);

}

↓変換!!

int main(){

int x = 0;

//ここからSet10

int* a;

a = &x;

*a = 10;

//ここまで

printf(“%d\n”,x);

}

 これをちゃんと読めば分かるが、xがきちんと書き換えられている。

 関数は、ポインタ変数である、aを作る。そこに、xのアドレスを入れる。

 xのアドレスは10000だったとしよう。aには10000が代入される。

 で、*aというのは、「その番地にある変数」を意味する。10000番地にある変数はxだ。

 *a = 10 とすると、10000番地にある変数に10が代入される。つまり、xに10が代入される。

 騙されたと思うだろうか。ごまかされたと思うだろうか。しかしこれが、関数の中で変数を書き換えることができるカラクリの全てだ。ポインタを使うと、関数の中で、与えられた変数を書き換えることができる。

 ポインタについて普通に知っておかなければならない情報はこれが全てだ。なんでこんなこと馬鹿丁寧に説明したくなったんだろうな。