ポインタとメモリと型(構造体)の関係 (1)

作成:

さて、前回までに BMP 形式の読み込み、書き込みを行う関数を紹介しました。 そこで出てきた小技の説明をしたいと思います。
それにはメモリとポインタの関係についての知識が必要ですので、 ポインタの基本を説明します。というかこれがすべて。

ポインタとメモリと型(構造体)の関係

ポインタというのは C 言語が持っている一つの強力な機能です。
一方で、 C 言語初心者にとって最大の難関ともいえるものです。
しかし、これをマスターしてしまえば非常に便利で、様々な応用ができるようになります。 (実際ポインタだけで1冊の本が出ているくらい奥が深いもの)
まあ、いいことばかりではなくて、ポインタは使い方を誤ると非常に危険な諸刃の刃です・・・
(ポインタはバグの宝庫とまで呼ばれてたりする)

あと、注意事項として以下でプログラムを書いて実行していますが。この結果は処理系依存となります。 このような結果になるという前提でプログラムを書くと、移植性の低いコードになってしまいます。 (ここでは Windows 上の Cygwin の gcc の実行結果を見せています。)

ポインタの正体

と、前置きはこれくらいにして、説明に入ります。
まず、ポインタとは何かということですが、 これはある変数がメモリのどこに納められているかを示すアドレスを格納し、アドレスとして扱うための変数です。 (アドレスそのものもポインタと呼んでもかまいませんが、アドレスはあくまでポインタの値です) つまり、ポインタが持っている値はメモリの位置情報です。 そのため、アドレス変数と呼ばれることもあります。
これを使うことで、変数を介した間接的な方法ではなく、 直接メモリを書き換える、読み出すという操作が可能になります。 (直接と言っても物理的なメモリアドレスをを扱っているわけではないのですが)

ところで、そのポインタの持つ実際の値はどのようなものなのでしょうか?
以下のコードを走らせてみます。

char   a1;
short  a2;
int    a3;
float  a4;
double a5;
printf("char    %d, %d, %p\n",sizeof(a1),sizeof(&a1),&a1);
printf("short   %d, %d, %p\n",sizeof(a2),sizeof(&a2),&a2);
printf("int     %d, %d, %p\n",sizeof(a3),sizeof(&a3),&a3);
printf("float   %d, %d, %p\n",sizeof(a4),sizeof(&a4),&a4);
printf("double  %d, %d, %p\n",sizeof(a5),sizeof(&a5),&a5);

実行結果は

char    1, 4, 0x22fedf
short   2, 4, 0x22fedc
int     4, 4, 0x22fed8
float   4, 4, 0x22fed4
double  8, 4, 0x22fec8

printf の中にある %p はポインタの値を出力するものです。(第1回参照) つまり、実行結果にある 0x22fedf とかいう16進数で示された値がポインタの値です。
これは、メモリに1バイトごとに順に割り当てられた番号なのです。
また、 sizeof の結果を見てもらうとわかりますが、 ポインタはその型にかかわらず4バイト※の大きさを持っています。 (これは環境依存ですが、 32bit CPU ではふつうはこうなっています。)

※ 蛇足:この理由から、 32bit のメモリアドレスを使用する環境では 1プロセスが 4GB 以上のメモリが扱えないという問題があったりします。 システムとして、ではないのは、最近の CPU は仮想記憶という機能を持っていて、 プロセス内で見られるポインタは物理アドレスではなく仮想記憶上のアドレスを指しています (x86系CPUが扱える物理メモリアドレスは 36bit とかだったりする。つまり最大 64GB まで扱える)。 これによってプロセスごとに自由なメモリ空間が扱えるため、 プロセスが異なれば、アドレスが一緒でもかまわないようにできているからです。 また、この機能によって頻繁にメモリのデフラグメンテーションを行わなくても、 複数のアプリケーションを同時に効率よく実行することができます。 特に大きな連続した領域の確保なんて物理アドレスを指定していたら大変ですね。 って、そんな知識全く役に立ちませんよね・・・

ところで、このポインタは型によらず、サイズが等しく。また、表示されているポインタの値も同じよな値が表示されています。
実は、ポインタの持つアドレス情報に型による違いはないのです。
ポインタが持っているアドレス値はその変数が利用しているメモリ領域の先頭を示しているだけです。 先頭の位置がわかると、次に必要なのはどこまでを一つの値として扱うか、それをどう解釈するかが問題となってきますが。 この情報はポインタの型によって決められます。

ポインタと型情報

以下のコードを実行してみます。
註:ここではキャストによって解釈が違ってくるということを示しているだけで、この結果についてはシステムに依存します。

char  c[4] = {0x11,0x22,0x33,0x44};
short *s   = (unsigned short*)c;
long  *l   = (unsigned long*)c;
printf("%#x,%#x,%#x,%#x\n",c[0],c[1],c[2],c[3]);
printf("%#x,%#x\n",s[0],s[1]);
printf("%#x\n",l[0]);

実行結果は

0x11,0x22,0x33,0x44
0x2211,0x4433
0x44332211

ここで何をやっているのかというと、 まず、1要素が 1Byte の char 型配列に(配列は連続したメモリ領域)、 0x11 から 0x44 の数値を入れておき、 その先頭ポインタを別の型にキャストして表示するとどうなるかを示しています。
当然、char 型のままの出力は、初期化したときの 0x11, 0x22, 0x33, 0x44 という数値が出力されています。 では、1要素が 2Byte の short 型にキャストした場合どうなるかというと、 このメモリ領域は、 0x2211 と 0x4433 という二つの値に解釈されています。 また、1要素が 4Byte の int 型にキャストした場合は 0x44332211 という1つの数値に解釈されます。

16進数だとちょっとイメージがわかないかも知れませんので、10進数で表示すると、

char  a[4] = {1,1,1,1};
char  *x   = (unsigned char*)a;
short *y   = (unsigned short*)a;
long  *z   = (unsigned long*)a;
printf("%d\n",*x);
printf("%d\n",*y);
printf("%d\n",*z);

実行結果は

1
257
16843009

メモリ上に 0x01, 0x01, 0x01, 0x01 のデータが入っている場合、
char 型として解釈すると、はじめから 1Byte 分 0x01 を整数と解釈し 1、
short 型として解釈すると、はじめから 2Byte 分 0x0101 を整数と解釈し 257、
long 型として解釈すると、はじめから 4Byte 分 0x01010101 を整数と解釈し 16843009 となります。
すべてのポインタに代入されたアドレス値は同一ですが、出力される値が違っていますね。 このことから、どこからどこまでをひとくくりにするか、どう解釈するかを決めているのが型であることがわかってもらえると思います。

当然浮動小数点数についても同様のことが言えます。 こちらは整数と違ってビットの解釈の仕方が複雑ですが、 メモリ上にあるビット情報は整数などと違いはありません。

蛇足:このポインタのキャストを使って、浮動小数点数のビットの解釈がどうなっているのか等を見ることができます。 まあ、わざわざそんなことをしなくても printf で %x で浮動小数点数を表示すればいいだけです。 %xの16進表示は整数型のみに適用されるものではなく、 指定された変数のビット配列を16進表示してくれるというものなので、浮動小数点数にも適用できますから。 また、signed 型の(符号あり変数の)負の数の表現が2の補数で表現されていることなんかも見ることができます。 ただし、何度もいいますが、これはシステム依存です。 無駄話、長い?・・・すまん

配列

さて、配列ですが、これはこれまでにも出てきましたが、こいつの正体は連続したメモリ領域です。
例えば、int 型 (32bit) 4つからなる配列とは 16byte のひとかたまりの領域が用意されています。
また、それを指す変数は、例えば a[0], a[1], a[2], a[3] と表現されます。 この場合、配列全体を表すaという変数の保持している値は、 この配列のメモリ領域の先頭アドレスになっています。つまりポインタです。
これと、いわゆる変数としてのポインタとの違いは、その変数の指し示すアドレスの値を変更できるかどうかだけです。
そして、 a[1] という添え字表現は *(a+1) と等価です。 ですから配列の添え字表現は配列変数に限らず、ポインタにも使用できますし、 a[1] は 1[a] と表現してもかまいません。 (必然性もありませんし、混乱を招くので、そんな表現をするのはおすすめしませんが)

ところで、この *(a+1) という表現、というか、いわゆるポインタ演算ですが加算減算をするときの単位はどうなっているのでしょうか?
これは型情報から得られるサイズが1単位になっています。
以下のコードを実行してみます。

char  mem[8];
char  *a;
short *b;
int   *c;
a = (char  *)mem;
b = (short *)mem;
c = (int   *)mem;
printf("size:%d a:%p a+1:%p\n", sizeof(*a), a, a+1);
printf("size:%d b:%p b+1:%p\n", sizeof(*b), b, b+1);
printf("size:%d c:%p c+1:%p\n", sizeof(*c), c, c+1);

実行結果は

size:1 a:0x22fee8 a+1:0x22fee9
size:2 b:0x22fee8 b+1:0x22feea
size:4 c:0x22fee8 c+1:0x22feec

元の値が等しいポインタに対して、同じ演算を行ったにもかかわらず、 その値が異なっています。
a というポインタに対し、a+1という演算はそのポインタが保持している値に対し、 sizeof(*a) だけ加算していることがわかります。

次回へ続く・・・