3

多重継承を使用する場合、C ++はいくつかのvtableを維持する必要があり、これにより、共通の基本クラスの「いくつかのビュー」が作成されます。

コードスニペットは次のとおりです。

#include "stdafx.h"
#include <Windows.h>

void dumpPointer( void* pointer )
{
    __int64 thisPointer = reinterpret_cast<__int64>( pointer );
    char buffer[100];
   _i64toa( thisPointer, buffer, 10 );
    OutputDebugStringA( buffer );
    OutputDebugStringA( "\n" );
}

class ICommonBase {
public:
    virtual void Common() = 0 {}
};

class IDerived1 : public ICommonBase {
};

class IDerived2 : public ICommonBase {
};

class CClass : public IDerived1, public IDerived2 {
public:
    virtual void Common() {
        dumpPointer( this );
    }
    int stuff;
};

int _tmain(int argc, _TCHAR* argv[])
{
    CClass* object = new CClass();
    object->Common();
    ICommonBase* casted1 = static_cast<ICommonBase*>( static_cast<IDerived1*>( object ) );
    casted1->Common();
    dumpPointer( casted1 );

    ICommonBase* casted2 = static_cast<ICommonBase*>( static_cast<IDerived2*>( object ) );
    casted2->Common();
    dumpPointer( casted2 );

    return 0;
}

次の出力が生成されます。

206968 //CClass::Common this
206968 //(ICommonBase)IDerived1::Common this
206968 //(ICommonBase)IDerived1* casted1
206968 //(ICommonBase)IDerived2::Common this
206972 //(ICommonBase)IDerived2* casted2

ここcasted1casted2は、さまざまなサブオブジェクトを指しているため、妥当なさまざまな値があります。仮想関数が呼び出された時点で、基本クラスへのキャストが実行され、コンパイラはそれが元々最も派生したクラスであったことを認識していません。それでもこれは毎回同じです。それはどのように起こりますか?

4

4 に答える 4

3

別のタイプにキャストする場合、フィールドのオフセットとvtableのエントリは一貫した場所にある必要があります。パラメータとしてを受け取るコードはICommonBase*、オブジェクトが実際にであるかどうかを認識しませんIDerived2。それでも->foo、仮想メソッドを逆参照または呼び出すことができるはずbar()です。これらが機能する方法がない予測可能なアドレスにない場合。

単一継承の場合、これは簡単に正しく理解できます。Derivedから継承する場合、のオフセット0はのオフセット0でもBaseあると言うことができ、に固有のメンバーはの最後のメンバーの後に行くことができます。多重継承の場合、の最初のバイトがの最初のバイトになることもできないため、明らかに機能しません。それぞれに独自のスペースが必要です。DerivedBaseDerivedBaseBase1Base2

したがって、2つから継承するようなクラスがある場合(それを呼び出すFoo)、コンパイラは、型Fooの場合、Base1部分がオフセットXで始まり、Base2部分がオフセットYで始まることを知ることができます。どちらかの型にキャストする場合、コンパイラは次のことを実行できます。適切なオフセットをに追加しますthis

の実際の仮想メソッドFooが呼び出され、実装がによって提供されるFoo場合でも、オブジェクトへの「実際の」ポインタが必要です。これにより、ベースBase1またはの特定のインスタンスだけでなく、そのすべてのメンバーにアクセスできますBase2。したがってthis、「実際の」オブジェクトを指す必要があります。

これの実装の詳細は説明されているものとは異なる場合があることに注意してください。これは、問題が存在する理由の高レベルの説明にすぎません。

于 2009-11-17T07:59:01.223 に答える
3

仮想関数呼び出しで多重継承が使用される場合、仮想関数への呼び出しは、thisポインターを調整する「サンク」に送られることがよくあります。あなたの例では、casted1ポインタのvtblエントリはサンクを必要としません。これは、のIDerived1サブオブジェクトがCClassたまたまCClassオブジェクトの開始と一致するためです(これがcasted1ポインタ値がポインタと同じである理由CClass objectです)。

ただし、サブオブジェクトcasted2へのポインターIDerived2はオブジェクトの開始と一致しないCClassため、vtbl関数ポインターは実際には関数を直接指すのではなくサンクを指しCClass::Common()ます。thisサンクは、実際のオブジェクトを指すようにポインターを調整してCClassから、関数にジャンプしCClass::Common()ます。CClassしたがって、どのタイプのサブオブジェクトポインタから呼び出されたかに関係なく、常にオブジェクトの先頭へのポインタを取得します。

これについては、 StanleyLippmanの「InsidetheC++ Object Model」のセクション、セクション4.2「仮想メンバー関数/MIでの仮想関数」に非常によく説明されています。

于 2009-11-17T08:33:57.217 に答える
1

このためにメモリ内のオブジェクトレイアウトを見ると、次のようになります。

v-pointer for IDerived1
v-pointer for IDerived2
....
....

それ以外の場合もありますが、アイデアを与えるためだけです。

this常にオブジェクトの先頭、つまりIDerived1のvポインターが格納されている場所を指します。ただし、ポインターをキャストすると、キャストされたポインターは、ポインターからsizeof(pointer)によってオフセットされるIDetived2vポインターを指します。IDerived2this

于 2009-11-17T07:57:17.540 に答える
1

あなたが示したのと同じオブジェクトモデルがG++4.3にあります(Naveenの回答を参照)。casted1とcasted2の値は異なると言います。

さらに、G ++ 4.3では、残忍なキャストを使用する場合でも、次のようになります。

ICommonBase* casted1 = (ICommonBase*)(IDerived1*)object; 
ICommonBase* casted2 = (ICommonBase*)(IDerived2*)object;

結果は同じです。

非常に賢いコンパイラ

于 2009-11-17T08:25:31.260 に答える