C++ポリモーフィズムの詳細説明(仮想関数の書き換え、インターフェースの継承、仮想関数テーブルの詳細説明)

目次

1. ポリモーフィズムの概念

2. ポリモーフィズムの定義と実装

  2.1 ポリモーフィズムの条件

  2.2 仮想関数の書き換え

  2.3 C++11 オーバーライドと最終

  2.4 オーバーロード、オーバーライド(オーバーライド)、隠蔽(再定義)の比較

3. 抽象クラス

  3.1 コンセプト

  3.2 インターフェースの継承と実装の継承

4. ポリモーフィズムの原理

  4.1 仮想機能テーブル

  4.2 ポリモーフィズムの原理

  4.3 動的バインディングと静的バインディング

5. 単一継承と多重継承関係の仮想関数テーブル

  5.1 単一継承の仮想関数テーブル

  5.2 多重継承における仮想関数テーブル

6. 面接でよくある質問と回答


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++ によって掘られた穴)

  • サブクラス仮想関数は仮想を追加しないため、やはり書き換えが発生します。(仮想関数を継承してから書き換え(実装)します)

  •  共変 (基本クラスと派生クラスの仮想関数の戻り値の型が異なる)

派生クラスが基底クラスの仮想関数をオーバーライドする場合、基底クラスの仮想関数の戻り値の型が異なります。つまり、基本クラス仮想関数は基本クラス オブジェクトへのポインターまたは参照を返し、派生クラス仮想関数は派生クラス オブジェクトへのポインターまたは参照を返します。これは共分散と呼ばれます。

  •  デストラクターの書き換え (基本クラスと派生クラスのデストラクターの名前は異なります)
基底クラスのデストラクタが仮想関数の場合、この時点で派生クラスのデストラクタが定義されていれば、virtualキーワードを追加するかどうかに関係なく、基底クラスのデストラクタで書き換えられますが、基本クラスと派生クラスのデストラクタ名が異なります。関数名が異なるため、書き換え規則に違反しているように見えますが、そうではありません。ここでは、コンパイラがデストラクタ名を特別に処理していることがわかります。コンパイル後、デストラクタ名は一律に処理されます。デストラクターとして (仮想関数として定義することを推奨します)

  2.3 C++11 オーバーライドと最終

C++ には関数の書き換えに関するより厳しい要件がありますが、場合によっては、過失により、関数名がアルファベット順に記述される可能性がありますが、オーバーロードを構成することはできません。また、このエラーはコンパイル中には報告されず、プログラムの実行中にのみ報告されます。期待した結果が得られずにデバッグするのは無駄ですので、次のようにします。
C++11 には、override と fifinal という 2 つのキーワードが用意されており、ユーザーが書き換えるべきかどうかを判断するのに役立ちます。
  • 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 コンセプト

仮想関数の後に =0 を書き込むと この関数は 純粋な仮想関数になります 純粋な仮想関数を含むクラスは、抽象クラスと呼ばれます (インターフェイスとも呼ばれます)
class)、抽象クラスはオブジェクトをインスタンス化できません派生クラスが継承された後は、オブジェクトをインスタンス化することはできず、 純粋な仮想関数と派生クラスを書き換えるだけです。
クラスはオブジェクトをインスタンス化できます 純粋仮想関数は、派生クラスを書き換える必要があることを指定し 、純粋仮想関数は インターフェイスの継承も反映します
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 つ持ちます。これは、仮想関数のアドレスを仮想関数テーブルに配置する必要があり、仮想関数テーブルは仮想テーブルとも呼ばれるためです。

注:仮想関数はどこに存在しますか? 仮想テーブルはどこに存在しますか? 

仮想テーブルには、仮想関数ではなく仮想関数ポインタが格納されます。仮想関数は通常の関数と同じで、すべてコード セグメント内に存在しますが、ポインタは仮想テーブルに格納されます。なお、オブジェクトに格納されるのは仮想テーブルではなく、仮想テーブルポインタである。 VS でのテストにより、仮想テーブルにコード セグメントがあることが判明しました

  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= は機能しますが、実際の値はありません。

 

おすすめ

転載: blog.csdn.net/bang___bang_/article/details/130851285