こんにちは、ナナです。
この記事は『C++ 参照【関数におけるポインタ渡しと参照渡しの違い】』のおまけとして書いたものです。
C++を扱う上では知らなくてもいいのではないかと思っている内容ですので、興味のある方のみお読みください。
改めて説明すると「参照」とは
とある変数ラベルに対して、異なるラベルを貼りつけることができる機能
であるということです。
この概念は理解できるのですが、私の最初の印象は「違和感」です。「本当にそんなことできるの?」という騙されているような感覚です。
この機能を言語レベルでどのように実現しているか?という点について感覚的に腑に落ちないのです。
結論から言うと、
「参照」とは結局は「ポインタ」で実現されている
という内容になっています。
今回の検証内容は「Visual Stuidio 2019」にて行っています。コンパイラの違いなどによってひょっとしたら異なる実装もあるかもしれません。
参照変数はメモリ領域が確保されない?についての考察
「参照」とは、変数につける別ラベルであるとするならば、参照変数を定義してもメモリは確保されないはずです。
図で示す通り、これこそがエイリアス(別名)と言われる理由ですね。
この事実が本当かどうかをわかりやすく確認するために、グローバル変数として静的メモリ上に同様の変数定義を行ってみます。
#include <stdio.h>
long num = 50;
long & refnum = num;
int main()
{
printf("num :0x%p\n", &num);
printf("refnum:0x%p\n", &refnum);
return 0;
}
num :0x0079A000
refnum:0x0079A000
確かに、参照変数である「refnum」の番地は「num」変数と同一番地との結果です。
それでは次に、mapファイルでも「num」「refnum」がどのように割り当てられるのかを確認してみます。mapファイルは、リンカが生成するシンボルのメモリ割り当てを一覧化したファイルです。
・・・
0003:00001dd0 ___rtc_tzz 00418dd0 MSVCRTD:initsect.obj
0004:00000000 ?num@@3JA 0041a000 main.obj
0004:00000004 ?refnum@@3AAJA 0041a004 main.obj
0004:0000000c ?_RTC_ErrorLevels@@3PAHA 0041a00c MSVCRTD:error.obj
・・・
printf関数での表示アドレスとは異なりますが、mapファイル上では「refnum」が「num」とは異なるメモリ(0x0041a004)に割りついているのがわかります。
printfの結果とのアドレスが異なるのは、おそらく仮想メモリによる仕業かと思いますが、詳細な理由はわかりません。
ただし、mapファイル上は別領域で参照変数にもメモリが割りついているということなんです。
参照変数用のメモリに格納される情報とは?
参照変数には実はメモリが割り当てられているということがわかりました。
では、このメモリにはどんな情報が管理されているのでしょうか?
結果をわかりやすくするため、「num」「refnum」「tmp」の3つを変数定義してみましょう。初期値は、次のように目立つようにしておきます。
#include <stdio.h>
long num = 0x01234567;
long & refnum = num;
long tmp = 0x89ABCDEF;
int main()
{
printf("num :0x%p\n", &num);
printf("refnum:0x%p\n", &refnum);
printf("tmp :0x%p\n", &tmp);
return 0;
}
num :0x0035A000
refnum:0x0035A000
tmp :0x0035A008
それでは、デバッガのメモリウィンドウを使って、実際のメモリの中身を考察してみましょう。
「refnum」が存在すると思われるメモリ場所には、なんと「num」変数の番地情報が格納されているのがわかりますね。
番地情報を格納するメモリといえば、それは「ポインタ変数」ということです。
わかりやすくグローバル変数にて確認していますが、ローカル変数でも同様の結果が得られます。
このように、「参照」の正体は実は「ポインタ変数」であるということなんです。
参照変数はなぜ別ラベルを貼ったかのように見えるのか?
「参照」の正体は「ポインタ」ということなんですが、プログラマーの目には「別のラベルを変数に貼り付けた」という姿として映ります。
なぜ、こんなことが可能なのでしょうか?
「参照」を「ポインタ変数」を使って表現してみる
次のプログラムは「参照」を使ったプログラムを「ポインタ変数」に変更して表現したものです。
int main()
{
int num = 100;
int& refnum = num;
refnum = 200;
printf("num :%d\n", num);
printf("refnum :%d\n", refnum);
printf("&num :0x%p\n", &num);
printf("&refnum:0x%p\n", &refnum);
return 0;
}
num :200
refnum :200
&num :0x00F8FA80
&refnum:0x00F8FA80
int main()
{
int num = 100;
int * refnum = &num
*refnum = 200;
printf("num :%d\n", num);
printf("refnum :%d\n", *refnum);
printf("&num :0x%p\n", &num);
printf("&refnum:0x%p\n", &(*refnum));
return 0;
}
num :200
refnum :200
&num :0x00BEFA2C
&refnum:0x00BEFA2C
「num」と「refnum」は同一番地としてどちらも表示されています。つまり、「参照」のプログラムは「ポインタ」のプログラムで代替可能ということなのです。
ポインタを「参照」に見せるコンパイラの技
このギミックを成立させているのは「コンパイラ」です。コンパイラの手によって「参照」のプログラムはこっそりと「ポインタ変数」として変換されるということです。
皆さんの目には新しい「参照」という機能が追加されたように思わせながら、実は内部的には「ポインタ」を使って実現しているというコンパイラの巧みな技なのです。