目次
2.4 オーバーロード、オーバーライド(オーバーライド)、隠蔽(再定義)の比較
1. ポリモーフィズムの概念
ポリモーフィズム: 多くの形式。具体的には、ある動作を完了することであり、異なるオブジェクトが完了すると、異なる状態が生成されます。
チケットを購入する行為。一般人は定価でチケットを購入し、学生は半額でチケットを購入し、軍人は優先的にチケットを購入します。
ここでは、一般人、学生、兵士 (異なるオブジェクト) が、チケット購入アクションを完了するための異なる状態を持っています。
同時に、ほとんどの人が使ったことがあるはずで、新規ユーザーの場合は確実に 0.1 元の切断に成功しますが、古いユーザーの場合は失敗するか、いくつかのフラグメントが追加される可能性があります (かなり多くのフラグメントが 0.1 元に結合されます) )。
レベルが異なると、ナイフを切る状態も異なります。
Alipay はコードをスキャンして赤い封筒を受け取ります。赤い封筒をスキャンして 8 元や 10 元を獲得できる人もいます...一方、赤い封筒をスキャンしても数セントしか得られない人もいます... この背後には、実際にはポリモーフィックな動作があります。(PS: 娯楽のみのための作り話) Alipay は最初にあなたのアカウント データを分析します。たとえば、あなたが新規ユーザーであるか、頻繁に Alipay で支払わない場合は、Alipay の使用を奨励する必要があり、次にスキャン コードの量を分析します。 = random() %99; たとえば、支払いに Alipay を頻繁に使用する場合、または Alipay アカウントに一年中お金がない場合は、Alipay の使用をあまり奨励する必要はありません。その場合、スキャン コードの量 = random() %1; 同じスキャン コード アクション、異なるユーザー コードをスキャンして異なる赤い封筒を取得することも、多様な動作です。
2. ポリモーフィズムの定義と実装
2.1 ポリモーフィズムの条件
ポリモーフィズムとは、異なる継承関係にあるクラス オブジェクトが同じ関数を呼び出すことを指し、その結果、動作が異なります。
継承で多態性条件を形成するには:
- 仮想関数は、基本クラスのポインターまたは参照を通じて呼び出す必要があります。
- 呼び出される関数は仮想関数である必要があり、派生クラスは基本クラスの仮想関数をオーバーライドする必要があります。
2.2 仮想関数の書き換え
仮想関数の書き換え・上書き条件:仮想関数(仮想を追加できるのはメンバ関数のみ)+トリプル(関数名、パラメータ、戻り値)
書き換えに応じず、関係を隠すことです。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
仮想関数の書き換えの例外 (C++ によって掘られた穴) :
- サブクラス仮想関数は仮想を追加しないため、やはり書き換えが発生します。(仮想関数を継承してから書き換え(実装)します)
- 共変 (基本クラスと派生クラスの仮想関数の戻り値の型が異なる)
派生クラスが基底クラスの仮想関数をオーバーライドする場合、基底クラスの仮想関数の戻り値の型が異なります。つまり、基本クラス仮想関数は基本クラス オブジェクトへのポインターまたは参照を返し、派生クラス仮想関数は派生クラス オブジェクトへのポインターまたは参照を返します。これは共分散と呼ばれます。
- デストラクターの書き換え (基本クラスと派生クラスのデストラクターの名前は異なります)
2.3 C++11 オーバーライドと最終
- Final: 仮想関数を変更し、仮想関数が書き換えられなくなったことを示します。
class Car
{
public:
virtual void Drive() final {}
};
class BMW :public Car
{
public:
virtual void Drive() {cout << "BMW-舒适" << endl;}
};
- override: 派生クラスの仮想関数が基本クラスの仮想関数をオーバーライドするかどうかを確認し、そうでない場合はコンパイルしてエラーを報告します。
class Car{
public:
virtual void Drive(){}
};
class BMW :public Car {
public:
virtual void Drive() override {cout << "BMW-舒适" << endl;}
};
2.4 オーバーロード、オーバーライド(オーバーライド)、隠蔽(再定義)の比較
3. 抽象クラス
3.1 コンセプト
class Car
{
public:
virtual void Drive() = 0;
};
3.2 インターフェースの継承と実装の継承
- 通常の関数の継承は実装継承の一種であり、派生クラスは基底クラスの機能を継承して利用することができ、継承されるのは関数の実体である。
- 仮想関数の継承はインターフェース継承の一種であり、派生クラスは基底クラスの仮想関数のインターフェースを継承し、書き換えて実現することを目的としています。
路上インタビューの質問をして、インターフェイスの継承を見てみましょう。
class A { public: virtual void func(int val = 1){ std::cout<<"A->"<< val <<std:endl;} virtual void test(){ func();} }; class B : public A { public: void func(int val=0){ std::cout<<"B->"<< val <<std::endl; } }; int main(int argc ,char* argv[]) { B*p = new B; p->test(); return 0; }
A: A->0 B: B->1 C: A->1 D: B->0 E: コンパイルエラー F: 上記のどれも正しくありません
答え:B
分析: 仮想関数の書き換えは、インターフェイスの継承、実装の書き換え、およびインターフェイスの継承です。
上記の例では、インターフェイスは上記の void func(int val=1) を継承します。テストを呼び出し、B* が A* に渡されます (スライス操作) が、これは依然として B オブジェクトを指します。this-> オブジェクトを指す関数を呼び出します。(多態性)
ポリモーフィズムは、誰が誰を呼び出すかを示します。
4. ポリモーフィズムの原理
4.1 仮想機能テーブル
仮想関数テーブルの本質は関数ポインタの配列です。
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
観察とテストを通じて、b オブジェクトは 8 バイトであることがわかりました。_b メンバーに加えて、オブジェクト (VS プラットフォーム) の前に追加の __vfptr が配置されています。オブジェクト内のポインター (VS x86、ポインター 4 バイト)は仮想関数テーブルポインタと呼ばれます。仮想関数テーブルを持つクラスは、仮想関数テーブル ポインタを少なくとも 1 つ持ちます。これは、仮想関数のアドレスを仮想関数テーブルに配置する必要があり、仮想関数テーブルは仮想テーブルとも呼ばれるためです。
注:仮想関数はどこに存在しますか? 仮想テーブルはどこに存在しますか?
4.2 ポリモーフィズムの原理
実証するモデルを与えてください:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void Func() { cout << "Func" << endl; }
int _a = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
int _b = 0;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
要約:
ポリモーフィズムの本質的な原理は、ポリモーフィズムの 2 つの条件を満たしています。次に、呼び出し時に、オブジェクトを指す仮想テーブル内の対応する仮想関数アドレスを見つけて、それを呼び出します。
ポリモーフィズム (プログラムの実行中に、オブジェクトを指す仮想テーブル内の関数アドレスを見つけて呼び出すため、p は誰がどの仮想関数を呼び出すかを指します)
通常の関数呼び出し。コンパイルおよびリンク時に関数のアドレスを決定し、実行時に直接呼び出します。タイプは誰が誰に電話するかです。
4.3 動的バインディングと静的バインディング
- 静的バインディングは、アーリー バインディング (早期バインディング) とも呼ばれ、次のようなプログラムのコンパイル中のプログラムの動作を決定します。静的ポリモーフィズムとも呼ばれます。
- 動的バインディングは、遅延バインディング(遅延バインディング)とも呼ばれ、プログラムの実行中に取得される特定の型に従ってプログラムの特定の動作を決定し、特定の関数を呼び出すことであり、動的多態性とも呼ばれます。
5. 単一継承と多重継承関係の仮想関数テーブル
5.1 単一継承の仮想関数テーブル
結論は:
- 単一継承:同じクラスのオブジェクトが仮想テーブルを共有します。
- VSでは、書き換えが完了したかどうかに関わらず、サブクラスの仮想テーブルは親クラスの仮想テーブルと同じになりません(内容が変わります)
- サブクラス仮想テーブルには、独自の仮想関数と親クラスの仮想関数があります(仮想関数ポインタが格納されます)。
サブクラスの仮想関数がサブクラスの仮想テーブルに配置されているか、親クラスの仮想テーブルに配置されているかをテストします。
class Base {
public :
virtual void func1() { cout<<"Base::func1" <<endl;}
virtual void func2() {cout<<"Base::func2" <<endl;}
private :
int a;
};
class Derive :public Base {
public :
virtual void func1() {cout<<"Derive::func1" <<endl;}
virtual void func3() {cout<<"Derive::func3" <<endl;}
virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
int b;
};
すごいですね、ここではサブクラスの func3 と func4 が欠落していますが、本当に欠落しているのでしょうか? 実際にはそうではありません! ここで、vs コンパイラの監視ウィンドウはこれら 2 つの関数を意図的に隠しており、これはバグであると考えられます。では、d の仮想テーブルを表示するにはどうすればよいでしょうか? 次のコードを使用して、仮想テーブル内の関数を出力します。
typedef void(*VFPTR)();
void PrintVFTable(VFPTR* table)
{
for (size_t i = 0; table[i] != nullptr; ++i)
{
printf("vft[%d]:%p->", i, table[i]);
VFPTR pf = table[i];
pf();
}
cout << endl;
}
その結果、サブクラスの仮想関数が実際にサブクラス独自の仮想テーブルに配置されていることがわかります。
5.2 多重継承における仮想関数テーブル
結論は:
- 複数の継承派生クラスのオーバーライドされていない仮想関数は、最初に継承された基本クラス部分の仮想関数テーブルに配置されます。
class Base1 {
public:
virtual void func1() {cout << "Base1::func1" << endl;}
virtual void func2() {cout << "Base1::func2" << endl;}
private:
int b1;
};
class Base2 {
public:
virtual void func1() {cout << "Base2::func1" << endl;}
virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
private:
int d1;
};
int main()
{
Derive d;
return 0;
}
サブクラス自己インクリメントの仮想関数は、VS 監視ウィンドウでは確認できません。
上で記述した関数コードを使用して結果をテストします。
Diamond Inheritance と Rhombus Virtual Inheritance に、 Chen Hao 氏による 2 つの記事が投稿されました: C++ 仮想関数テーブル分析 | Cool Shell - CoolShell C++ オブジェクトのメモリ レイアウト | Cool Shell - CoolShell
6. 面接でよくある質問と回答
- インライン関数を仮想関数にすることはできますか?
回答: はい。ただし、仮想関数は仮想テーブルに配置する必要があるため、コンパイラは inline 属性を無視し、この関数はインラインではなくなります。
- 静的メンバーを仮想関数にすることはできますか?
回答: いいえ、静的メンバー関数には this ポインターがなく、type:: メンバー関数を呼び出して仮想関数テーブルにアクセスできないため、静的メンバー関数を仮想関数テーブルに配置できません。
- コンストラクターは仮想にできますか?
回答: いいえ、オブジェクト内の仮想関数テーブル ポインターはコンストラクターの初期化リストの段階で初期化されるためです。
- デストラクターは仮想化できますか? どのような状況でデストラクターは仮想関数になりますか?
回答: はい。基本クラスのデストラクターを仮想関数として定義するのが最善です。
- 通常の関数へのオブジェクト アクセスは高速ですか?それとも仮想関数へのアクセスは高速ですか?
回答: まず、普通の物体であれば同じくらい速いです。ポインター オブジェクトまたは参照オブジェクトの場合、呼び出される通常の関数はポリモーフィズムを構成するため高速であり、実行時に仮想関数を呼び出すには仮想関数テーブルを検索する必要があります。
- 仮想関数テーブルはどの段階で生成され、どこに存在しますか?
回答: 仮想関数テーブルはコンパイル段階で生成され、通常はコードセグメント (定数領域) に存在します。
- 抽象クラスとは何ですか? 抽象クラスの役割は?
回答: 機能: 抽象クラスは仮想関数を強制的に書き換え、抽象クラスはインターフェイスの継承関係を反映します。
- コピー構築と演算子=は仮想関数にできますか?
回答: コピー構築は不可能です。コピー構築もコンストラクターです。Operator= は機能しますが、実際の値はありません。