Help us understand the problem. What is going on with this article?

参照型変数にバインドされた領域

More than 3 years have passed since last update.

「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

つまり、参照型オブジェクトにバインドされた記憶領域には、言語レイヤーからはアクセスできないように文法的に縛りがかけられているらしい。そもそも、その領域には何の値が格納されているのか。それが気になったので、以下のようなテストコードを書いてみた。

sample.cpp
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セクションをダンプしてみる。

.rodataセクションのダンプ
$ readelf -x 15 ./sample
  セクション '.rodata' の 十六進数ダンプ:
  0x080487d8 03000000 01000200 34a00408

アドレス「0x80487e0」に格納された値を確認すると、リトルエンディアンで「34a00408」と表示されているが、これはシンボルテーブルで確認した変数nのアドレス「0x804a034」と一致する。

試しに、アドレス「0x80487e0」に格納された値をポインタ値とし、そのポインタ値を間接参照して取得した値を出力するコードを書いてみる。

sample.cpp
#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に変更するコードです。

sample.cpp
#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にバインドしたコード例。こうして見ると、部分配列へのポインタを取得する方法よりも、直観的に分かりやすいと感じる。

sample.cpp
#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型配列
×T型=参照型とするT型配列
int a, b, c;
int &r[] = {a, b, c};
コンパイル結果
error: declaration of ‘r’ as array of references
  int &r[] = {a, b, c};
         ^
yz2cm
究極のソリューションとは「人類自体が最初から誕生しないこと」である。もう手遅れだが。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away