web-dev-qa-db-ja.com

C ++のリファレンスは、メモリ的にどのように見えますか?

与えられた:

int i = 42;
int j = 43;
int k = 44;

変数のアドレスを見ると、それぞれが4バイトを占めることがわかります(ほとんどのプラットフォーム)。

ただし、次の点を考慮してください。

int i = 42;
int& j = i;
int k = 44;

変数iは実際に4バイトを使用しますが、jnoneを使用し、kはスタックで4バイトを使用します。

ここで何が起きてるの? jは実行時に存在しないようです。また、関数の引数として受け取る参照はどうなりますか? mustはスタック上にいくらかのスペースを必要とします...

そして、その間、配列や参照を定義できないのはなぜですか?

int&[] arr = new int&[SIZE]; // compiler error! array of references is illegal
45
Yuval Adam

参照 j が出現するすべての場所で、 i のアドレスに置き換えられます。したがって、基本的に参照コンテンツアドレスはコンパイル時に解決され、実行時にポインタのように逆参照する必要はありません。

i

void function(int& x)
{
    x = 10;
}

int main()
{
    int i = 5;
    int& j = i;

    function(j);
}

上記のコードでは、 j main stackでスペースを取るべきではありませんが、 function の参照 x は、スタック上で行われます。つまり、 function j を引数として呼び出すと、 i のアドレスが function のスタックにプッシュされます。コンパイラは、メインスタックのスペースを j のために予約できます。

配列部分について、標準は言う::

C++標準8.3.2/4:

参照への参照、参照の配列、参照へのポインタがあってはなりません。

参照の配列はなぜ不正ですか?

51
AraK

C++のリファレンスは、メモリ的にどのように見えますか?

そうではありません。 C++標準は、それがどのように実装されるべきかではなく、どのように動作するべきかについてのみ述べています。

一般的なケースでは、コンパイラーは通常、参照をポインターとして実装します。しかし、彼らは一般的に参照が指すかもしれないものについてのより多くの情報を持ち、それを最適化のために使用します。

参照の唯一の要件は、参照されるオブジェクトのエイリアスとして動作することです。したがって、コンパイラがこのコードに遭遇した場合:

int i = 42;
int& j = i;
int k = 44;

表示されるのは、「変数iへのポインターを作成する」ではなく(コンパイラーがそれを実装することを選択する場合もあります)ではなく、「シンボルテーブルにjiのエイリアスになりました。 "

コンパイラーはjの新しい変数を作成する必要はありません。jが今後参照されるときは常にそれを実際にスワップアウトしてi代わりに。

参照の配列を作成することに関しては、それは役に立たず意味がないのでそれを行うことはできません。

配列を作成すると、すべての要素がデフォルトで作成されます。参照をデフォルトで作成するとはどういう意味ですか?それは何を指していますか?参照の重要な点は、別のオブジェクトを参照するためにinitializedを再実行することです。その後、再挿入することはできません。

したがって、それが可能であれば、-nothingへの参照の配列が作成されます。そして、それらを参照に変更することはできませんsomethingはすでに初期化されているためです。

41
jalf

アセンブリを使用してこれを説明して申し訳ありませんが、これは参照を理解するための最良の方法だと思います。

_    #include <iostream>

    using namespace std;

    int main()
    {
        int i = 10;
        int *ptrToI = &i;
        int &refToI = i;

        cout << "i = " << i << "\n";
        cout << "&i = " << &i << "\n";

        cout << "ptrToI = " << ptrToI << "\n";
        cout << "*ptrToI = " << *ptrToI << "\n";
        cout << "&ptrToI = " << &ptrToI << "\n";

        cout << "refToI = " << refToI << "\n";
        //cout << "*refToI = " << *refToI << "\n";
        cout << "&refToI = " << &refToI << "\n";

        return 0;
    }
_

このコードの出力は次のようになります

_    i = 10
    &i = 0xbf9e52f8
    ptrToI = 0xbf9e52f8
    *ptrToI = 10
    &ptrToI = 0xbf9e52f4
    refToI = 10
    &refToI = 0xbf9e52f8
_

逆アセンブリを見てみましょう(私はこれにGDBを使用しました。ここで8、9、10はコードの行番号です)

_8           int i = 10;
0x08048698 <main()+18>: movl   $0xa,-0x10(%ebp)
_

ここで_$0xa_は、iに割り当てている10(10進数)です。ここでの-0x10(%ebp)は、_ebp register_ –16(decimal)の内容を意味します。 -0x10(%ebp)は、スタック上のiのアドレスを指します。

_9           int *ptrToI = &i;
0x0804869f <main()+25>: lea    -0x10(%ebp),%eax
0x080486a2 <main()+28>: mov    %eax,-0x14(%ebp)
_

iのアドレスをptrToIに割り当てます。 ptrToIは再びアドレス-0x14(%ebp)にあるスタックにあります。つまり、ebp – 20(10進数)です。

_10          int &refToI = i;
0x080486a5 <main()+31>: lea    -0x10(%ebp),%eax
0x080486a8 <main()+34>: mov    %eax,-0xc(%ebp)
_

今ここにキャッチがあります! 9行目と10行目の逆アセンブリを比較すると、行番号10の-0x14(%ebp)-0xc(%ebp)に置き換えられていることがわかります。-0xc(%ebp)refToIのアドレスです。スタックに割り当てられます。ただし、アドレスを知る必要がないため、コードからこのアドレスを取得することはできません。

そう;参照はメモリを占有します。この場合、ローカル変数として割り当てているため、スタックメモリです。それはどのくらいのメモリを占有しますか?ポインタが占めるのと同じくらい。

ここで、参照とポインタにアクセスする方法を見てみましょう。簡単にするために、アセンブリスニペットの一部のみを示しました

_16          cout << "*ptrToI = " << *ptrToI << "\n";
0x08048746 <main()+192>:        mov    -0x14(%ebp),%eax
0x08048749 <main()+195>:        mov    (%eax),%ebx
19          cout << "refToI = " << refToI << "\n";
0x080487b0 <main()+298>:        mov    -0xc(%ebp),%eax
0x080487b3 <main()+301>:        mov    (%eax),%ebx
_

上記の2つの行を比較すると、驚くべき類似性がわかります。 -0xc(%ebp)は、ユーザーがアクセスできないrefToIの実際のアドレスです。簡単に言えば、参照を通常のポインタと考える場合、参照へのアクセスは、参照が指すアドレスで値をフェッチするようなものです。つまり、以下の2行のコードで同じ結果が得られます

_cout << "Value if i = " << *ptrToI << "\n";
cout << " Value if i = " << refToI << "\n";
_

これを比較してください

_15          cout << "ptrToI = " << ptrToI << "\n";
0x08048713 <main()+141>:        mov    -0x14(%ebp),%ebx
21          cout << "&refToI = " << &refToI << "\n";
0x080487fb <main()+373>:        mov    -0xc(%ebp),%eax
_

ここで何が起こっているのかがわかると思います。 _&refToI_を要求すると、-0xc(%ebp)アドレスの場所の内容が返され、-0xc(%ebp)refToiが存在する場所であり、その内容はiのアドレスにすぎません。

最後に、なぜこの行にコメントがあるのですか?

_//cout << "*refToI = " << *refToI << "\n";
_

_*refToI_は許可されておらず、コンパイル時にエラーが発生するためです。

11
Prasad Rane

実際には、参照はポインターと同等ですが、参照の使用を許可する方法に関する追加の制約により、コンパイラーはより多くの場合に「最適化」できるようになります(コンパイラーのスマートさ、最適化設定によっては)もちろん等)。

10
Alex Martelli

参照を初期化する構文がないため、参照の配列を定義することはできません。 C++では、初期化されていない参照は許可されません。最初の質問については、コンパイラーは不要な変数にスペースを割り当てる義務はありません。 jが別の変数を指すようにする方法はないので、これは事実上、関数のスコープ内のiの単なるエイリアスであり、コンパイラーがそれを処理する方法です。

8
Peter Ruderman

どこか他の場所でのみ言及されているもの-コンパイラに参照用のストレージ領域を割り当てる方法:

class HasRef
{
    int &r;

public:
    HasRef(int &n)
        : r(n) { }
};

これは、コンパイラーが単にそれをコンパイル時の別名(同じストレージの代替名)として扱う機会を否定します。

6

参照は、物理的な明示が必要になるまで(つまり、集合体のメンバーとして)、実際には物理的に存在しません。

おそらく参照のため、参照の配列を持つことは違法です。しかし、参照メンバーを持つ構造体/クラスの配列を作成することを妨げるものは何もありません。

私は誰かがこれすべてを言及する標準的な条項を指摘すると確信しています。

3
MSN

それは修正されていません-コンパイラーはケースバイケースでリファレンスを実装する方法に大きな自由があります。したがって、2番目の例では、jをiのエイリアスとして扱います。他に何も必要ありません。 refパラメーターを渡す場合、スタックオフセットを使用することもできますが、オーバーヘッドはありません。しかし、他の状況では、ポインターを使用する可能性があります。

3
Henk Holterman