そして、多型のvtable
仮想関数テーブルとマルチステートは、問題はC ++開発者は、最終的に顔になりますです。
長い間、私はCを書いていないものの++、ここでそれはまだきちんと記録されています。
コンパイラ情報:
- GCC:gccの(Debianの7.3.0-19)7.3.0。
- 打ち鳴らす:7.0.1-8(タグ/ /最終RELEASE_701)。
クラス1つのスペース
class Empty {
public:
Empty() = default;
~Empty() = default;
void hello() { std::cout << "hello world" << std::endl; }
};
// sizeof(Empty) = 1
まず、1の大きさを有する、(非仮想関数を含む)をクリア、空のクラスを必要としています。
配列にクラスのインスタンスにできるようにするために、あなたは空のクラスのサイズを持っている必要があります、はsizeof配列は、そうでない場合は、災害になります。
しかし、基本クラスの空のクラスとして、4バイト以上、コンパイラの最適化遊離塩基のクラスを占めることができるそれぞれを整列します。
空のベースの最適化:実際には0バイトを占有せずに、基本クラスの仮想関数を非静的データメンバをしてみましょう。
今、私たちは、仮想関数の追加を開始、再びクラスのサイズを参照してください。
class Empty {
public:
Empty() = default;
~Empty() = default;
void hello() { std::cout << "hello world" << std::endl; }
virtual void virtual_test() {}
};
// sizeof(Empty) = 8
仮想関数を加えた後、1バイトから8バイトからクラスサイズが大きくなります。
コンパイラは暗黙的クラス仮想関数テーブルポインタを(挿入されたからであるvoid *vptr
)、ポインタ・サイズは8バイトです。
背中に行うには、コンパイラの事について、探査の深さを確認することをお勧めします<< >> C ++オブジェクトモデルは(が、忘れて見えたが、良くありませんよりも少し見えます)。
2仮想関数テーブルポインタ(vptr)と、仮想関数テーブル(VTBL)
クラスの仮想関数を含む、コンパイラは、対応する仮想関数テーブル(VTBL)クラスを作成します。
Vtable、アドレスに対応する主記憶クラスの仮想関数。
コンパイル時には、コンストラクタでコンパイラ、vptrの割り当て、アドレスのVTBL値。
擬似コードは以下のように:
class Empty {
public:
Empty() {
vtpr = (void*)&Empty::vtbl;
}
}
いくつかの改善、我々は空のクラスを変更する次のとおりです。
class Empty {
public:
Empty() = default;
virtual ~Empty() {}
virtual void virtual_func1() {}
virtual void virtual_func2() {}
public:
int m1 = 0x01020304, m2 = 0x04030201;
};
int main() {
Empty empty;
std::cout << empty.m1 << std::endl;
return 0;
}
主な改善は、メンバ変数M1、M2、及び(仮想関数を含む)を加え、多数の機能を追加することです。
以下に示すように、GDBを見る例示的なメモリレイアウトを空にする。
メモリの例を空のレイアウトとして、図から分かります。
- vptr(赤線、仮想テーブルをエンプティポイント)。
- M1、M2。
以上の3ステート・コール
C ++は、実装、仮想関数に依存しなければならない多型、三つの特徴のカプセル化、継承やポリモーフィズムです。
仮想関数テーブルポインタ(vptr)入口を呼び出して仮想関数テーブル(VTBL)を発見し、仮想関数、多型に使用するプログラムを実行する場合は、人気のあるポイント。
例えば:
class Base {
public:
virtual void virtual_func() {}
};
int main() {
Base *a = new Base();
a->virtual_func(); // 多态调用
Base b;
b.virtual_func(); // 非多态调用
Base *c = &b;
c->virtual_func(); // 多态调用
return 0;
}
ビューのコメントを確認するために、我々は証拠コンパイルされたコードを使用します。
図は、3回の呼び出していることが分かるvirtual_func
、アセンブリコードが大きく異なります。
その理由は、Cインスタンスコールvirtual_func
Bコールインスタンスに対してvirtual_func
、マルチ仮想テーブル(VTBL)ルックアップに必要virtual_func
手順機能エントリー。
4メモリレイアウト
以下、それぞれ単一継承、多重継承、及びダイヤモンド継承示さ3つの仮想テーブルメモリレイアウト(使用からg++
エクスポートメモリレイアウト)。
4.1単一継承
class A
{
int ax;
virtual void f0() {}
};
class B : public A
{
int bx;
virtual void f1() {}
};
class C : public B
{
int cx;
void f0() override {}
virtual void f2() {}
};
次のようにメモリのレイアウトは次のとおりです。
Vtable for A
A::vtable for A: 3 entries
0 (int (*)(...))0 // 类型转换偏移量
8 (int (*)(...))(& typeinfo for A) // 运行时类型信息(Run-Time Type Identification,RTTI)
16 (int (*)(...))A::f0 // 虚函数f0地址
Class A
size=16 align=8
base size=12 base align=8
A (0x0x7f753a178960) 0
vptr=((& A::vtable for A) + 16)
Vtable for B
B::vtable for B: 4 entries
0 (int (*)(...))0 // 类型转换偏移量
8 (int (*)(...))(& typeinfo for B) // 运行时类型信息(Run-Time Type Identification,RTTI)
16 (int (*)(...))A::f0 // 虚函数f0地址(未override基类函数,因此继承自A)
24 (int (*)(...))B::f1 // 虚函数f1地址
Class B
size=16 align=8
base size=16 base align=8
B (0x0x7f753a00e1a0) 0
vptr=((& B::vtable for B) + 16)
A (0x0x7f753a178a20) 0
primary-for B (0x0x7f753a00e1a0)
Vtable for C
C::vtable for C: 5 entries
0 (int (*)(...))0 // 类型转换偏移量
8 (int (*)(...))(& typeinfo for C) // 运行时类型信息(Run-Time Type Identification,RTTI)
16 (int (*)(...))C::f0 // 虚函数f0地址
24 (int (*)(...))B::f1 // 虚函数f1地址(未override基类函数,因此继承自B)
32 (int (*)(...))C::f2 // 虚函数f2地址
Class C
size=24 align=8
base size=20 base align=8
C (0x0x7f753a00e208) 0
vptr=((& C::vtable for C) + 16)
B (0x0x7f753a00e270) 0
primary-for C (0x0x7f753a00e208)
A (0x0x7f753a178ae0) 0
primary-for B (0x0x7f753a00e270)
ここで明確にする必要があり、Class A/B/C
対応する仮想テーブルがあります。
仮想テーブルは、主に情報の3種類が含まれています。
- オフセット型変換。
- 実行時型情報(ランタイムタイプ識別、RTTI)。
- 仮想関数のアドレスは(1つより多く含むことができる)、具体的な情報は、コメントセクションを参照してください。
以上の4.2継承
class A {
int ax;
virtual void f0() {}
};
class B {
int bx;
virtual void f1() {}
};
class C : public A, public B {
virtual void f0() override {}
virtual void f1() override {}
};
以下のようにして得られたクラスのメモリレイアウト:
// 因为类A与类B比较简单,因此省略内存布局(可参考单继承内存布局)
Vtable for C
C::vtable for C: 7 entries
0 (int (*)(...))0
8 (int (*)(...))(& typeinfo for C)
16 (int (*)(...))C::f0
24 (int (*)(...))C::f1
32 (int (*)(...))-16 // 类型转换偏移量
40 (int (*)(...))(& typeinfo for C) // 运行时类型信息(Run-Time Type Identification,RTTI)
48 (int (*)(...))C::non-virtual thunk to C::f1()
Class C
size=32 align=8
base size=28 base align=8
C (0x0x7f9ce2bde310) 0
vptr=((& C::vtable for C) + 16)
A (0x0x7f9ce2d37ae0) 0
primary-for C (0x0x7f9ce2bde310)
B (0x0x7f9ce2d37b40) 16
vptr=((& C::vtable for C) + 48)
コードは、クラスAからクラス継承とCクラスB、より大きなメモリレイアウトの変更が発生した(添加三行の終わり)。
G ++のメモリレイアウトは、比較的それがより直感的になり、打ち鳴らすメモリレイアウト(基本的に同じ)を用いて導出される、不明瞭。
*** Dumping AST Record Layout
0 | struct C
0 | struct A (primary base)
0 | (A vtable pointer)
8 | int ax
16 | struct B (base)
16 | (B vtable pointer)
24 | int bx
| [sizeof=32, dsize=28, align=8,
| nvsize=28, nvalign=8]
メモリレイアウトから明らかな打ち鳴らす、クラスCの例は、仮想ポインタクラスAとクラスBが含まれています
これは、AとBは、仮想関数F0とF1の間のシーケンス関係は、完全に独立して存在していないため、基本クラスに対しては、同一の開始位置ずれ量を有するあります。
従って、クラスCにおいて、クラスAとクラスBの情報は、2つの仮想ポインタがインデックス化され、仮想テーブルで2つの互いに素の領域に格納されなければなりません。
C Vtable (7 entities)
+--------------------+
struct C | offset_to_top (0) |
object +--------------------+
0 - struct A (primary base) | RTTI for C |
0 - vptr_A -----------------------------> +--------------------+
8 - int ax | C::f0() |
16 - struct B +--------------------+
16 - vptr_B ----------------------+ | C::f1() |
24 - int bx | +--------------------+
28 - int cx | | offset_to_top (-16)|
sizeof(C): 32 align: 8 | +--------------------+
| | RTTI for C |
+------> +--------------------+
| Thunk C::f1() |
+--------------------+
図は、仮想テーブルの内容に対応し、比較ポインタの仮想イメージを示します。
まず説明しoffset_to_top
、派生クラスの変換にこのポインタを基本クラスを加えたアドレスの実際の型を得るためにオフセット。:Thunk
、C + 16で参照されるシーン内の開始アドレス(1)B&B = C、もし直接呼び出しF1、16以上、このポインタのバイトオフセットエラー原因ため、
(2)プロンプトサンク16バイトオフセットに応じて、このポインタは、その後、関数f1を呼び出し、offset_to_top減算されます。
基本クラスの参照は、派生クラスのインスタンスを保持している場合、対応する仮想関数を呼び出すサンク説明は、多型特性を用いて説明します。
4.3ダイヤモンドの継承
class A {
public:
virtual void foo() {}
virtual void bar() {}
private:
int ma;
};
class B : virtual public A {
public:
virtual void foo() override {}
private:
int mb;
};
class C : virtual public A {
public:
virtual void bar() override {}
private:
int mc;
};
class D : public B, public C {
public:
virtual void foo() override {}
virtual void bar() override {}
};
基本クラスは、クラスAのメンバ変数に含まれている場合ので、派生クラスB / C / Dは、理解するのが困難で、最適化され、メンバ変数MAを追加します。
Bのファーストクラスのメモリレイアウトビュー:
*** Dumping AST Record Layout
0 | class B
0 | (B vtable pointer)
8 | int mb
16 | class A (virtual base)
16 | (A vtable pointer)
24 | int ma
| [sizeof=32, dsize=28, align=8,
| nvsize=12, nvalign=8]
クラスBは、2つのダミーポインタを含むなお、この場合、クラスAの仮想ポインタ位置B + 16を起動。
以下のように、クラスの仮想テーブル構造のBを参照してください。
Vtable for 'B' (10 entries).
0 | vbase_offset (16)
1 | offset_to_top (0)
2 | B RTTI
-- (B, 0) vtable address --
3 | void B::foo()
4 | vcall_offset (0)
5 | vcall_offset (-16)
6 | offset_to_top (-16)
7 | B RTTI
-- (A, 16) vtable address --
8 | void B::foo()
[this adjustment: 0 non-virtual, -24 vcall offset offset]
9 | void A::bar()
このとき、ヘッドは、仮想テーブルを増加させるvbase_offset
、コンパイル時に、ベース・クラスは、クラスBのメモリの所定のシフト量Aとすることができないので、仮想テーブルを追加する必要がありvbase_offset
、クラスBの基底クラスのランタイムメモリを標識位置インチ
加えて、2つの仮想テーブルに追加vcall_offset
クラスAを呼び出して仮想基本クラスを使用して、参考例Bは、それぞれの仮想機能が、このポインタのオフセットに対しても異なっていてもよく応答するように仮想関数であり、それはvcall_offset記録に必要ですインチ
- vcall_offset(0):A ::バーに対応();
- vcall_offset(-16):Bに対応::のfoo()。
従って、呼び出し参考例B A ::バー機能、このポインタ点vptr_aなぜなら、そう無調整、コールB ::のfoo()、Bは、このようFOO関数をオーバーロードされて、このポインタを調整する必要がありますvptr_bを指しています。
クラスDのメモリレイアウトを表示します。
*** Dumping AST Record Layout
0 | class D
0 | class B (primary base)
0 | (B vtable pointer)
8 | int mb
16 | class C (base)
16 | (C vtable pointer)
24 | int mc
32 | class A (virtual base)
32 | (A vtable pointer)
40 | int ma
| [sizeof=48, dsize=44, align=8,
| nvsize=28, nvalign=8]
この場合、なおため使用する仮想継承の、それだけ1つのクラスAので、3つのカテゴリD仮想ポインタの合計。
下に示すように、比較的複雑な仮想が、基本的なクラスBを指すことができるコンテンツのテーブルは、仮想テーブルを解析します。
Vtable for 'D' (15 entries).
0 | vbase_offset (32)
1 | offset_to_top (0)
2 | D RTTI
-- (B, 0) vtable address --
-- (D, 0) vtable address --
3 | void D::foo()
4 | void D::bar()
5 | vbase_offset (16)
6 | offset_to_top (-16)
7 | D RTTI
-- (C, 16) vtable address --
8 | void D::bar()
[this adjustment: -16 non-virtual]
9 | vcall_offset (-32)
10 | vcall_offset (-32)
11 | offset_to_top (-32)
12 | D RTTI
-- (A, 32) vtable address --
13 | void D::foo()
[this adjustment: 0 non-virtual, -24 vcall offset offset]
14 | void D::bar()
[this adjustment: 0 non-virtual, -32 vcall offset offset]
5拡張
C ++仮想テーブル、およびランタイムメモリモデルが絶えず彼らの知識をリフレッシュされた製造法では、非常に複雑な問題です。
ここではいくつかの実施形態では、メモリ内のダンプメモリモデルオブジェクト、および仮想テーブル構造タイプがあります。
打ち鳴らすコンパイラを使用しますclang++ -cc1 -emit-llvm -fdump-record-layouts -fdump-vtable-layouts main.cpp
。
使用GCCコンパイラ:
g++ -fdump-class-hierarchy -c main.cpp
// g++ dump的内容比较晦涩,因此需要使用c++ filt导出具有可读性的文档
cat [g++导出的文档] | c++filt -n > [具有一定可读性的输出文档]
ここメモリレイアウトのセクションを参照:https://zhuanlan.zhihu.com/p/41309205を記事。
PS:
私のマイクロチャンネル公衆番号にあなたはあなたに私の記事が参考に思われる場合は、してください注意を払う、ありがとうございました!