[前へ]   [目次へ]   [次へ]

宣言、定義の正体(3)(C/C++)


今回は、前回に引き続き「変数の作成時に裏で行われていること」です。
今度は、配列変数を作って使う時の話です。

今回流れを追うのは、以下のソースです。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
#include <stdio.h>

int main(void){
   
int n[10];//(1)
   
n[5]=10;//(2)
   
printf("%d",n[5]);
   
return 0;
今回も(1)4行目からです。
まず、前回同様 main 関数ブロックのネームテーブルに n という名前が登録されます。
今回は n の型は int[10] なので、前回とは設定される型名が異なります。

配列型を設定された場合は、単体の場合とは以下のように処理が変わります。
●確保するメモリサイズは、「配列の元の型の大きさ」×「配列の要素数」として計算されます。
●実体は要素数分作られます。この時各実体の「実体ID(相対/絶対両方)」は必ず隣接して配置されます。
●ネームテーブルに設定される「実体ID(相対)」は作られた実体のうち、一個目の要素の「実体ID(相対)」が設定されます。

この場合、設定された型は int[10] なので、「配列の元の型の大きさ」は int 型の4、「配列の要素数」は10です。
なので、4×10で40バイトが確保されます。
「隣接して配置される」というのは、実体IDがバイト単位で管理されているためです。
というのも、大きさが1バイトでない実体の場合、
実体IDを連番にすると同じバイトを使ってしまい、値を独立して保存できなくなってしまいます。
そのため、実体IDは同じバイトを使わないように、 実体の大きさ分づつ離れて連番 になります。
(unionはこれを敢えて同じバイトを使うように設定しますが、当分使うことはないでしょう)

イメージとしては、以下のような感じですね。(一番上の列がID/メモリ上のバイト、実体の大きさは4バイトとします)
↓連番にした場合
|00|01|02|03|04|05|06|07|08|09|10|11|12|
|実体A      |
   |実体B      |
      |実体C      |
   |***********|←の区間が同じバイトを多重して使ってしまう


↓実際に行われる「隣接配置」
|00|01|02|03|04|05|06|07|08|09|10|11|12|
|実体A      |
            |実体B      |
                        |実体C      |
                                     ←同じバイトを多重して使わないように配置されています
|**|        |**|        |**|←各実体の実体ID(相対)はここを指すので、各実体ID(相対)はA=0,B=4,C=8となります


と、このような理由により、「実体ID(相対)」は「実体の大きさ分づつ離れて連番」となるわけです。

さて、今回のような配列を作ると、実体は要素数分、つまり10個作られます。
一方で、ネームテーブルには n として一個登録されたのみです。
n に設定されている「実体ID(相対)」は1個目の要素に対するものです。
この時の main 関数ブロックのネームテーブルと実体のリストは以下のようになっています。

ネームテーブル
名前型名相対ID
nint[10]0

main 関数内実体リスト(今回確保されたメモリベース:0x0013FF58)
実体ID(絶対)実体の型保持値
0x0013FF58int不定(未初期化)
0x0013FF5Cint不定(未初期化)
0x0013FF60int不定(未初期化)
0x0013FF64int不定(未初期化)
0x0013FF68int不定(未初期化)
0x0013FF6Cint不定(未初期化)
0x0013FF70int不定(未初期化)
0x0013FF74int不定(未初期化)
0x0013FF78int不定(未初期化)
0x0013FF7Cint不定(未初期化)
今回は確保されたメモリIDは0x0013FF58です。
なので実体ID(相対)から実体ID(絶対)への変換は「0x0013FF58+実体ID(相対)」です。
これ、16進数ですよ。念のため。

と、このように実体はタップリ10個作られてますが、ネームテーブルから示されているのは
確保領域0x0013FF58+0の絶対IDが0x0013FF58の一個だけですね。
他の実体の名前はというと・・・他の実体には名前がないんです。本当に。

では、一体どうやってこれらの実体を使うか、というと、その辺りがポインタの出番なのです。
ポインタというのは実体ID(絶対)を使ってアクセスするので、
実体ID(絶対)さえ分かれば、ネームテーブルに登録されていない、
名前を持たない実体でも問題なくアクセスできます。

ここで、先ほどの「実体の大きさ分づつ離れて連番」というのが利いてきます。
つまり、一番ID値の低い実体の実体IDさえ分かれば、残りは計算で割り出せるという算段なのです。

この辺の計算を裏でやっているのが [ ] 配列添字参照演算子なんですね。
n[0]=0x0013FF58=0x0013FF58+0 +0
n[1]=0x0013FF5C=0x0013FF58+0 +4
n[2]=0x0013FF60=0x0013FF58+0 +8
n[3]=0x0013FF64=0x0013FF58+0 +12
     ↑実体ID(絶対)  ↑確保されたメモリIDの起点
                           ↑nの実体ID(相対)
                              ↑添字から計算される距離(nの元の型の大きさ4×要素番号)

といったように、配列の要素番号と、実体IDの移動距離には「要素番号×配列の元の型の大きさ」という関係があります。

さて、勘のいい人は気付くかも知れませんが、これ、確保した要素数以上の要素番号を指定しても計算できちゃうんです。
そして実際に、 [ ] 演算子は計算してしまうし、その計算結果の位置に実体があるものとして使ってしまえるんです。
これこそが、「配列の確保要素数以上の要素番号を指定すると変にバグる」理由です。

計算できないのならそこで演算エラーになって止まってくれるのでしょうが、
計算できて、しかも使えてしまう(もちろんそこに実体がある保証はない)ので、
他の変数の領域や管理領域を壊したりしてしまうわけですね。

この辺、ポインタの時に最重要なんで、よく覚えておくようにしてください。



次に(2)5行目です。この式を今までよりじっくり見てみます。
A B C B' D E
n [ 5 ]  = 10


通し記号種別
Aint[10]省略配列変数(n)
B演算子( [] )(その他、優先16、結合→)
Cconst int5定数
B'演算子Bの終点
D演算子(=)(2項、算術、優先2、結合←)
Econst int10定数
優先順位から、演算子Bから処理されます。
[ ] 演算子が必要とするのは、
今までは「左辺に配列へのポインタ、中辺に要素番号」としてきましたが、
今回の内容の通り、本当は「左辺に配列の実体ID(絶対)、中辺に移動距離」です。

もっとも、ポインタというのは実体ID(絶対)を格納する型なので、
ポインタの値を取り出せばそれは実体ID(絶対)です。
なので、変わっていないと言えば変わってません。

今回の左辺の n は上記の通り、0x0013FF58になります。
中辺は5で、要素あたりの移動距離は「対象の型1個分のバイト数×要素数」です。
左辺の n は int 型の配列なので、「対象の型1個分のバイト数」は4、要素数は中辺の5です。
なので、移動距離は4*5の20(16進だと0x14)です。
これを左辺の実体ID(絶対)に加算するので、0x0013FF58+0x14=0x0013FF6Cになります。

そして、 [ ] 演算子が返すのは実体への参照です。
実体への参照というのは、「ある実体ID(絶対)を、特定型の実体の実体ID(絶対)であるものとして扱う」
というところまで決めたものです。
この場合は、「0x0013FF6Cは、 int 型の実体である」という情報を持った参照を返します。

そして、 = 代入演算子は「左辺の実体」に右辺を代入しようとするので、
先ほどの「0x0013FF6Cは、 int 型の実体である」参照の情報から「0x0013FF6Cの int 型実体」に
対して代入演算を行うという流れになります。


次回は、前回から詳しく出てきた「ネームテーブル」の参照順序の予定です。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2008/10/17