C++【ポリモーフィズムの深い理解】

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

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

平たく言えば様々な形で、簡単に言えばある行動を完成させることであり、異なる対象が完成すると、異なる状態が生まれる。
具体的には、派生クラス オブジェクトのアドレスを基底クラス ポインターに割り当てることができます。基底クラスのポインターまたは参照を介して、基底クラスと派生クラスの両方で同じ名前と同じパラメーター リストを持つ仮想関数を呼び出すステートメントの場合、基底クラスの仮想関数が基底クラスの仮想関数であるか派生クラスの仮想関数であるかが不明です。コンパイル時に実行され、プログラムの実行時 このステートメントに到達すると、基本クラス ポインターが基本クラス オブジェクトを指している場合、基本クラスの仮想関数が呼び出され、基本クラス ポインターが派生オブジェクトを指している場合クラス オブジェクトの場合、派生クラスの仮想関数が呼び出されます。このメカニズムはポリモーフィズムと呼ばれます。

最初にポリモーフィック シナリオを見てみましょう:
最初に次の図を見てください: この操作の結果は問題ありません。まず、サブクラスでサブクラスのデストラクタを呼び出します。サブクラスのデストラクタが完了すると、親クラスのデストラクタが自動的に呼び出されます。最後に親クラスのデストラクタ。後で定義されたものは最初に破棄されます。
ここに画像の説明を挿入
次に、シーンを再び変更します。
親クラス オブジェクトまたはサブクラス オブジェクトを指すことができる親クラス ポインターがありますが、デストラクタが正しく調整されていません。親クラス オブジェクトを指すと、親クラス デストラクタが呼び出され、クラスのデストラクタは依然として親クラスを呼び出しており、サブクラスに解放するリソースがある場合、それを呼び出さないとメモリ リークが発生します。
理由: delete は、p1->destructor()、演算子 delete(p1); p2->destructor、演算子 delete(p2) の 2 つの部分で構成されているため、親クラスとサブクラスのデストラクタは隠れた関係を形成します。ポリモーフィズムがない場合は、型が誰であっても呼び出すことができ、型が親クラスへのポインターである場合は親クラスを呼び出すことができます.これは問題ありませんが、ここでは型によって呼び出されることを期待していません.ポインターのオブジェクトによって呼び出されることを期待します。親クラス オブジェクトをポイントして親クラスの破棄を呼び出し、サブクラス オブジェクトをポイントしてサブクラスの破棄を呼び出します。
ここに画像の説明を挿入
なぜ関数名の最初の部分が同じなのか、それはポリモーフィック シーンに備えるためです。同じであればデストラクタ関数を一様に呼び出すことができます。図に示すように、仮想関数を追加するにはどうすればよいですか。下:
ここに画像の説明を挿入

(2) 多型の作り方

ポリモーフィズムとは、異なる継承関係を持つクラス オブジェクトが同じ関数を呼び出すことで、異なる動作が発生することです。たとえば、child は Person から継承します。Person オブジェクトは大きいサイズの服を購入し、chlid オブジェクトは小さいサイズの服を購入します。
継承においてポリモーフィズムを構成する条件は次の2つです
1. 呼び出される関数が仮想関数であり、派生クラスが基底クラスの仮想関数(関数名、パラメータ、戻り値)を書き換えていること。さまざまなデフォルト パラメータも、タイプを参照するトリプルを構成します。
2. 仮想関数は、基本クラスのポインターまたは参照を介して呼び出す必要があります。
仮想関数: virtual によって変更されたクラス メンバー関数は、仮想関数と呼ばれます

class Person {
    
    
public:
	virtual void BuyClothes() {
    
     cout << "买衣服-大码" << endl; }
};
class child : public Person {
    
    
public:
	virtual void BuyClothes() {
    
     cout << "买衣服-小码" << endl; }
};
int main()
{
    
    
	Person p;
	child c;
	Person& p1 = p;
	Person& c2 = c;
	p.BuyClothes();
	c.BuyClothes();
	return 0;
}

ここに画像の説明を挿入
1. ポリモーフィズムを満たさない場合は、型に依存します。つまり、呼び出し元の型を確認し、この型のメンバ関数を呼び出し、ポインタか参照か、親であれば親を調整しますクラスを削除し、派生クラスの場合は派生を削除します。
2. ポリモーフィズムが満たされるかどうかは、呼び出されるオブジェクトと指されるオブジェクトに依存します。
3. 親クラスに virtual があり、サブクラスに virtual がある場合、それはポリモーフィックではなく、隠されています。

(3) 仮想関数書き換えの2つの例外

最初のもの: サブクラスは virtual を記述する必要はありません.
親クラスが virtual を持ち、サブクラスが virtual を持たない場合、それも多態的です. これは 3 つのルールに準拠していません.これはこのようなものです.親クラスが virtual と書かない場合,それは​​仮想関数であってはなりません.親クラスが仮想関数の場合,サブクラスは virtual を追加しませんが,仮想関数を書き換えます. 書き込みは、インターフェイスの継承 (関数宣言) を反映し、それをvirtual void BuyClothes() 継承し、親クラスのインターフェイスを継承し、親クラスの関数の実装を書き換えるため、仮想を追加せずに仮想関数と見なされます。親クラスは仮想関数であり、継承されます。
2つ目:共分散、戻り値が異なる前提 親子関係 ポインタまたは
参照 値の型が異なります。つまり、基本クラス仮想関数は基本クラス オブジェクトへのポインターまたは参照を返し、派生クラス仮想関数は派生クラス オブジェクトへのポインターまたは参照を返します。写真に示すように:
ここに画像の説明を挿入

(4) 知識ポイントを統合するための古典的な分析

次の質問を参照してください。

class A
{
    
    
public:
	virtual void func(int val = 1) {
    
     cout << "A=" << val << endl; }
	virtual void test() {
    
     func(); }
};
class B :public A
{
    
    
public:
	void func(int val=0) {
    
     cout << "B=" << val << endl; }
};
int main()
{
    
    
	B* p = new B;
	p->test();
	return 0;
}

A と B の func は書き換えで構成されており、これは 3 つの同じを満たすものであり、デフォルトのパラメーターが含まれているため、3 つの曲は型についても語られていますが、これも書き換えです. , そしてサブクラスはそれが追加されるかどうかは問題ではありません. それは親クラスのインターフェースを継承するので仮想関数でもあります. 書き換えられた実装はポリモーフィズムの最初の条件を満たします. B は A を継承するので, p はテストを呼び出しますA を呼び出す人は誰でも this であり、その型は A* です。This は継承されますが、関数のパラメーターは変更されません。これは、p がサブクラス ポインターであることと同じです。つまり、サブクラス オブジェクトは a に渡されます。ポリモーフィズムの 2 番目の条件を満たす親クラス ポインター。ポリモーフィズムが満たされている場合、指している者はサブクラスを指している者を呼び出し、サブクラスのメンバー関数を呼び出します。
以上をまとめると、サブクラスのメンバー関数のうち、func は仮想関数、書き換えられるのは実装、継承されるのはインターフェースであり、同時にデフォルト値を継承すると、最終結果は B=1; となります。

(5)override 和 final

C++ には関数の書き換えに関する厳しい要件がありますが、過失によって関数が書き換えられない場合があります。たとえば、親クラスが virtual を追加するのを忘れた場合などです。ユーザーが書き換えを検出するのに役立ちます。
1. final: 仮想関数を変更します, これは、仮想関数がもはや書き換えられないことを示します. 仮想関数を作成し、書き換えられたくない場合は、final を使用してください.

class P
{
    
      public: virtual void sleep() final {
    
    }};
class s :public P
{
    
      public: virtual void sleep() {
    
    cout << "睡觉" << endl;}};

2.override: サブクラスの仮想関数が親クラスの仮想関数をオーバーライドするかどうかを確認し、そうでない場合はコンパイルしてエラーを報告します。基本的に、このキーワードを使用すると、強制的に書き換えることができます。

class P
{
    
    public:virtual void sleep(){
    
    }};
class S :public P
{
    
    public:virtual void sleep() override {
    
    cout << "睡觉" << endl;}};

(6) まとめ

通常の関数の継承は一種の全体継承であり、サブクラスは親クラスの関数を継承し、関数を使用でき、関数の実装を継承します。仮想関数の継承はインターフェース継承の一種で、サブクラスは親クラスの仮想関数のインターフェースを継承し、書き換えてポリモーフィズムを形成することを目的としており、継承されるのはインターフェースです。ポリモーフィズムを実装しない場合は、関数を仮想関数として定義しないでください。
仮想関数は書き換えのために生まれ、書き換えはポリモーフィズムのために生まれます。

第二に、ポリモーフィズムの原則

(1) 仮想関数テーブルを知る

最初に、仮想関数を含むクラスのサイズを観察します。
ここに画像の説明を挿入
オブジェクトの先頭に追加のポインター __vfpt があるため、なぜ 4 バイトではなく 8 バイトなのか、このポインターは仮想関数テーブル ポインターと呼ばれ、v は仮想を表します。 f は関数を表します。仮想関数を含むクラスには、少なくとも 1 つの仮想関数テーブル ポインターがあります。これは、仮想関数のアドレスを仮想関数テーブルに配置する必要があり、仮想関数テーブルは仮想テーブルとも呼ばれるためです。しかし、派生クラスでこのテーブルに配置されているもの。
図に示すように、
ここに画像の説明を挿入
次のコードを見てください。

class Person {
    
    
public:
	virtual void BuyClothes() {
    
     cout << "买衣服-大码" << endl; }


};
class child : public Person {
    
    
public:
	virtual void BuyClothes() {
    
     cout << "买衣服-小码" << endl;  }
};
int main()
{
    
    
	Person p;
	child c;
	Person& p1 = p;
	Person& c2 = c;
	p1.BuyClothes();
	c2.BuyClothes();
	return 0;
}

ここに画像の説明を挿入
上記を観察すると、p1 が p オブジェクトを指している場合、p->BuyClothes は p の仮想テーブルで仮想関数 Person::BuyClothes を見つけることがわかります。c2 が c オブジェクトを指している場合、c2->BuyClothes は c の仮想テーブルで仮想関数 child::BuyClothes を見つけます。

(2) 原理の深い理解

分析:上記はどのように行われたのですか?親クラスの仮想関数は親クラス オブジェクトの仮想テーブルに格納され、サブクラスの仮想関数はサブクラス オブジェクトの仮想テーブルに格納されていることがわかります。コンパイラは、構造体がポリモーフィズムを構成するかどうかも判断します: ポリモーフィズムを構成
しない場合、呼び出しアドレスはコンパイル時に直接決定され、それが指している相手とは関係ありません. 親クラスのポインタまたは参照である場合, 親クラスの関数を呼び出して親クラスに行く. クラスでこの関数を見つけてアドレスを決定する. 簡単に言えば, 型に関連する通常の呼び出し. ポリモーフィズムを構成する
場合,コンパイラは呼び出し元を認識せず, まず独自の仮想関数テーブルを生成します. 指し示した仮想テーブルを見つけるには, 指し示したオブジェクトを調べます. 親クラスを指すと, 親クラスの仮想関数が親クラスのオブジェクト. サブクラスをポイントすると、それを切り取ってサブクラスの仮想関数を見つけることができます.
さらに、私たちが使用する Person 参照またはポインターは、それが親クラスを指しているのかサブクラスを指しているのかわからない. 親クラスのオブジェクトを私に渡し、親クラスのオブジェクトの仮想テーブルを見てください. この仮想関数が見つかった場合、 サブクラスのオブジェクトが渡された場合、私はここでそれをカットします. 実際、私が見ているのは親クラスのオブジェクトでもありますが、これは純粋な親クラスのオブジェクトではなく、サブクラスのオブジェクト内の親クラスの一部です. p1 呼び出しと c2 呼び出しは同じ関数であり、呼び出しの最終結果は異なります。これは、異なるオブジェクトが渡され、異なるオブジェクトの vtables には独自の仮想関数があり、親クラスを呼び出すために親クラスを指し、この 2 行のコードは区別されません. 表示されるのは親クラス オブジェクトですが、それは直接の親クラス オブジェクト、またはサブクラス オブジェクト内の親クラス オブジェクトです.
再解析:
書き換えにはカバレッジという名前もあります. 書き換えとは, 親クラスのインターフェースを継承し, その実装を書き換えることです. 文法レベルでの上位概念です. クラスに対応する仮想テーブルの場所はイメージをコピーし,原則レベルの概念である独自の仮想関数で上書きします。ポリモーフィズムは、検索するオブジェクトの仮想テーブルで実行されるため、テーブルが既に生成されているため
、渡されたものは何でも実行できます、親クラスの仮想テーブルには親クラスの仮想関数が格納され、子クラスの仮想テーブルにはサブクラスの仮想関数が格納されます.親クラスに渡された場合、親クラスの仮想関数を見つけるためにテーブルに移動します.子クラスに渡されると、サブクラスの仮想関数を探します。設計された実装です。
カバレッジの理解を深めましょう:
上記の関数の親クラスに別の仮想関数を追加すると、結果:
ここに画像の説明を挿入
最初の仮想関数が書き換えられ、サブクラスは最初に親クラスのパフォーマンスをコピーし、書き換えられたものをサブクラスにコピーします。自分自身、書き換えられていないことをカバレッジとは呼びません。親クラスの関数が仮想関数を持つ場合、まず親クラスの仮想テーブルの内容をサブクラスの仮想テーブルにコピーすることを意味し、サブクラスが親クラスの仮想関数を書き換える場合、サブクラスは独自の仮想関数を使用する関数は、仮想テーブル内の親クラスの仮想関数をオーバーライドし、サブクラスでの宣言の順序に従って、サブクラス自身の新しく追加された仮想関数がサブクラスの仮想テーブルの末尾に追加されます。

(3) トラブルシューティング

ポリモーフィック条件が書き換えられるのはなぜですか?
仮想テーブル内の仮想関数の位置を上書きする必要があるため、書き換えられた場合にのみ、サブクラスの仮想テーブルは対応する仮想関数を上書きします。
なぜポインタまたは参照なのでしょうか?
ポインタ参照は、親クラスのオブジェクトとサブクラスのオブジェクトの両方を指すことができるためです. サブクラスを指していても、表示されるのは親クラスですが、サブクラスにカットされています.
仮想関数アドレスをオブジェクト ヘッダーの直前に格納しないのはなぜですか?
オブジェクトには複数の仮想関数が存在する可能性があり、同じ型の仮想テーブルは同じであるため、仮想関数テーブルは仮想関数ポインタの配列であり、これはコンパイル時に決定されています。ポリモーフィズムは見つけることです。

(4) オブジェクトがポリモーフィズムを達成できないのはなぜですか?

ポインタまたは参照の場合は親クラスを指すか親クラスを参照し、ポインタまたは参照の場合はサブクラスを指す場合は指すサブクラスオブジェクトの親クラスの部分を切り取りますその部分、そしてあなたが見るものはまだサブクラスにあります その部分の部分、その部分の仮想テーブルはまだサブクラスです。
ここに画像の説明を挿入

オブジェクトなら親クラスなら問題ない サブクラスならサブクラスは親クラスをスライスしてメンバをコピーする 仮想テーブルをコピーしないと中の仮想テーブル親クラスのオブジェクトは常に親クラスになります仮想関数、サブクラス オブジェクトを c2 に渡す場合を除き、メンバーのコピーに加えて、サブクラスの仮想テーブルもコピーしますが、サブクラスがスライスされている場合はあえて仮想テーブルをコピーしません、ポインターまたは参照は、パーツまたは参照エイリアスを指している部分に切り取られているため、ポリモーフィズムを実現するためにあえてコピーしますが、実際にはコピーしないと、親クラスオブジェクトの仮想テーブルが親クラスの仮想関数またはサブクラスの仮想関数 わかりません。おそらくこれは親クラスのオブジェクトであり、親クラスを指しており、親クラスの仮想テーブルを探しています。問題ありませんが、オブジェクトである可能性もあります親クラスのオブジェクトをサブクラスオブジェクトにコピーしたり代入したりするだけでは、まったく不明です. 親クラスオブジェクトを与えると、親クラスオブジェクト内の仮想テーブルの所有者を確認できなくなります.ポリモーフィックな場合、それを呼び出すためのポインタ、ポインタは親クラスを指します.サブクラスを呼び出すことも可能です.親クラスのオブジェクトにはサブクラスの仮想テーブルもあるため、これは無理があります. 親クラス オブジェクトは純粋な親クラス オブジェクトであり、親クラス オブジェクトは親クラスの仮想テーブルである必要があります。
オブジェクトの場合、スライス時にメンバーのみがコピーされ、仮想テーブルはコピーされません。次の図に示すように、c がサブクラスで、c2 が親クラスです。
ここに画像の説明を挿入

(5) 動的バインディングと静的バインディング

1. アーリー バインディング (アーリー バインディング) とも呼ばれる静的バインディングは、関数のオーバーロード、cin、cout などの静的ポリモーフィズムとも呼ばれる、プログラムのコンパイル中のプログラムの動作を決定します。
2. 動的結合は、動的結合 (遅延結合) とも呼ばれ、プログラムの実行中に取得された特定の型のポインターに従ってプログラムの特定の動作を決定し、特定の関数を呼び出すことであり、動的ポリモーフィズムとも呼ばれます。

(6)補足

仮想テーブルには、仮想関数ではなく、仮想関数ポインタが格納されます. 仮想関数は通常の関数と同じであり、それらはすべてコードセグメントに存在しますが、それらのポインタは仮想テーブルに格納されます. オブジェクトに格納されるのは、仮想テーブルではなく、仮想テーブル ポインターです。次に、仮想テーブル ストレージを検証すると、vs の下にコード セグメントがあることがわかります。コード セグメントは、アセンブリからバイナリへの命令を生成するコンパイル済みコードです。コンパイル中に仮想テーブルが生成されます。オブジェクト内の仮想テーブルへのポインターは、実行時のコンストラクターの初期化リスト フェーズで生成されます。

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

(1) 単一継承で仮想関数テーブルを出力する方法

class A {
    
    
public:
	virtual void func1() {
    
     cout << "A::func1" << endl; }
	virtual void func2() {
    
     cout << "A::func2" << endl; }
private:
	int a;
};
class B :public A{
    
    
public:
	virtual void func1() {
    
     cout << "B::func1" << endl; }
	virtual void func3() {
    
     cout << "B::func3" << endl; }
	virtual void func4() {
    
     cout << "B::func4" << endl; }
private:
	int b;
};
int main()
{
    
    
    A a;
	B b;
	return 0;
}

ここに画像の説明を挿入
ここに画像の説明を挿入

監視ウィンドウを観察すると、派生クラスの func3 と func4 が見えず、派生クラスの仮想テーブルには入力されていませんが、メモリからは見えることがわかります。ここでは、コンパイラの監視ウィンドウが意図的にこれら 2 つの関数を非表示にしていますが、これもバグと見なすことができます。真かどうかはdの仮想テーブルで確認できます。
コードを使用して、vtable 内の関数を出力できます。

class A {
    
    
public:
	virtual void func1() {
    
     cout << "A::func1" << endl; }
	virtual void func2() {
    
     cout << "A::func2" << endl; }
private:
	int a;
};
class B :public A {
    
    
public:
	virtual void func1() {
    
     cout << "B::func1" << endl; }
	virtual void func3() {
    
     cout << "B::func3" << endl; }
	virtual void func4() {
    
     cout << "B::func4" << endl; }
private:
	int b;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
    
    
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
    
    
		printf(" [%d]:%p\n", i, vTable[i]);
		VFPTR f = vTable[i];
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
    
    
	A a;
	B b;
	VFPTR* vTableb = (VFPTR*)(*(int*)&a);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&b);
	PrintVTable(vTabled);
	return 0;
}

上記のコード: 最初に関数ポインタを型定義し、関数ポインタを宣言し、定義された変数が内部にある必要があります。
In the PtinfVFtable() function, VF_PTR[] is a array of function pointers. To print this array, use a for loop. VS では、仮想関数配列の後ろに空スペースを置き、空スペースに遭遇すると停止します。 .
重要なのは、メイン関数で、仮想テーブルのアドレスを取り出して渡す必要があることです。
2つの方法:
最初の方法は、仮想テーブルのポインタであるオブジェクトaとbの最初の4バイトを取り出すことです. 仮想関数テーブルの本質は、仮想関数ポインタを格納するポインタの配列です. nullptrを配置します.配列の最後に。
最初に b のアドレスを取得し、それを intポインターに変換します。次に、値を逆参照し、仮想テーブルへのポインターである b オブジェクトの先頭にある 4 バイトの値を取得し、それを VFPTR に強制します。仮想テーブルはストレージであるため VFPTR 型 (仮想関数ポインター型) の配列; 仮想テーブル ポインターは PrintVTable に渡され、仮想テーブルを出力します。コンパイラが仮想テーブルをきれいに処理しない場合があり、仮想テーブルの最後に nullptr が配置されず、範囲外になるため、仮想テーブルを出力するコードがクラッシュすることが多いことに注意してください。これはコンパイラの問題です。ディレクトリ バーで [Generate] - [Clean Solution] をクリックし、コンパイルします。
2 番目のタイプ:(VFPTR*)(*(int*)&a);に置き換えるだけです(*(VF_PTR**)&a)最初に言っておきますが、int* の配列がある場合、その最初の要素アドレスは int** である必要があります。これは、仮想関数テーブルのポインター配列が VF_PTR* であることに類似していますが、それを過去に渡すときは、最初にレベル 2 の ** を解き、型が一致するように参照が * になります。渡さ((VF_PTR*)&a)れるアドレスはオブジェクトの最初の 4 バイトにあり、4 バイトを間接的に取得するには逆参照する必要があるため、直接記述しないでください。オブジェクトの最初の 4 バイトは VF_PTR* です。取得方法は次のとおりです。このように変更すると、オブジェクト全体の最初のアドレスが渡されますが、これは私たちが望んでいるものではありません。
プラットフォームが変更されると int サイズが変更される可能性があるため、2 番目のタイプは最初のタイプよりも移植性が高くなります。2 番目のタイプはアダプティブですが、ポインタも変化します。
最終実行結果:
ここに画像の説明を挿入

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

(1) 多重継承仮想関数テーブルで発生する異なる仮想アドレスの問題

最初に次のコードを観察し、ウィンドウを監視します。

class A {
    
    
public:
	virtual void func1() {
    
     cout << " A::func1" << endl; }
	virtual void func2() {
    
     cout << " A::func2" << endl; }
private:
	int _a;
};
class B :public A {
    
    
public:
	virtual void func1() {
    
     cout << " B::func1" << endl; }
	virtual void func2() {
    
     cout << " B::func2" << endl; }
private:
	int _b;
};
class C :public A, public B
{
    
    
public:
	virtual void func1() {
    
     cout << " B::func1" << endl; }
	virtual void func3() {
    
     cout << " B::func3" << endl; }
private:
	int _c;
};
//typedef void(*VFPTR) ();
//void PrintVTable(VFPTR vTable[])
//{
    
    
//	for (int i = 0; vTable[i] != nullptr; ++i)
//	{
    
    
//		printf(" [%d]:%p:\n", i, vTable[i]);
//		VFPTR f = vTable[i];
//		f();
//	}
//	cout << endl;
//}
int main()
{
    
    
	C c;
//VFPTR* vTableb = (VFPTR*)(*(int*)&c);
//PrintVTable(vTableb);
//VFPTR* vTabled = (VFPTR*)(*(int*)((char*)&c+sizeof(A)));
//PrintVTable(vTabled);

	return 0;
}

ここに画像の説明を挿入
最初に仮想テーブルを印刷しないでください.監視ウィンドウから、c は 2 つの仮想テーブルを持っています.これは、A と B を同時に継承し、func1 を書き換えますが、func3 が配置されている場所です.このとき、最初に 2 つの仮想テーブルを印刷できます.テーブル:
印刷方法:
最初の仮想テーブルは、主に 2 番目に長いため、簡単に印刷できます. C では、A と B は連続していません. 最初に A をスキップして、A バイトのサイズを追加する必要があります. A*、そして 1 を追加することは A を追加することです。ここでは &c を char* に強制し、バイトを追加する必要があります。offset: を使用することもできますB* b=&c;PrintVFTable((VF_PTR*)(*(int*)(b)))。スライスのポインタは自動的にオフセットされます。図に示すように、最初の仮想テーブルに func3 が格納されていることがわかります
ここに画像の説明を挿入
また、多重継承後に、仮想テーブルに書き換えられた func1 のアドレスが異なることもわかりました。
ここに画像の説明を挿入

(2) 多重継承における異なる仮想アドレスの原則

上記で観察された現象に基づいて、なぜ異なるべきかを考えてみてください。

class A {
    
    
public:
	virtual void func1() {
    
     cout << "  A::func1" << endl; }
	virtual void func2() {
    
     cout << "  A::func2" << endl; }
private:
	int _a;
};
class B :public A {
    
    
public:
	virtual void func1() {
    
     cout << "  B::func1" << endl; }
	virtual void func2() {
    
     cout << "  B::func2" << endl; }
private:
	int _b;
};
class C :public A, public B
{
    
    
public:
	virtual void func1() {
    
     cout << "  B::func1" << endl; }
	virtual void func3() {
    
     cout << "  B::func3" << endl; }
private:
	int _c;
};
int main()
{
    
    
	C c;
	A* a = &c;
	B* b = &c;
	a->func1();
	b->func1();
	return 0;
}

ここに画像の説明を挿入

最初に上記のコードを見て、a と b が func1 を呼び出し、同じ関数を呼び出します。最終結果は同じです。カプセル化されたアドレスが含まれていると見なすことができます。
図に示すように、それぞれの分解を簡単に見てみましょう。
ここに画像の説明を挿入

1 つ目はアドレスを呼び出すことです. このアドレスは仮想テーブルから取得されます. このアドレスは jmp 命令です. jmp 命令は次にアドレスを jmp して関数の実アドレスに到達します. は通常の通話です。
2 つ目は、jmp を連続して数回実行することです。これは、キー命令 sub、ecx、8 が中間にあるため、数回巡回することと同じです。ecx は this ポインターを格納し、a と b はサブクラス関数を呼び出し、そしてa は Subclass 関数を呼び出し、ポインターは問題ありません。a はオブジェクトの先頭を指し、このポインターはサブクラス オブジェクトを指し、処理せずにサブクラス オブジェクトの先頭を指すだけですが、b は this ポインターを呼び出して実行します。オブジェクトの先頭を指していないため、この命令は正しいポインタです。マイナス 8 は A のサイズを引いたものです。誰が誰を継承し、誰が修正します。
結局、彼らは同じ住所に来ました。

4.抽象クラス

仮想関数の後ろに =0 を書くと、この関数は純粋な仮想関数になり、実装は書かれません。純粋仮想関数を含むクラスは抽象クラスまたはインターフェースクラスと呼ばれます. 対応する実体を持ちません. オブジェクトをインスタンス化したくない場合は, 実際には対応する実体を持たない型でクラスを定義できます.抽象クラスとして。
抽象クラスの特徴は、オブジェクトをインスタンス化できないことと、派生クラスが継承後のオブジェクトをインスタンス化できないことであり、純粋仮想関数を書き換えることによってのみ、派生クラスはオブジェクトをインスタンス化できます。純粋仮想関数の意味の 1 つは、派生クラスを強制的に書き換えることであり、純粋仮想関数はインターフェイスの継承も反映します。

class P
{
    
    
public:
virtual void sleep() = 0;
};
class S :public P
{
    
    
public:
virtual void sleep()
{
    
    
cout << "Benz-舒适" << endl;
}
};
class T:public P
{
    
    
public:
virtual void sleep()
{
    
    
cout << "走路" << endl;
}
};
void Test()
{
    
    
P* s1 = new s;
s1->sleep();
P* t1 = new T;
t1->sleep();
}

5. ポリモーフィック問題のまとめ

インライン関数を仮想関数にすることはできますか?
理論的に言えば、インライン関数は呼び出された場所で展開され、アドレスはありません. 仮想関数であれば、仮想テーブルにアドレスが必要であり、インライン関数は仮想関数になることはできません.アドレス。しかし、実際の運用では、コンパイラは inline 属性を無視するので、コンパイルできます. lnline を追加することは、必ずしもインライン化を意味するわけではありません. インライン化は、コンパイラへの提案に過ぎないため、この関数は、もはやインライン化されていないため、vtable に入れることができます. ポリモーフィズムに準拠していない場合は、インラインの規則に従うことができます. ポリモーフィックである場合、仮想関数は仮想テーブルにアドレスを配置する必要があるため、アドレスが存在する必要があるため、インライン化できません. 実際に実行中のプロセスは、1 つの属性のみにすることができます。
静的メンバーを仮想関数にすることはできますか?
いいえ、仮想テーブルに静的メンバー関数を配置することは不適切です, 仮想テーブルは指定されたオブジェクトを介して検出されるため. 静的メンバー関数にはこのポインターがなく、オブジェクト型からのみアクセスできますが、これは仮想関数にアクセスできません. テーブルでは、ポリモーフィズムを達成できません。
コンストラクターを仮想にすることはできますか?
いいえ、オブジェクトの仮想関数テーブル ポインターは、コンストラクターの初期化リストの段階で初期化されるためです。
コンストラクターがポリモーフィズムを実装している場合、呼び出しは仮想関数を見つける必要があります. それを見つける方法は、仮想関数テーブル ポインターが必要であり、仮想関数テーブル ポインターは呼び出しコンストラクターの初期化フェーズでのみ使用できるため、次のようになります。その後、オブジェクトがインスタンス化された後に呼び出すことができますが、コンストラクター呼び出しはオブジェクトがインスタンス化される前に呼び出されるため、矛盾し、仮想関数にすることはできず、意味がありません.
コピーの構築と代入は仮想機能になり得るか?
コピー構築は構築に似ています.代入を仮想関数として定義しないのが最善であり,コンパイラはそれをサポートできます. 仮想関数は書き換えを完了するためのものであり、親クラスを指して親クラスを調整し、サブクラスを指してサブクラスを調整します. ポリモーフィズムの後、それは親または子のいずれかです. 基底クラスの代入関数が仮想関数として定義されている場合, サブクラスの代入演算子の使用には影響しません. それらの戻り値と仮パラメータは異なります. それらが基底クラスとまったく同じで代入関数が仮想関数として定義されている場合、派生クラスは親または子になることはできず、その子は親を順番に調整する必要がありますが、これは無意味です。
フレンド機能は仮想化できますか?
いいえ、メンバー関数ではないので継承できません。
デストラクタは仮想関数にできますか?
はい、基本クラスのデストラクタを仮想関数として定義することをお勧めします。動的オブジェクトに応じて適切なデストラクタが呼び出されます。サブクラスを指す親クラス ポインターを削除して、サブクラスを分解できます。基底クラスのポインタがサブクラスのオブジェクトを指している場合, スライスが発生します. このプロセスは, 基底クラスにないメンバーを切り捨てることです. デストラクタが仮想関数として定義されていない場合, 基底クラスが削除されると,基底クラスの一部が破棄され、サブクラスが破棄されない場合、サブクラスが破棄されてリソースが解放されると、メモリ リークが発生します。
通常の関数へのオブジェクトアクセスは速いですか、それとも仮想関数へのアクセスは速いですか?
オブジェクトであれば、仮想関数であるかどうかに関係なく、通常の呼び出しであるため同じように高速です; 親クラスのポインターまたは参照である場合は、仮想関数のみが含まれ、通常の呼び出し構造体が書き換えを構成するかどうかに関係なく、多態的な呼び出しであるため、関数の方が高速です. これは通常の呼び出しではなく、コストが高すぎてコンパイラが通常の呼び出しとして認識したくない.判断する必要があります。したがって、ポリモーフィック呼び出しが仮想関数テーブルで仮想関数を見つける必要がある場合、処理が遅くなります。
書き換えていない仮想関数はどこへ行く?
仮想関数である限り、仮想テーブルに入ります. 単一継承では、サブクラスで書き換えられていない仮想関数は、それ自身の仮想テーブルに入ります. 多重継承では、書き換えのない仮想関数は、最初の親クラスに入る 仮想テーブル。
仮想関数テーブルと仮想ベーステーブル
仮想関数は、多態性を実現する仮想関数のアドレスを格納し、誰が誰を呼び出したかを参照し、仮想ベース テーブルは、データの冗長性と曖昧性を解決するオフセットを格納します。
オーバーロード | オーバーライド (オーバーライド) | 隠された比較
オーバーロード: 2 つの関数が同じスコープ内にあり、同じ関数名と異なるパラメーターを使用しています。
書き換え/カバー: 2 つの関数はそれぞれ親クラスと子クラスのスコープ内にあり、関数名、パラメーター、および戻り値は同じでなければならず (共分散を除く)、2 つの関数は仮想関数でなければなりません。
再定義/非表示: 2 つの関数は、それぞれ親クラスと子クラスのスコープ内にあり、関数名は同じです. 親クラスと子クラスで同じ名前の関数は、書き換えまたは再定義を構成しません.

おすすめ

転載: blog.csdn.net/m0_59292239/article/details/130131722