1. 仮想関数とは何ですか?
2. 仮想関数の用途は何ですか?
3. 仮想関数テーブルを生成するにはどうすればよいですか?
4. 仮想関数テーブルはどこに保存されますか?
5. 仮想関数テーブルはどのようなものですか?
6. 仮想関数を使用してポリモーフィズムを実現するときに、直接代入ではなく、ポインターまたは参照を使用する必要があるのはなぜですか?
そういう需要があったとします
class Creature
{
public:
int hp;
int mp;
char* name;
void Attack();
void Defend();
void Dead();
Creature(char* name);
};
class Player:public Creature
{
public:
int nSmart;
int nPower;
void Buy();
void Dead();
Player(char* name);
};
親クラスと子クラスの両方に、同じ名前の関数 void Dead(); があります。
void Creature::Dead()
{
printf("I am Dead.\n");
}
void Player::Dead()
{
printf("game over.\n");
}
親クラスとサブクラスのvoid Dead()を呼び出す関数を作成し、親クラスのオブジェクトが渡された場合は親クラスの同名の関数が呼び出され、サブクラスのオブジェクトが渡された場合はサブクラスの同名の関数が呼び出されるようにしたいと考えています。(いわゆるポリモーフィズム)
void DoSometing(Creature* c)
{
c->Dead();
}
仮パラメータが Player* p ではなく Creature* c である理由については、Player パーミッションを持つポインタ (//大きすぎる) が Creature 構造体にアクセスするのは安全ではありませんが、Creature パーミッションを持つポインタが Player 構造体にアクセスするのは安全であるためです (いわゆる継承機能)
では、なぜそうではないのでしょうか?(質問 6)、この問題は後で解決されます
void DoSometing(Creature c)
{
c.Dead();
}
DoSometing を呼び出したときの結果
I am Dead.
I am Dead.
原則として、Creature はCreature 型の構造を指すため、Creature に関連する Dead を呼び出すのが合理的ですが、Creature が Player の構造を指す場合、Player に関連する Dead を呼び出すのは不合理です。注意)、仮想関数とは何ですか? (質問 1)、virtual キーワードを持つ関数は仮想関数であり、仮想関数の関数は先ほどの要求を実現するものです(
質問2 ) 。
virtual void Dead();
void Dead() override;
DoSometing の実行結果は期待通りになりました
I am Dead.
game over.
さて、問題は、キーワードを追加した後、この要件を達成するために C++ がどのような追加作業を行うかということです。その後、アセンブリ コードを見てください。
lea ecx,dword ptr ss:[ebp-18] //局部变量Player
Player 自体のメンバー変数は 20 バイトしか占有していませんが、現在は 24 バイトを占有しています
00EFFB50 00F15B40 @[ñ. x86test.const Player::`vftable'
00EFFB54 00000064 d...
00EFFB58 00000032 2...
00EFFB5C 00F15BD0 Ð[ñ. "sanqiu"
00EFFB60 0000000A ....
00EFFB64 00000007 ....
Player 構造体の最初のメンバーは vftable (仮想関数テーブル) を指しているため、最初のメンバーは仮想関数テーブル ポインターとも呼ばれます。そのため、仮想関数テーブルの場所は構造体の最初のメンバーから見つけることができます (質問 4)。もちろん、構造体に仮想関数テーブルがあるという前提の下でです。次の5人のメンバーは以前と同じです
lea eax,dword ptr ss:[ebp-28]
Creature 自体のメンバー変数は 12 バイトしか占有していませんが、現在は 16 バイトを占有しています
00EFFB40 00F15B34 4[ñ. x86test.const Creature::`vftable'
00EFFB44 00000064 d...
00EFFB48 00000032 2...
00EFFB4C 00F15BD0 Ð[ñ. "sanqiu"
Creature 構造体の最初のメンバーも vftable (仮想関数テーブル) を指しており、後の 3 つのメンバーは以前と同じです。
これで、親クラス関数に virtual キーワードを持つ関数がある場合、仮想関数テーブルが存在し、サブクラス関数が親クラスの基本関数を書き換える場合、仮想関数テーブルも存在することがわかりました。すると、別の問題が生じます。サブクラスが親クラスの仮想関数を書き換えなかった場合はどうなるでしょうか。, 答えは、仮想関数テーブルはまだ生成されるということです (質問 3)
。サブクラスが親クラスの仮想関数を書き換えたとしても、仮想関数テーブルが存在すると推測できます。ただし、2 つの仮想関数テーブルの内容は異なっていなければなりません (これは確実です。そうしないと、コンソールは異なる結果を出力しません)。仮想関数はどのようなものですか
? (質問 5)、仮想関数テーブルは、関数アドレス、関数アドレス、関数アドレス、0x00000000 のように、0x00000000 で終わる特殊なポインター文字列 (一般的な文字列は 0x0 または 0x00 で終わります) として理解できます。もちろん X64 プログラムでは 0x0000000000000000
書き換えない場合の Player の仮想関数テーブルはこんな感じ関数アドレス (I am Dead の関数
)
0x00000000
書き換えると Player の仮想関数テーブルはこんな感じ 関数テーブルの対応する関数アドレス!!!最後の質問は質問 6 です。この問題を解決するには、DoSomething のコードを分析する必要があります。
void DoSometing(Creature* c)
{
c->Dead();
}
关键汇编代码
mov eax,dword ptr ss:[ebp+8] //eax = 对象指针
mov edx,dword ptr ds:[eax] //edx = 虚函数表指针
mov ecx,dword ptr ss:[ebp+8] //ecx = 对象指针
mov eax,dword ptr ds:[edx] //eax = 虚函数(I am dead 或者 game over)
call eax //调用
//在汇编下,虚函数的功能一目了然
void DoSometing(Creature c)
{
c.Dead();
}
关键汇编代码
lea ecx,dword ptr ss:[ebp+8]
call x86test.B9115E //C++根本没使用虚函数表,而是直接CALL了 I am dead的那个
//函数(x86test.B9115E)
これで、問題 6 も解決されました。
ただし、アセンブリ レベルでは、Creature c と Creature *c はまったく同じ方法でパラメーターを渡すため、C++ はオブジェクトを渡すときにポリモーフィズムを完全に実現できますが、C++ は最終的にこれを実行しないため、不明です。
ps: 同じクラスのオブジェクトの仮想関数テーブル ポインタは同じです。つまり、すべてが同じ仮想関数テーブルを指すため、メモリが節約されます。