9

このコードがあります:

#include <iostream>

class Base
{
public:
    Base() {
        std::cout << "Base: " << this << std::endl;
    }
    int x;
    int y;
    int z;
};

class Derived : Base
{
public:
    Derived() {
        std::cout << "Derived: " << this << std::endl;
    }

    void fun(){}
};

int main() {
   Derived d;
   return 0;
}

出力:

Base: 0xbfdb81d4
Derived: 0xbfdb81d4

ただし、関数 'fun' が Derived クラスで virtual に変更された場合:

virtual void fun(){} // changed in Derived

次に、「this」のアドレスは両方のコンストラクターで同じではありません:

Base: 0xbf93d6a4
Derived: 0xbf93d6a0

もう 1 つは、クラス Base がポリモーフィックである場合です。たとえば、別の仮想関数をそこに追加しました。

virtual void funOther(){} // added to Base

次に、両方の「this」のアドレスが再び一致します。

Base: 0xbfcceda0
Derived: 0xbfcceda0

問題は、ベース クラスがポリモーフィックではなく、派生クラスがポリモーフィックである場合に、ベース クラスと派生クラスで「この」アドレスが異なるのはなぜですか?

4

3 に答える 3

14

クラスのポリモーフィックな単一継承階層がある場合、ほとんどの (すべてではないにしても) コンパイラが従う典型的な規則は、その階層内の各オブジェクトが VMT ポインター (仮想メソッド テーブルへのポインター) で始まる必要があるというものです。このような場合、VMT ポインターは早い段階でオブジェクト メモリ レイアウトに導入されます。つまり、ポリモーフィック階層のルート クラスによって、すべての下位クラスは単にそれを継承し、適切な VMT を指すように設定します。このような場合、派生オブジェクト内のすべてのネストされたサブオブジェクトは同じthis値を持ちます。そのようにして、コンパイラでメモリ位置を読み取ること*thisにより、実際のサブオブジェクトのタイプに関係なく、VMT ポインターにすぐにアクセスできます。これはまさに、最後の実験で起こったことです。ルート クラスをポリモーフィックにすると、すべてのthis値が一致します。

ただし、階層内の基本クラスがポリモーフィックでない場合、VMT ポインターは導入されません。VMT ポインターは、階層の下位にある最初のポリモーフィック クラスによって導入されます。このような場合、一般的な実装アプローチは、階層の非ポリモーフィック (上位) 部分によって導入されたデータのに VMT ポインターを挿入することです。これは、2 番目の実験で見られるものです。のメモリ レイアウトはDerived次のようになります。

+------------------------------------+ <---- `this` value for `Derived` and below
| VMT pointer introduced by Derived  |
+------------------------------------+ <---- `this` value for `Base` and above
| Base data                          |
+------------------------------------+
| Derived data                       |
+------------------------------------+

一方、階層の非ポリモーフィック (上位) 部分のすべてのクラスは、VMT ポインターについて何も認識しない必要があります。タイプのオブジェクトはBasedata field で始まる必要がありますBase::x。同時に、階層のポリモーフィック (下位) 部分のすべてのクラスは、VMT ポインターで開始する必要があります。これらの両方の要件を満たすために、ネストされた基本サブオブジェクトから別のサブオブジェクトに階層を上下に変換するときに、コンパイラはオブジェクト ポインター値を調整する必要があります。これは、ポリモーフィック/非ポリモーフィック境界を越えたポインター変換がもはや概念的ではないことをすぐに意味します。コンパイラーはオフセットを追加または減算する必要があります。

階層の非ポリモーフィック部分のthisサブオブジェクトは値を共有しますが、階層のポリモーフィック部分のサブオブジェクトは独自の異なるthis値を共有します。

階層に沿ってポインター値を変換するときにオフセットを加算または減算する必要があるのは珍しいことではありません。複数の継承階層を処理するとき、コンパイラーは常にそれを行う必要があります。ただし、例では、単一継承階層でもそれを実現する方法を示しています。

加算/減算の効果はポインタ変換でも明らかになります

Derived *pd = new Derived;
Base *pb = pd; 
// Numerical values of `pb` and `pd` are different if `Base` is non-polymorphic
// and `Derived` is polymorphic

Derived *pd2 = static_cast<Derived *>(pb);
// Numerical values of `pd` and `pd2` are the same
于 2012-07-21T16:25:21.950 に答える
6

これは、オブジェクト内の v-table ポインターを使用したポリモーフィズムの典型的な実装の動作のように見えます。Base クラスには仮想メソッドがないため、このようなポインターは必要ありません。これにより、32 ビット マシンのオブジェクト サイズが 4 バイト節約されます。典型的なレイアウトは次のとおりです。

+------+------+------+
|   x  |   y  |   z  |
+------+------+------+

    ^
    | this

ただし、Derived クラスにはv-table ポインターが必要です。通常、オブジェクト レイアウトのオフセット 0 に格納されます。

+------+------+------+------+
| vptr |   x  |   y  |   z  |
+------+------+------+------+

    ^
    | this

そのため、Base クラスのメソッドがオブジェクトの同じレイアウトを参照できるようにするために、コード ジェネレーターは、Base クラスのメソッドを呼び出す前に、 thisポインターに 4 を追加します。コンストラクターは次を参照します。

+------+------+------+------+
| vptr |   x  |   y  |   z  |
+------+------+------+------+
           ^
           | this

これは、Base コンストラクターの this ポインター値に 4 が追加されている理由を説明しています。

于 2012-07-21T16:35:51.510 に答える
1

技術的に言えば、これはまさに起こることです。

ただし、言語仕様によると、ポリモーフィズムの実装は必ずしも vtables に関連しているとは限らないことに注意する必要があります。これが仕様です。仕様の範囲外である「実装の詳細」として定義します。

私たちが言えることは、それthisには型があり、その型を介してアクセスできるものを指しているということだけです。メンバーへの逆参照がどのように発生するかは、実装の詳細です。

pointer to something暗黙的、静的または動的な変換のいずれかによってa が に変換された場合、周囲のものに対応するために変更する必要があるという事実は、例外ではなく規則pointer to something elseと見なす必要があります。

C++ が定義されている方法では、実装が想定されたレイアウトに基づいていることを暗黙のうちに想定しているため、答えと同様に質問は無意味です。

特定の状況下で、2 つのオブジェクト サブコンポーネントが同じオリジンを共有するという事実は、(非常に一般的な) 特殊なケースにすぎません。

例外は「再解釈」です。型システムを「ブラインド」し、「このバイトの束をこの型のインスタンスとして見てください」とだけ言う場合: アドレスが変更されないことを期待しなければならない唯一のケースです (および責任はありません)。そのような変換の意味についてコンパイラから)。

于 2012-07-21T17:24:59.367 に答える