vfptrおよびvbptr
注:
この記事では主にvfptrとvbptrについて説明しているため、仮想関数はありません。仮想継承には
vfptr:仮想関数ポインター
vbptr:仮想基本クラスポインターは含まれません。
1.通常の継承(仮想)
#include <iostream>
using namespace std;
class A {
public:
virtual void function1() {
;
}
private:
int data1 = 1;
};
class B {
public:
virtual void function2() {
;
}
private:
int data2 = 2;
};
class C :public A, public B {
};
int main() {
return 0;
}
上記のコードでCのメモリレイアウトを見てみましょう
注:
VS2019では、次のコマンドを使用して、指定したクラスのメモリレイアウトを表示できます
。cl-d1reportSingleClassLayoutCtest.cpp
ここで、Cはクラス名、test.cppはソースファイルの名前
です。Cのメモリレイアウト以下のとおりであります:
class C size(16):
+---
0 | +--- (base class A)
0 | | {vfptr}
4 | | data1
| +---
8 | +--- (base class B)
8 | | {vfptr}
12 | | data2
| +---
+---
C::$vftable@A@:
| &C_meta
| 0
0 | &A::function1
C::$vftable@B@:
| -8
0 | &B::function2
从C的内存布局我们可以看出:
- Cには2つのvfptrがあります
- 最初のvfptrはAによってダウンされたvfptrであり、内部の仮想関数は&A :: function1です。
- 2番目のvfptrはBによってダウンされたvfptrであり、内部の仮想関数は&B :: function2です。
Q:なぜ2つのvfptrがあるのですか?
回答:次のように
- 単一継承の関連付けから理解できます。単一継承では、パブリック継承はisの関係であり、サブクラスのオブジェクトを親クラスのオブジェクトに割り当てることができます。次に、仮想+参照/ポインタを介しての多型特性を使用できます
- 上記のコードでは、次のようなコードを書くと、CはA、CはBになります。
A* pa = new C();
B* pb = new C();
- では、vfptrが1つしかない場合、上記のコードのポリモーフィズムをどのように実現できますか?vfptrが1つしかないため、paはpbの仮想関数にアクセスすることもできますが、これは不合理です。
- 2つのvfptrが接続されておらず、それらの間にまだ数バイトがあることがわかります。では、コンパイラーはどのようにしてポリモーフィズムを実現するのでしょうか。実際にこの文が実行されるとき:
B* pb = new C();
- コンパイラーは、対応するこのポインターのオフセットを暗黙的に調整します
2.仮想継承
#include <iostream>
using namespace std;
class A {
private:
int data1 = 1;
};
class B:virtual public A {
private:
int data2 = 2;
};
class C :virtual public A {
private:
int data3 = 3;
};
class D :public B, public C {
private:
int data4 = 4;
};
int main() {
return 0;
}
上記のコードDのメモリレイアウトを見てみましょう。
class D size(24):
+---
0 | +--- (base class B)
0 | | {vbptr}
4 | | data2
| +---
8 | +--- (base class C)
8 | | {vbptr}
12 | | data3
| +---
16 | data4
+---
+--- (virtual base A)
20 | data1
+---
D::$vbtable@B@:
0 | 0
1 | 20 (Dd(B+0)A)
D::$vbtable@C@:
0 | 0
1 | 12 (Dd(C+0)A)
vbi: class offset o.vbptr o.vbte fVtorDisp
A 20 0 4 0
それは発見することができます:
- Dには2つのvbptrがあります
- 最初のものは、基本クラスBからダウンしたvbptrです。
- 2つ目は、基本クラスCからダウンしたvbptrです。
- Vbtableは実際にオフセットを保存します
原則は次のとおりです。
- クラスに1つ以上の仮想基本クラスサブオブジェクトが含まれている場合、クラスは2つの領域(定数領域、共有領域)に分割されます。
- 不変領域のデータは、後でどのように変更されても、常にオブジェクトの先頭から計算された固定オフセットを持ちます
- 共有領域は仮想基本クラスのサブオブジェクトを参照します。この部分のデータは、派生するたびに変更されます
- 仮想継承の目的は、クラスがステートメントを作成できるようにすることであり、基本クラスを共有することを約束します。その中で、この共有基本クラスは仮想基本クラス(仮想基本クラス)と呼ばれ、この例のAは仮想基本クラスです。このメカニズムでは、仮想基本クラスが継承システムに何度表示されても、派生クラスには仮想基本クラスのメンバーが1つだけ含まれます。
3.仮想関数+仮想継承
#include <iostream>
using namespace std;
class A {
public:
virtual void function1() {
}
private:
int data1 = 1;
};
class B:virtual public A {
public:
virtual void function2() {
}
private:
int data2 = 2;
};
class C :virtual public A {
public:
virtual void function3() {
}
private:
int data3 = 3;
};
class D :public B, public C {
public:
virtual void function4() {
}
private:
int data4 = 4;
};
int main() {
return 0;
}
このコードDのメモリレイアウトを見てみましょう。
class D size(36):
+---
0 | +--- (base class B)
0 | | {vfptr}
4 | | {vbptr}
8 | | data2
| +---
12 | +--- (base class C)
12 | | {vfptr}
16 | | {vbptr}
20 | | data3
| +---
24 | data4
+---
+--- (virtual base A)
28 | {vfptr}
32 | data1
+---
D::$vftable@B@:
| &D_meta
| 0
0 | &B::function2
1 | &D::function4
D::$vftable@C@:
| -12
0 | &C::function3
D::$vbtable@B@:
0 | -4
1 | 24 (Dd(B+4)A)
D::$vbtable@C@:
0 | -4
1 | 12 (Dd(C+4)A)
D::$vftable@A@:
| -28
0 | &A::function1
D::function4 this adjustor: 0
vbi: class offset o.vbptr o.vbte fVtorDisp
A 28 4 4 0
次の質問があります。
- 私がこのようなコードを書いた場合、あなたはそれを正しく見ていますか?
B* pb = new D;
pb->function4();
- 実際、これは間違っています。なぜなら、親クラスの仮想関数をカバーしていない親クラスのポインタ/参照を介してサブクラスの仮想メンバー関数を呼び出そうとする試みは、コンパイラの観点からは違法です。
- D 36のサイズはなぜですか?
- 基本クラスBの場合、vfptrとvbptrおよびdata2-3 * 4バイトがダウンします
- 基本クラスCの場合、vfptrとvbptrおよびdata3-3 * 4バイトがダウンします
- D自体の場合、彼はデータを持っています4-1 * 4バイト
- 仮想基本クラスAの場合、vfptrとdata1-2 * 4Bytesがあります。
- したがって、合計9 * 4バイト= 36バイト