0
class Base1 { 
    virtual void fun1() { cout << "Base1::fun1()" << endl; } 
    virtual void func1() { cout << "Base1::func1()" << endl; } 
}; 
class Base2 { 
    virtual void fun1() { cout << "Base2::fun1()" << endl; } 
    virtual void func1() { cout << "Base2::func1()" << endl; } 
}; 

class Test:public Base1,public Base2
{
public:
    virtual void test(){cout<<"Test";}
};


typedef void(*Fun)(void); 

int main()
{
    Test objTest; 
    Fun pFun = NULL;

    pFun = (Fun)*((int*)*(int*)((int*)&objTest+0)+0); pFun(); 
    pFun = (Fun)*((int*)*(int*)((int*)&objTest+0)+1); pFun(); 

//The following isnt supposed to print Test::test() right?
    pFun = (Fun)*((int*)*(int*)((int*)&objTest+0)+2); pFun(); 

    pFun = (Fun)*((int*)*(int*)((int*)&objTest+1)+0); pFun(); 
    pFun = (Fun)*((int*)*(int*)((int*)&objTest+1)+1); pFun();


//Isnt the following supposed to print Test:test() because the order of   
construction   object is Base1 followed by construction of Base2 followed by 
construction of Test.

    pFun = (Fun)*((int*)*(int*)((int*)&objTest+1)+2); pFun(); 
}

Test オブジェクトのサイズは 8 バイトです。したがって、この例から、オブジェクトが 2 つの 4 バイトの _vptr で構成されていることが明らかです。継承の順序はpublic Base1,public Base2次のとおりであるため、オブジェクトは次のように作成する必要があります。

| _vptr to class Base1 vTable | -->this Base1 vtable should have 2 elements.
| _vptr to class Base2 vTable | -->this Base2 vtable should have 3 elements.

しかし、コード スニペットから、オブジェクトは次のように作成されているように見えます。

| _vptr to class Base1 vTable | -->this Base1 vtable should have 3 elements.
| _vptr to class Base2 vTable | -->this Base2 vtable should have 2 elements.

最初の vptr は、3 つの関数ポインター (1 番目のポイントBase1::fun1()、2 番目のポイント、Base1::func1()および 3 番目のポイント) の配列をポイントしTest::test()ます。

派生オブジェクトは、Base+Derived で構成されます。つまり、バイトの最初のチャンクが Base オブジェクトであり、残りが Derived であることを意味します。もしそうなら、私たちの objTest の例では、2 番目は 3 つの関数ポインタ (1 番目と、および)_vptrを指すはずです。しかし、代わりに、最初のが の関数ポインタを指していることがわかります。Base2::fun1()Base2::func1()Test::test()_vptrTest::test()

質問:

1.この動作はコンパイラ固有のものですか?

2.規格はこの動作について何か言及していますか? それとも私の理解は完全に間違っていますか?

4

2 に答える 2

0

vtable の構築方法は、間違いなくコンパイラに依存します。複数の継承がある場合に何が起こるか、特に2つのvtablesが生成される順序...

型情報など、vtable 以外にも保存されているものがあるため、動的キャストを行うことができます。

標準から要求されるのは、「仮想関数が機能する」ことだけです (確かに、「機能」が何を意味するかを説明するのは何千もの単語です) が、それがどのように実装されるかは完全にコンパイラー (およびおそらくある程度の C++ ライブラリー) 次第です。

于 2016-02-06T17:53:08.000 に答える
0

まず、vtables は C++ 標準の範囲外であることに注意してください。これは、標準が適合プログラムの動作のみに関係しており、実装がそれを達成する方法には関係していないためです。

既知の実装はすべて vtable を使用して仮想関数と RTTI を実装していますが、重要なニュアンスは、多重継承、共変リターン、仮想ベースなどのより「微妙な」機能に存在します。

単純なケースでは、基本クラスのサブオブジェクトが派生オブジェクトと同じアドレスにあるため、基本クラスの vtable を拡張できます。ベースクラスは「プライマリベース」と呼ばれます。

より複雑なケースでは、派生クラスに複数のポリモーフィック基本クラスがあり、それぞれに独自の vptr があります。1 つの基本クラスがプライマリ ベースになり、派生クラスがその vtable を拡張します。オブジェクトのレイアウトは、他の基本クラスのサブオブジェクトが派生オブジェクトのゼロ以外のオフセットになるようになっているため、そのthisような非プライマリ ベースの vtable を介して仮想関数が呼び出されると、ポインターを調整する必要があります。その vtable で参照される関数は、基本クラスのサブオブジェクトまたは完全なオブジェクトへのポインターを想定しています。

  • 基本クラスの vtable で参照される関数は、完全な基本オブジェクトへのポインターを想定しています。
  • 派生オブジェクトの非プライマリ ベース vtable で参照される関数は、ベース サブオブジェクトへのポインターを期待します。正しいthisポインターを取得するために、ベースから派生した調整を行う必要があります。

非プライマリ ベースを介した仮想関数の呼び出しは、調整が必要なため、わずかに効率が低下します。コンパイラは、プライマリ vptr の使用を優先します。

コンパイラは、他の基底クラスから継承されたすべての仮想関数を使用して、プライマリ ベースの vtable を拡張します。

于 2016-03-10T03:08:44.463 に答える