【配列とポインタ】第8回 そもそも変数とは何か?(2)

今回は、変数の残りのキモである「データの長さ」「データを正しく解釈する方法」についてです。

データの長さ

前回は、メモリ上の特定領域にアクセスするための「位置情報」(アドレス)について書きました。この位置情報を使って、特定領域にアクセスするとします。するとその場所には、以下のようなデータが並んでいたとします(1マス1バイトとします)。

 +---------------------------------------------
 | FF | FF | 2A | 3B | EF | 0A | … | 01 | …
 +---------------------------------------------

目的のデータが格納されている場所は分かりました。しかし、これだけでは、データを取り出したり、代入したりすることができません。

なぜなら、
・参照する場合、どこまで切り出すべきなのか?
・代入する場合、どこまで格納していいのか?
が分からないからです。

例えば、目的のデータが4バイトなら、参照時には「FF FF 2A 3B」というデータを切り出せばいいですが、もし1バイトなら「FF」というデータを切り出すべきです。目的のデータにアクセスするための情報として、切り出すデータのバイトサイズが必要になってきます。

そこで登場するのが、「データ型」です。

int=4バイトの処理系を仮定すると、
目的のデータを(unsigned)int型として切り出すとすると「FF FF 2A 3B」というデータを切り出せばいいし、(unsigned)char型として切り出すとすると「FF」というデータを切り出せばいい、と分かります。

つまり、ソースコードで宣言しているデータ型というのは、コンパイラに「確保するべき、または切り出すべき領域のバイトサイズ」を教えているわけです。

このバイトレングスの情報も、シンボルの名前解決と同様、コンパイル時に型名からバイトサイズに置換されます。例えば、コンパイル済みのオブジェクトファイルを逆アセンブルしてみると、

 804842b:	c7 05 30 a0 04 08 64 	mov    DWORD PTR ds:0x804a030,0x64
 8048432:	00 00 00 
 8048435:	c6 44 24 1b 7f       	mov    BYTE PTR [esp+0x1b],0x7f

みたいに、「DWORD」だの「BYTE」だのといった単語が見つかりますが、これが目的の領域のサイズを指定する修飾子となります。

これで、目的とする領域のサイズが分かりました。

データを正しく解釈する方法

仮に、目的の領域の型が1バイトと分かったとします。当然、「FF」というデータになります。

しかし、これだけでは、まだ「このデータをどう解釈すればいいか?」が分かりません。


例えば「FF」というデータは、さらにブレイクダウンすると、
+------------------------------
| 1111 1111 1111 1111 | …
+------------------------------

というビット列が並んでいることになります。
人間にとって意味のあるデータでも、CPUから見れば0/1が羅列されたビット列だ、という話はどこかで聞いた話ですが。

このビット列を、char型として解釈するとします。すると10進数で「-1」という意味に解釈されます。仮にunsigned char型で解釈すると、「255」という意味に解釈されます。つまり無意味なビット列に対して「正しい解釈」を絞り込むために、「型」という情報が必要となるわけです。これは、「符号拡張」という仕組みで重要となる概念です。

まとめ

メモリ上の特定領域に対して、データを参照・代入する場合、必要となる情報は、

  1. メモリ上の位置情報
  2. 領域のサイズ
  3. データを正しく解釈する方法

この3つの情報をコンパイラやCPUに伝えてはじめて、データの参照・代入が可能となるわけです。後者2つは、「型」で指定する情報です。

なので、なぜC言語ではvoid型変数が宣言できないのかというと、それは位置情報を特定することができても、領域のサイズが分からない以上、参照時にどこまで切り出せばいいのか分からない。また、データを取り出せたとしても、データを正しく解釈する方法が分かりません。また、void型ポインタ変数に値を代入・参照することは可能だけど、void型ポインタを経由して実体を間接参照することができないのも、同じ理由(参照先のデータ型が分からない)からです。