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

ネームテーブルの参照順序(1)(C/C++)

今回は、「ネームテーブルの参照順序」です。

前々回、前回に登場した「ネームテーブル」ですが、
前々回に書いた通り、これは同時に複数存在する可能性があります。
「複数のネームテーブルに同じ名前があったらどうなる?」というのは、
今まで軽く触れただけで詳しくやっていなかったので、それについて今回詳しく書きます。

<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
< 22>
< 23>
< 24>
< 25>
< 26>
< 27>
< 28>
< 29>
< 30>
< 31>
< 32>
< 33>
< 34>
< 35>
< 36>
< 37>
< 38>
< 39>
< 40>
< 41>
< 42>
< 43>
#include <stdio.h>

int n=0;//(1)グローバルネームテーブルのn

int func1(void){//(2)
   
int n;//(3)func1ネームテーブルのn
   
n=10;//(4)
   
printf("func1:%d\n",n);//(5)
   
return n;//(6)
}//(7)
int func2(void){//(8)
   
if(n){//(9)
      
int n;//(10)func2内のifネームテーブルのn
      
n=50;//(11)
      
printf("func2-1:%d\n",n);//(12)
   }//(13)
   
printf("func2-2:%d\n",n);//(14)
   
n=20;//(15)
   
printf("func2-3:%d\n",n);//(16)
   
return n;//(17)
}//(18)
int func3(void){//(19)
   
printf("func3:%d\n",n);//(20)
   
return n;//(21)
}//(22)
int main(void){//(23)
   
int n;//(24)mainネームテーブルのn
   
n=30;//(25)
   
func1();//(26)
   
func2();//(27)
   
func3();//(28)
   
printf("main1:%d\n",n);//(29)
   
if(n){//(30)
      
int n;//(31)main内のifネームテーブルのn
      
n=40;//(32)
      
printf("func1():%d\n",func1());//(33)
      
printf("func2():%d\n",func2());//(34)
      
printf("func3():%d\n",func3());//(35)
      
printf("main2:%d\n",n);//(36)
   }//(37)
   
printf("main3:%d\n",n);//(38)
   
return 0;
}
//(39) 
実行結果:
func1:10
func2-2:0
func2-3:20
func3:20
main1:30
func1:10
func1():10
func2-1:50
func2-2:20
func2-3:20
func2():20
func3:20
func3():20
main2:40
main3:30

今回は実行結果もソースも長めですが、気を抜かずに行きます。
さて、今回のソースに出てくる変数名は全て n というかなりややこしい編成になっています。
このような編成を故意にやることはあまりないと思いますが、
結果的にこのような編成が生まれるケースは存在します。
ネームテーブルと実体をコンパイラがどのように使うかを正しく覚えておかないと、
後々痛い目を見ることになると思うので、ここで基本を覚えてください。


まず、コンパイル時点で処理されるネームテーブルについて、追いかけていきます。
ネームテーブルの処理はソースの先頭から一直線に行われます。

まず、(1)3行 int n 「グローバルネームテーブルの n 」変数定義です。
これは関数の外に書いてあり、 static 装飾されていないので、
「グローバルネームテーブル」に登録されます。

グローバルネームテーブル(ソース上から定義したもの)
名前型名相対ID
nint0


続いて、(2)5行 int func1(void){ 関数定義の開始です。
ここで、ネームテーブル「 func1 関数ブロック」が作られます。
同時に、 func1 関数がグローバルネームスペースに登録されます。

関数がネームスペースに登録される場合、「関数オブジェクト」という形で登録されます。
変数と違って関数は都度メモリを確保する必要がない(コンパイルした後一つあれば十分である)ため、
相対IDの設定が変数とは独立しています(実行時にも別々のテーブルを参照します)。

関数の呼び出しに相対IDを用いるかどうかはコンパイラに依存するような気もするのですが、
とりあえず Visual C++ は相対IDを用いて設定するようになっているようです。

「関数オブジェクト」はその名の通り「関数」を表すものです。
「関数オブジェクト」は「関数ポインタ」への暗黙変換が可能で、
普通に使用している限り、ほぼ自動的に関数ポインタへの変換が行われます。
以前、printfなどの関数の型を関数ポインタとして表に記述しましたが、あれは暗黙変換の後、ということになります。


続いて、(3)6行 int n 変数定義です。
今ネームテーブルは「グローバル」と「 func1 関数ブロック」がありますが、
コンパイラは記述位置にもっとも近いネームテーブルに名前を登録します。
「 func1 関数ブロック」の方が近くで作られたので「 func1 関数ブロック」に登録します。

グローバルネームテーブル(ソース上から定義したもの)
名前型名相対ID
nint0
func1int(void)5

func1 関数ブロック ネームテーブル
名前型名相対ID
nint0
グローバルとは違うテーブルなので多重定義エラーにはなりません。
相対IDが両方とも0になってますが、コンパイラは実行時には必要に応じてメモリを確保します。
そのため、異なるネームテーブルに所属していると別々のタイミングでメモリが確保されます。
当然、同じIDのメモリは同時に確保できないので、実際に確保されたメモリ領域のIDは異なることになります。
結果として、相対IDが同じ0であっても、計算される絶対IDは異なることになります。(詳細は次回)


続いて、(4)7行 n=10 です。
ここで参照される n は、「登録されている n の中でもっとも近いネームテーブルのもの」とされています。
現在、ネームテーブルには両方とも n が登録されていますが、近い方、ということなので
「 func1 関数ブロック」にある n の方を指定したものとして処理されます。
(5)8行、(6)9行も同様なので、今は省略します。


(7)10行 func1 関数ブロックの終点です。
「 func1 関数ブロック」ネームテーブルはコンパイル時にはここで退場することになります。
実行時には func1 関数を実行する時に再登場します。



続いて、(8)11行 int func2(void){ 関数定義の開始です。
ここで、ネームテーブル「 func2 関数ブロック」が作られます。
また、「グローバル」ネームテーブルにも func2 が登録されます。


続いて、(9)12行 if(n){ です。
まず、条件式に使われている n の参照先を探します。
今のネームテーブルは以下のような状態です。

グローバルネームテーブル(ソース上から定義したもの)
名前型名相対ID
nint0
func1int(void)5
func2int(void)10

func2 関数ブロック ネームテーブル
名前型名相対ID
何もありません
「 func1 関数ブロック」ネームテーブルはさっき退場したのでもう出てきません。
今回は、「グローバル」ネームテーブルにしか n が登録されていません。
ので、今回は「グローバル」ネームテーブルの n を指定したものとして処理されます。

と同時に、この場所は if ブロックの開始地点でもあるので、
「12行目ブロック(func2内のif)」ネームテーブルが作成されます。


続いて、(10)13行の int n 変数定義です。
今度は「グローバル」「 func2 関数ブロック」「12行目(func2内のif)ブロック」の3個のネームテーブルがありますが、
前回同様にもっとも近い「12行目ブロック(func2内のif)」が選択され、同ブロックに登録されます。

グローバルネームテーブル(ソース上から定義したもの)
名前型名相対ID
nint0
func1int(void)5
func2int(void)10

func2 関数ブロック ネームテーブル
名前型名相対ID
何もありません

12行目(func2内のif)ブロック ネームテーブル
名前型名相対ID
nint0


続いて、(11)14行 n=50 です。
今回も n は「グローバル」「12行目(func2内のif)ブロック」二つに登録されています。
複数登録されている場合はやはり近い方を選ぶので、「12行目ブロック(func2内のif)」が指定されたものとされます。
(12)15行も同様なので省略します。


(13)16行は、「12行目からの if ブロック」の終点です。
「12行目(func2内のif)ブロック」ネームテーブルはここで退場となります。


(14)17行は、再び n が出現しますが、先ほど「12行目(func2内のif)ブロック」ネームテーブルが退場したため、
現在のネームテーブルは以下のような状態になっています。

グローバルネームテーブル(ソース上から定義したもの)
名前型名相対ID
nint0
func1int(void)5
func2int(void)10

func2 関数ブロック ネームテーブル
名前型名相対ID
何もありません
今度は「グローバル」ネームテーブルにしか n が登録されていないので、
「グローバル」ネームスペースの n を指定したものとして処理されます。
(15)18行〜(17)20行も同様なので省略します。


(18)21行は、 func2 関数ブロックの終点です。
何も登録されていませんが、「 func2 関数ブロック」ネームテーブルはここで退場です。


(19)22行は、 int func3(void){ 関数定義の開始です。
ここで、ネームテーブル「 func3 関数ブロック」が作られます。
さらに、「グローバル」ネームテーブルに func3 が登録されます。


(20)23行、(21)24行の n は、この時点のネームテーブルの状態は次のようになっているので、
「グローバル」の n を指定したものとして処理されます。

グローバルネームテーブル(ソース上から定義したもの)
名前型名相対ID
nint0
func1int(void)5
func2int(void)10
func3int(void)15

func3 関数ブロック ネームテーブル
名前型名相対ID
何もありません


そして(22)25行、 func3 関数ブロックの終点です。
「 func3 関数ブロック」ネームテーブルは何もされないまま退場です。


そして(23)26行、ようやく main 関数ブロックの開始です。
ここで「 main 関数ブロック」ネームテーブルが作成されます。
加えて main がネームテーブルに登録される・・・というわけではありません。
実は main はC/C++言語の開始位置として決められているので、最初から宣言だけされていたりします。
(注:コンソールアプリの場合。WindowsアプリケーションやDLLの開始位置は別の名前になっています)
なので、実は最初から登録されていたりするのですが、今はそこまで気にしなくてもいいです。


(24)27行は、ふたたび n の変数定義です。
今回も、もっとも近い場所に登録するので、「 main 関数ブロック」ネームテーブルに登録されます。
登録後のネームテーブルの状態は次のようになっています。

グローバルネームテーブル(ソース上から定義したもの)
名前型名相対ID
nint0
mainint(void)0
func1int(void)5
func2int(void)10
func3int(void)15

main 関数ブロック ネームテーブル
名前型名相対ID
nint0


(25)28行の n は、ネームテーブルが上記の通りになっているので、
「 main 関数ブロック」の n が指定されたものとして処理されます。


(26)29行〜(28)31行は各関数を呼び出すように指示されます。
この時も「グローバル」ネームスペースから func1 func2 func3 それぞれが検索され、
実際に呼び出すべき関数との対応付けが行われます。


(29)32行は(25)と同様なので、省略します。
(30)33行の n は(25)(29)と同様に、「 main 関数ブロック」の n が指定されたものとして処理されます。
そしてブロックの開始なので「33行目(main内のif)ブロック」ネームテーブルが作成されます。


(31)34行は、またしても int n 変数定義です。
今回ももっとも近い「33行目(main内のif)ブロック」ネームテーブルに登録されます。

グローバルネームテーブル(ソース上から定義したもの)
名前型名相対ID
nint0
mainint(void)0
func1int(void)5
func2int(void)10
func3int(void)15

main 関数ブロック ネームテーブル
名前型名相対ID
nint0

33行目(main内のif)ブロック ネームテーブル
名前型名相対ID
nint0


(32)35行の n も、これまで通りに登録されているネームテーブルのうち、
もっとも近いネームテーブルである「33行目(main内のif)ブロック」を選択したものとして解釈されます。


(33)36行〜(35)38行は、再び func1 func2 func3 各関数を呼び出し、その戻り値を表示します。
この項目は次回。


(36)39行は(32)同様なので、省略します。

(37)40行は「33行目からの if ブロック」の終点です。
ここで「33行目(main内のif)ブロック」ネームテーブルは退場となります。


(38)41行の n は、現在のネームテーブルの状態(↓)から、
「 main 関数ブロック」の n を指定したものとして処理されます。

グローバルネームテーブル(ソース上から定義したもの)
名前型名相対ID
nint0
mainint(void)0
func1int(void)5
func2int(void)10
func3int(void)15

main 関数ブロック ネームテーブル
名前型名相対ID
nint0


(39)43行で main 関数ブロックが終了です。
ここで「 main 関数ブロック」ネームテーブルも退場となり、ソースコードも終点です。



さて、長かったですがこれでコンパイル編が終わりました。
まだ実行時編をやってないわけなので、これで半分くらいでしょうか。(笑)
長くなりすぎるので実行時編は次回にします。

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

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

最終更新 2008/10/17