C ++オブジェクトと仮想関数テーブル記憶
すなわち、マルチ状態を実現するC ++仮想関数機構そのサブクラスのインスタンスの親型ポインタで、次にポインタの親クラスを介して実際のサブクラスのメンバ関数を呼び出すが、この技術は、「親クラスのポインタを有することができ様々な形態は、「これは、一般的な技術で、ある変数のアルゴリズムを実装するために同じコードを使用して
この記事では、この方法の使用は、もはや仮想関数ですが、仮想関数の実装機構の明確な分析を行うことを記載していません
参考博文:https://blog.csdn.net/u012879957/article/details/81027287
実装メカニズムについては、我々は最初のオブジェクトが格納されている方法を理解する必要があります
そして、オブジェクトデータストレージ機能
私たちは、オブジェクトを定義するクラスで、システムは、各オブジェクト・ストレージ・スペースを割り当てることを知っています
ストレージのクラスの印象では、このような次の表のようになります。
上のパネルは、各データストレージ機能とオブジェクトに割り当てられたコードを示し、これは確かに、メモリの使用率ではありませんC ++コンパイラシステムは、以下の方法を使用するように、低すぎます。
各オブジェクトデータ記憶空間は、機能コードの共通部分に属する、オブジェクト(仮想関数ポインタ及びポインタは、仮想基本クラスのデータ部に属している)の一部のみを占有します
私たちはしばしば、「オブジェクトのメンバ関数は、」の面で論理的な観点からのものであり、と言う事実ではない場合は、物理ストレージのメンバー関数
C ++メモリパーティション
C ++メモリパーティションは、おそらく5つの部分に分かれています。
- スタック(スタック):必要なときに自動的に変数格納領域は、通常、ローカル変数、関数のパラメータを格納したときに自動的にクリア、コンパイラによって必要はありません割り当てられています。
- ヒープ(ヒープ)は:である
new
(かかわらず、コンパイラの)プログラマによって割り当てられた空きメモリブロック、典型的にはnew
一つdelete
一つに対応するnew[]
ものにdelete[]
、プログラマが解放されていない場合、オペレーティング・システムによってリソースが自動的に番組の終了時に回収、対応 - フリーストアは:ある
malloc
ように割り当てられたメモリのブロック、およびで、スタックに非常に似てfree
リリース - グローバル/スタティックメモリ:グローバル変数と静的変数が同じメモリに割り当てられています
- 一定のストレージ領域:このストア定数内の特殊な記憶領域であるが、変更することはできません
(ヒープと無料の店が、それは実際には同じ面積、新しい基本となる実装コードではmalloc関数の新しい高度なバージョンは、インテリジェントとして見ることができ、malloc関数を呼び出します)
:あなたが求めることができる静的メンバ関数と非静的メンバ関数は、クラス定義コードのメモリ領域に配置されている、彼らがクラスに属していると言うことができますが、クラスがなぜ直接クラスの静的メンバ関数を呼び出すことができます(関数にパラメータがない場合であっても)かなり静的クラスメンバ関数よりも唯一のクラス・オブジェクトを呼び出すことができ
理由:クラスの非静的クラスのメンバ関数は、実際にオブジェクト・クラス・タイプ・パラメータへのポインタ(すなわち、へのポインタを含むこのポインタ)、これだけクラスオブジェクト(このインジケータは、実際の値を持ち、この時点で)呼び出すことができ
Vtable
C ++は、継承と仮想関数を介して、多型を達成するために仮想関数を通じて仮想関数テーブル、連続を解決するための実装、仮想関数テーブルカバレッジを仮想関数の問題を追加して、真の反応の実際の機能を確保します
あまりにも精通していない友人、以下は非常に無知な、個人的な勧告を見てダウン前後に見に
vtableの原則が概説します
C ++仮想関数は、メソッドを実装している:メンバーが呼び出されポインタ保持隠し隠し部材のそれぞれのオブジェクトクラスを追加し、仮想テーブルポインタを指し、(vptr)を仮想関数テーブル(仮想関数テーブル、VTBL)
仮想関数テーブル、アレイ、テーブル等は、多く存在するスロット(スロット)(各仮想関数ポインタを指すストレージアレイに理解されるように)、各溝は、仮想関数のアドレスであります
すなわち:仮想関数テーブルは、仮想テーブルポインタと、各クラスのオブジェクトを使用して、各クラスの
オブジェクト・クラスのインスタンスは、仮想機能を有しているに(その上に述べたように)、我々は親クラスを操作するためにポインタを使用する場合、テーブルは、オブジェクト・インスタンスのメモリに割り当てられている場合、子クラス、このテーブル実際の機能を示すマップが呼び出されるべきであると同じように
次のようにおそらく構造は次のとおりです。
この図の上方に、最後の仮想関数テーブルはで、文字列の終わりとして、仮想関数テーブルのエンドノードである余分なノードを、追加/0
のvtableの終了、終了フラグの値をマークしている、同じ異なるコンパイラの下で異なる場合があります
たとえば、次の
ベース・クラス、仮想テーブルオブジェクトはへのポインタ含ま仮想関数テーブルベースクラス
もダミーポインタを含むことになる派生オブジェクトのテーブルを派生クラスの仮想関数テーブル
- 派生クラスは、ベースクラスの仮想メソッドをオーバーライドする場合は、派生クラスの仮想関数テーブルが格納されたアドレスではなく、基本クラスの仮想関数のアドレスより、仮想関数を書き換えます
- 基底クラスの仮想メソッドは、派生クラスでオーバーライドされていない場合は、派生クラスが上書きされていない基底クラスの仮想関数のアドレスを保存するために、基本クラスの仮想メソッド、および派生クラスの仮想関数テーブルを継承しますが、新しい派生クラスは仮想メソッドを定義する場合、仮想関数のアドレスは、派生クラスの仮想関数テーブルに追加されます
あなたはそれは問題ではない、めまいがあり、私たちは、次のコード例ではお見せ使用します
仮想関数テーブルを検索
C ++コンパイラは、その保証するポインタの仮想関数テーブルは、オブジェクトインスタンスで最前位置に存在するという我々の手段(多層多重継承または継承場合には、仮想テーブルは、最高の性能を持っていることを保証するために注意します)、仮想関数テーブルのアドレスを取得し、このオブジェクトのインスタンスに対応し、前記関数ポインタを横断することができ、適切な関数を呼び出します
私たちは、新しいクラスを作成します
class Base
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
上の議論によれば、我々は、メモリ点F、ポインタG、これら三つの機能の時間のベーステーブル(アレイ)の一例として、仮想関数テーブルを取得することができ
typedef void(*Fun)(void);
int main()
{
Base bObj;
Fun pFun = NULL;
//指向void* pf(void)类的函数的指针pFun
cout << "虚函数表的地址:" << (int*)(&bObj) << endl;
cout << "虚函数表的第一个函数地址:" << (int*) * (int*)(&bObj) << endl;
//再次取址得到第一个虚函数的地址
//第一个虚函数
pFun = (Fun) * ((int*) * (int*)(&bObj));
pFun();
}
私たちは、このコードを見て、ゆっくりと開いて分割しました
typedef void(*Fun)(void);
typedef void(*Fun)(void)
タイプの別名を使用することと等価で、関数ポインタのアドレスがNULLで宣言しますtypedef decltype(void) *Fun
いくつかの今変更ポインタpFunを観察するために、ブレークポイントを挿入します。
ベースオブジェクトはBOBJからインスタンス化され、次いでFun pFun=NULL
関数へのポインタを返すように宣言されています
ここで破るブレークポイントFun pFun=NULL
あなたはpFunが初期化されていない見ることができ、前
初期化pFun = NULL値になった後0x00000000
そのオブジェクトのインスタンスBOBJた後、私たちが(int*)(&bObj)
することを強制&bObj
するために回しint*
配列の最初の要素の仮想関数テーブルのアドレスへのポインタである仮想関数テーブルのアドレスを取得し、ポインタがするもう一度フェッチアドレスを第1のダミーを得ることができます最初の仮想関数である関数(配列の最初の要素)のアドレスを、Base::f()
アドレス
cout << "虚函数表的地址:" << (int*)(&bObj) << endl;
cout << "虚函数表的第一个函数地址:" << (int*) * (int*)(&bObj) << endl;
//再次取址得到第一个虚函数的地址
//第一个虚函数
pFun = (Fun) * ((int*) * (int*)(&bObj));
pFun();
あなたはおそらくし、この操作を理解していない(int*) * (int*)(&bObj)
、理解される(int*)(&bObj)
キャストすることはBOBJになったオブジェクトをint*
直接呼び出しがあれば、アドレス*(int*)(&bObj)
データを指しているBOBJアドレスを指さが、ここでは仮想テーブルで、それはを参照オブジェクト過去に、なければならない(int*)
に変換する関数ポインタポイントに、(int*) * (int*)(&bObj)
オブジェクトBOBJをポイントは、最初のアドレスの関数となります
そして理由pFun
によりFun
、この関数の関数ポインタ宣言、それは、ファンエンティティに相当し、その後にアドレスを変換しなければならないpFun
と連結されている知識のタイプ、(Fun)*
必須の変換
全体のプロセスは、それがコンテンツの開始アドレスBOBJ 4バイト(から読み出され、シンプルである&bObj
)、次いで、コンテンツはメモリアドレス(と解釈(int*)(&bObj)
)し、このアドレス(アクセス(int*) * (int*)(&bObj)
)、アドレスに格納された最後の値を(関数のアドレスとして解釈(Fun) * ((int*) * (int*)(&bObj))
)
PFunは(仮想関数テーブルの値は、最初の要素に等しくなっている見ることができる_vfptr[0]
)の値を0x00b41168
、それがpFunで既に関数を指す関数にこのポインタf()
(値は、格納された仮想関数ポインタが指している仮想関数テーブルを覚え仮想関数のアドレス)
コンソール出力:
配列のように、あなたが呼び出したい場合Base::g()
やBase::h()
、私たちのことができます。
pFun = (Fun) * ((int*) * (int*)(&bObj));
// (Fun) * ((int*) * (int*)(&bObj) + 1); // Base::g()
// (Fun) * ((int*) * (int*)(&bObj) + 2); // Base::h()
この絵を見て、それがもう少し明確ではないでしょうか?
今、彼らは継承を表示されたときの状況を見てみましょう
単一継承(カバー無し)
class Base {
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
};
class Derive :public Base {
public:
virtual void f1() { cout << "Derive::f1()" << endl; }
virtual void g1() { cout << "Derive::g1()" << endl; }
virtual void h1() { cout << "Derive::h1()" << endl; }
};
typedef void(*Fun)(void);
int main()
{
//Base bObj;
Derive dObj;
Fun pFun = NULL;
cout << "虚函数表的地址:" << (int*)(&dObj) << endl;
cout << "虚函数表的第一个函数地址:" << (int*) * (int*)(&dObj) << endl;
pFun = (Fun) * ((int*) * (int*)(&dObj) + 0);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 1);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 2);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 3);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 4);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 5);
pFun();
return 0;
}
対突破、我々は3に、価値pFunは、関数f1の仮想アドレスとなることがわかりました。
結果:
これは、次のように我々は、オブジェクトDOB、その仮想関数テーブルをインスタンス化し、サブクラスは任意の親クラスをオーバーライドしていない、継承をカバーしていません。
その
- 配置表で自分の順番に従って宣言仮想関数
- 親クラスの仮想関数サブクラスの仮想関数の前に
単一継承(カバー)
今、私たちは派生下のクラスを変更します
class Base {
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
};
class Derive :public Base {
public:
virtual void f() { cout << "Derive::f()" << endl; }
virtual void g1() { cout << "Derive::g1()" << endl; }
virtual void h1() { cout << "Derive::h1()" << endl; }
};
継承関係、派生がされてf()
過負荷に基底クラスはf()
、のは、デバッグに同じ方法を使用してみましょう、主な機能は、本質的に同じです
int main()
{
//Base bObj;
Derive dObj;
Fun pFun = NULL;
cout << "虚函数表的地址:" << (int*)(&dObj) << endl;
cout << "虚函数表的第一个函数地址:" << (int*) * (int*)(&dObj) << endl;
pFun = (Fun) * ((int*) * (int*)(&dObj) + 0);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 1);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 2);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 3);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 4);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 5);
pFun();
return 0;
}
最初の関数の中に見ることができDerive::f()
、そして実行するためにpFun = (Fun) * ((int*) * (int*)(&dObj) + 5)
、値が空になっpFun
その構造は、今、このような仮想関数テーブルです:
その
- 関数f()を被覆する親クラスの仮想関数の仮想テーブルの元の位置に配置されます
- 機能はまだカバーされていません
この機能により、我々は、次のようにこのプログラムを見ることができます:
Base *b = new Derive();
b->f();
仮想関数テーブル内のメモリで示すB F()は、実際のコールが発生中のSO位置が導出:: F()関数のアドレスを交換された導出は:: F()が呼び出され、そのC ++は、動的ポリモーフィズムを可能にします
多重継承(カバー無し)
class Base1 {
public:
virtual void f() { cout << "Base1::f()" << endl; }
virtual void g() { cout << "Base1::g()" << endl; }
virtual void h() { cout << "Base1::h()" << endl; }
};
class Base2 {
public:
virtual void f() { cout << "Base2::f()" << endl; }
virtual void g() { cout << "Base2::g()" << endl; }
virtual void h() { cout << "Base2::h()" << endl; }
};
class Base3 {
public:
virtual void f() { cout << "Base3::f()" << endl; }
virtual void g() { cout << "Base3::g()" << endl; }
virtual void h() { cout << "Base3::h()" << endl; }
};
class Derive :public Base1, public Base2, public Base3 {
public:
virtual void f1() { cout << "Derive::f1()" << endl; }
virtual void g1() { cout << "Derive::g1()" << endl; }
};
typedef void(*Fun)(void);
int main()
{
//Base bObj;
Derive dObj;
Fun pFun = NULL;
cout << "虚函数表的地址:" << (int*)(&dObj) << endl;
cout << "虚函数表的第一个函数地址:" << (int*) * (int*)(&dObj) << endl;
pFun = (Fun) * ((int*) * (int*)(&dObj) + 0);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 1);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 2);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 3);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 4);
pFun();
pFun = (Fun) * ((int*) * (int*)(&dObj) + 5);
pFun();
return 0;
}
ここで実行すると、ブレークポイントはそれを見ることができた後、
pFunはNULLポインタになり
コンソールの結果
なぜ、+5後にそれを探してみませんか?我々はC ++コンパイラの話をする前に、複数の継承に、仮想関数テーブル記憶ポイントの変更は、行われたのですると、オブジェクト内の隠されたメンバーを追加し、今あなたが理解することができ、彼は多重継承を隠すために複数のメンバーを追加するだけでなく、私たちは今、複数の仮想関数テーブルを持っていることを意味し、図の具体的な構成は次のとおりです。
その後、我々はそれにアクセスする方法がありませんか?パワフルなC ++のコースがあり、あなたが発見し注意する必要があり、テーブル(配列)は、実際には2次元配列になって
int main()
{
Fun pFun = NULL;
Derive dObj;
int** pVtab = (int**)& dObj;
//Base1's vtable
pFun = (Fun)pVtab[0][0];
//等价于:pFun = (Fun) * ((int*) * (int*)((int*)& dObj + 0) + 0);
pFun();
pFun = (Fun)pVtab[0][1];
pFun();
pFun = (Fun)pVtab[0][2];
pFun();
//Derive's vtable
pFun = (Fun)pVtab[0][3];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[0][4];
cout << pFun << endl;
//Base2's vtable
pFun = (Fun)pVtab[1][0];
pFun();
pFun = (Fun)pVtab[1][1];
pFun();
pFun = (Fun)pVtab[1][2];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[1][3];
cout << pFun << endl;
//Base3's vtable
pFun = (Fun)pVtab[2][0];
pFun();
pFun = (Fun)pVtab[2][1];
pFun();
pFun = (Fun)pVtab[2][2];
pFun();
pFun = (Fun)pVtab[2][3];
cout << pFun << endl;
return 0;
}
その
多重継承(カバー)
class Base1 {
public:
virtual void f() { cout << "Base1::f()" << endl; }
virtual void g() { cout << "Base1::g()" << endl; }
virtual void h() { cout << "Base1::h()" << endl; }
};
class Base2 {
public:
virtual void f() { cout << "Base2::f()" << endl; }
virtual void g() { cout << "Base2::g()" << endl; }
virtual void h() { cout << "Base2::h()" << endl; }
};
class Base3 {
public:
virtual void f() { cout << "Base3::f()" << endl; }
virtual void g() { cout << "Base3::g()" << endl; }
virtual void h() { cout << "Base3::h()" << endl; }
};
class Derive :public Base1, public Base2, public Base3 {
public:
virtual void f() { cout << "Derive::f()" << endl; }
virtual void g1() { cout << "Derive::g1()" << endl; }
};
主な機能は、最終的にはあなたが以下のように現在の仮想関数テーブルがあるでしょう、行くことはありません。
セキュリティ問題
水はボートを運ぶことができ、ローイングもまた、右、悪い何かをするために使用することができますのは、仮想テーブルを見てみましょう、転覆することができます
親ポインタ型のサブクラスで独自の仮想関数を介したアクセス
私たちが見ることができますがBASE1仮想テーブルは、上の図に仮想関数を導出持っているが、我々は単純に、独自の仮想関数サブクラスを呼び出すために、次のステートメントを使用することはできません。
Base1 *b1 = new Derive();
b1->f1(); //编译出错
呼び出すために、親クラスのポインタを使用しようとするカバーしていない親クラスの子クラスのメンバ関数をコンパイラの動作は、そのようなプログラムがコンパイルできなかったので、違法とみなされます
しかし、あなたが見つけたはずのコードの多重継承部を介して
実行時には、我々はC ++セマンティクスの違反を達成するための仮想関数テーブルポインタ道を介してアクセスすることができます(つまり、我々は多重継承に使うコードです)
Fun pFun = NULL;
Derive dObj;
int** pVtab = (int**)& dObj;
//Base1's vtable
pFun = (Fun)pVtab[0][0];
//等价于:pFun = (Fun) * ((int*) * (int*)((int*)& dObj + 0) + 0);
//Derive's vtable
pFun = (Fun)pVtab[0][3];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[0][4];
cout << pFun << endl;
非パブリック仮想関数へのアクセス
親以外の公共の仮想関数は、仮想関数テーブルに存在していますので、私たちは非パブリック仮想関数にアクセスするには、同じアクセスの仮想関数テーブルを使用することができ、それを行うのは非常に簡単です
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base {
};
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun) * ((int*) * (int*)(&d) + 0);
pFun(); //挖藕?
}
最後に、ノート
仮想関数テーブルは、必ずしも最もの先頭に存在していないが、非常に多くの様々なコンパイラの設定があります