「C++のからくり」(スティーブン・R・デイビス)を読みながら、C++の勉強をしています。
で、参照型の使い方は分かったのですが、参照型変数にバインドされた記憶領域には何が格納されているのか?今回はそんな実験です。
##ファイルスコープの参照型変数
コードを書いて試してみて、まずは以下のことが分かった。
- 参照型オブジェクトをオペランドとするアドレス演算子の評価結果は、参照型オブジェクト自体へのポインタではなく、その参照先オブジェクトへのポインタとなる。
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
int n =0x7FFFFFFF;
int &n_r = n;
cout << "&n = " << &n << endl;
cout << "&n_r = " << &n_r << endl;
return 0;
}
$ ./main
&n = 0xbfb1e088
&n_r = 0xbfb1e088
つまり、参照型オブジェクトにバインドされた記憶領域には、言語レイヤーからはアクセスできないように文法的に縛りがかけられているらしい。そもそも、その領域には何の値が格納されているのか。それが気になったので、以下のようなテストコードを書いてみた。
int n = 0x7FFFFFFF;
int &n_r = n;
int型変数nと、それを参照先とする参照型変数n_rを宣言した上記コードをコンパイルし、readelfコマンドで、シンボルテーブルにエントリされた両変数のアドレスを確認する。
$ readelf -s ./sample
61: 080487e0 4 OBJECT GLOBAL DEFAULT 15 n_r
76: 0804a034 4 OBJECT GLOBAL DEFAULT 24 n
参照先変数nが「0x804a034」、参照型変数n_rが「80487e0」にそれぞれバインドされている。つぎに、アドレス「80487e0」の領域を含むセクションを確認する。
$ readelf -S ./sample
35 個のセクションヘッダ、始点オフセット 0x3fe0:
セクションヘッダ:
[番] 名前 タイプ アドレス Off サイズ ES Flg Lk Inf A
[15] .rodata PROGBITS 080487d8 0007d8 00000c 00 A 0 0 4
参照型変数n_rにバインドされた記憶領域は、15番目の.rodataセクション内に存在することが分かったので、.rodataセクションをダンプしてみる。
$ readelf -x 15 ./sample
セクション '.rodata' の 十六進数ダンプ:
0x080487d8 03000000 01000200 34a00408
アドレス「0x80487e0」に格納された値を確認すると、リトルエンディアンで「34a00408」と表示されているが、これはシンボルテーブルで確認した変数nのアドレス「0x804a034」と一致する。
試しに、アドレス「0x80487e0」に格納された値をポインタ値とし、そのポインタ値を間接参照して取得した値を出力するコードを書いてみる。
#include <iostream>
using namespace std;
int n = 0x7FFFFFFF;
int &n_r = n;
int main(int argc, char **argv)
{
std::cout << **(int **)0x80487E0 << std::endl;
return 0;
}
以下のように、参照先変数nの値「2147483647(=0x7FFFFFFF)」が出力された。
$ ./sample
2147483647
以上より、g++実装では、参照型変数にバインドされた記憶領域には参照先へのポインタ値が格納されていることが分かった。
#参照型変数の参照先を変えてみる
参照型変数がファイルスコープ変数の場合、.rodata内の領域にバインドされるため、参照先を変更するとsegvが発生するが、スタック変数なら自由に書き換えられるのでその実験。
以下は、参照型変数n_rの参照先を、変数nから変数mに変更するコードです。
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
int n = 0xdeadbeaf;
int m = 0xbadcafe;
int &n_r = n;
cout << hex;
cout << "n のアドレス : " << &n << endl;
cout << "n_r のアドレス : " << (int **)(&n+2) << endl;
cout << "n_r の格納値 : " << *(int **)(&n+2) << endl;
cout << "n_r の参照結果 : " << n_r << endl;
cout << endl;
*(int **)(&n+2) = &m;
cout << "m のアドレス : " << &m << endl;
cout << "n_r のアドレス : " << (int **)(&n+2) << endl;
cout << "n_r の格納値 : " << *(int **)(&n+2) << endl;
cout << "n_r の参照結果 : " << n_r << endl;
return 0;
}
参照型変数 n_r の参照先を、変数 n から変数 m へ変更した結果、n_rの参照結果が「deadbeaf」から「badcafe」へ変わった。
$ ./sample
n のアドレス : 0xbfce5044
n_r のアドレス : 0xbfce504c
n_r の格納値 : 0xbfce5044
n_r の参照結果 : deadbeaf
m のアドレス : 0xbfce5048
n_r のアドレス : 0xbfce504c
n_r の格納値 : 0xbfce5048
n_r の参照結果 : badcafe
#結局、参照型とは何なのか?
実装上は、参照型変数にバインドされた領域へアクセスする手段をプログラマに提供しないことにより、内部的なポインタ演算の実装を言語レイヤーから隠蔽したものが「参照型」である…と理解した。利用する上では「オブジェクトにバインドされた識別子」と捉えておけばいいかと思う。
ちなみに「参照型変数とは別名である」というのは定義が狭い。なぜなら、式でのみアクセスされる、名前の無いオブジェクト(多次元配列の部分配列など)に参照型変数をバインドさせる、という使い方も可能なので。
以下は、二次元配列listaの2要素目の部分配列を、参照型変数list_rにバインドしたコード例。こうして見ると、部分配列へのポインタを取得する方法よりも、直観的に分かりやすいと感じる。
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
int lista[][3] = {{101,102,103},
{201,202,203},
{301,302,303}};
int (&list_r)[3] = lista[1];
for(int i=0; i<(int)(sizeof(list_r)/sizeof(*list_r)); i++)
cout << list_r[i] << endl;
return 0;
}
$ ./sample
201
202
203
##参照型の制約(メモ)
以下は思いつきで気になったコードを書いてコンパイルしてみた実験メモです。これらは今後の調査課題として、規格を読み込んだのちに、リライトしたいと思います。
const int &n_r = 100; //定数への参照
const int (&list_r)[3] = {100,200,300}; //(C++11)初期化指定子にT型配列
int a, b, c;
int &r[] = {a, b, c};
error: declaration of ‘r’ as array of references
int &r[] = {a, b, c};
^