【配列とポインタ】第9回 配列とは概念である

前回までで、メモリ上のデータの参照/代入に必要な情報は、「位置」「長さ」「解釈の仕方」だ、と書きましたが、逆に言うと、その3つさえ分かれば、何でもできてしまいます。それがC言語の恐ろしいところですが…

以下は、アドレス0x804A024に位置する1バイトのデータをchar型として解釈するコードです。

  1 /* sample.c */
  2 #include <stdio.h>
  3 
  4 unsigned char   a;
  5 
  6 int main(void)
  7 {
  8     a = 255;
  9 
 10     printf("%d is located in %p(a)\n", a, &a);
 11     printf("*(char *)8048024 >>> %d\n", *(char *)0x804A024);
 12 
 13     return 0;
 14 }
# 実行結果
255 is located in 0x804a024(a)
*(char *)8048024 >>> -1

変数シンボルを使わずに、メモリアドレスを直に指定して目的の領域を参照しています。10進数の「255」は8ビット長の2進数で「1111 1111」ですが、これを符号ありchar型として解釈すると、10進数で「-1」になります。

ほかにも、

  1 /* sample.c */
  2 #include <stdio.h>
  3 
  4 int main(void)
  5 {
  6     char n1;
  7     unsigned char n2 = 255;
  8 
  9     printf("n2 is %d\n", n2);
 10     printf("n1's neigbor is %d\n", *(&n1+1));
 11 
 12     return 0;
 13 }
# 実行結果
n2 is 255
n1's neigbor is -1

char型変数n1へのポインタに1を加算した領域にあるデータを、char型として参照しています(つまり、n1の隣の領域のデータを除いている)。結果、n2の領域を覗いていることになります。これはあくまで、数値型の局所変数が宣言順にスタックのアドレス低位から隣接して格納されているから可能なワザです。あと、スタック領域の境界調整も影響してくるので、これはたまたまうまく行ったにすぎません。

その他に、n1の隣のn2を覗く書き方としては、

 10     printf("n1's neigbor is %d\n", (&n1)[1]);

という記法も可能です(実験目的以外でやってはいかんですが)。

しかし、n1は配列ではない変数なのに、なぜ添字指定が許されるのか?それは、添字がつけられるのは配列変数だけ…というのは思い込みで、単に添字指定が「*(p+i)」のシンタックスシュガーであるからです。今まで我々が配列の各セルにつけてきた添字というのは、あれはアドレス計算に使っているだけです。

つまりですよ。計算機からの視点では、「配列ってなにそれ食えるの?」なのですよ。計算機は、参照先のアドレスが計算できて、型を指定してもらえれば、なんでもできてしまう。言ってみれば、

配列とは概念である

のです。

ポインタはC言語特有の機能だ、とよく言われますが、たまには逆アセンブルしたコードを見てあげてご覧なさい。バイナリに変換されれば、中の人がやってるのは、結局「ポインタによるアクセス」それだけです。アドレスを計算して、その領域にあるデータをロードしてストアして…って、やってるのはそれだけです。ソースコードでは、メモリアドレスはシンボルに置換されて隠蔽されているだけで、ポインタが特殊機能というより、本当は「ポインタが主」なのです。