C++ 上級 - ポリモーフィズム

目次

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

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

 1. 仮想機能

 2. 多型の条件

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

  3.1 共分散(基底クラスと派生クラスの仮想関数の戻り値の型が異なる)

  3.2 デストラクタの書き換え(基底クラスと派生クラスのデストラクタの名前が異なる)

 4. C++11 オーバーライドとフィファイナル

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

3. 抽象クラス

 1. コンセプト

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

 第四に、ポリモーフィズムの原理

 1. 仮想関数テーブル

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

 3. 動的バインディングと静的バインディング

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

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

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

いくつかのクイズの質問:


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

        ポリモーフィズムの概念: 一般的に言えば、さまざまな形があります.特定のポイントは、特定の動作を完了することです. さまざまなオブジェクトが完了すると、 さまざまな状態が生成されます . 例えば、切符を買う行為において、 一般の人が 切符を買うときは定価で切符を買い、 学生が 切符を買うときは半額で切符を買い、 兵士が切符を買うときは先に切符を買う。

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

 1. 仮想機能

仮想関数: つまり、virtualによって変更されたクラス メンバー関数は、仮想関数と呼ばれます。

 2. 多型の条件

        ポリモーフィズムとは、異なる継承関係を持つクラス オブジェクトが同じ関数を呼び出すことで、異なる動作が発生することです。

継承でポリモーフィズムを構成するには、次の 2 つの条件があります
1. 仮想関数は、基本クラスのポインターまたは参照を介して呼び出す必要があります。
2. 呼び出される関数は仮想関数である必要があり、派生クラスは基底クラスの仮想関数を書き換える必要があります
仮想関数の書き換え: 派生クラスに、基底クラスとまったく同じ仮想関数 (つまり、派生クラスの仮想関数と基底クラスの仮想関数の戻り値の型、関数名、およびパラメーター リスト) があります。サブクラスの仮想関数と呼ばれる基本クラスの仮想関数はオーバーライドされます。
class Person 
{
public:
	//虚函数重写    三同(函数名,参数,返回类型)
 virtual void BuyTicket() { cout << "Person->买票-全价" << endl; }
};
class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "Student->买票-半价" << endl; }
};
//多态的条件:
//1.虚函数重写
//2.父类指针或引用调用虚函数
void func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	//普通调用:跟调用的类型有关
	//多态调用:跟指向的对象有关
	Person pn;
	Student st;
	func(pn);
	func(st);

	return 0;
}

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

  3.1 共分散(基底クラスと派生クラスの仮想関数の戻り値の型が異なる)

        派生クラスが基底クラスの仮想関数をオーバーライドする場合、基底クラスの仮想関数の戻り値の型が異なります。つまり、基本クラスの仮想関数は、基本クラス オブジェクトのポインターを返します。
派生クラス仮想関数が派生クラス オブジェクトへのポインターまたは参照を返す場合、それは共分散と呼ばれます。
class A{};
class B : public A {};
//协变:三同中,返回值可以不同,但是要求返回值必须是一个父子类关系的指针或引用
class Person {
public:
     virtual A* f() {return new A;}
};
class Student : public Person {
public:
     virtual B* f() {return new B;}
};

  3.2 デストラクタの書き換え (基底クラスと派生クラスのデストラクタの名前が違う)

        基底クラスのデストラクタが仮想関数の場合、このとき派生クラスのデストラクタが定義されていれば、virtual キーワードを追加しても、基底クラスのデストラクタで書き換えられますが、基底クラスと派生クラスのデストラクタの名前が異なります。関数名が違うだけで書き換え規則に違反しているように見えますが、そうではありません. ここでは、コンパイラがデストラクタの名前を特別扱いしていることがわかります. コンパイル後、デストラクタの名前は一律に処理されます.デストラクタとして。これはデストラクタだけでなく、他の関数も書き直す限り、サブクラスの仮想関数は virtual キーワードを追加する必要はありません。読みやすくするために、 virtual キーワードを追加することをお勧めします。

 要求されたメモリがある場合、virtual のない親クラスのデストラクタがメモリ リークを引き起こす可能性があります。

 4. C++11 オーバーライドフィファイナル

C++に は関数の書き換えに関するより厳しい要件がありますが、場合によっては、過失により、関数名がアルファベット順に記述される可能性がありますが、オーバーロードを構成することはできず、このエラーはコンパイル中には報告されず、プログラムが実行されている場合にのみ報告されます期待される結果を得ずにデバッグするのは無駄な ので、 C++11 は overridefifinalの 2 つのキーワード を提供します。これは、ユーザーが書き直すかどうかを検出するのに役立ちます。
  4.1  fifinal : 仮想関数を変更し、仮想関数がもはや書き換えられないことを示します
  4.2  オーバーライド: 派生クラスの仮想関数が基本クラスの仮想関数をオーバーライドするかどうかを確認し、そうでない場合はコンパイルしてエラーを報告します。

  5. オーバーロード、オーバーライドオーバーライド、隠蔽再定義の比較


3. 抽象クラス

 1. コンセプト

        virtual function の後に =0 と書くと 、この関数は純粋な仮想関数になります。 純粋仮想関数を含むクラスは、抽象クラス (インターフェイスとも呼ばれます) と呼ばれます。
クラス)、抽象クラスはオブジェクトをインスタンス化できません 派生クラスが継承された後、オブジェクトをインスタンス化することはできず、純粋仮想関数派生クラスを書き換えるだけです。
クラスはオブジェクトをインスタンス化できます純粋仮想関数は、派生クラスを書き換える必要があることを指定し、純粋仮想関数はインターフェイスの継承も反映します。
//抽象类-> 不能实例化出对象
class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
int main()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	
	return 0;
}

サブクラス オブジェクトは、純粋仮想関数を書き換えることによってのみインスタンス化できます

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

        通常の関数の継承は一種の実装継承であり、派生クラスは基底クラスの関数を継承し、関数を使用でき、関数の実装を継承します。仮想関数の継承は一種のインターフェース継承であり、派生クラスは基底クラスの仮想関数のインターフェースを継承する.目的は書き換えてポリモーフィズムを実現することであり,継承されるのはインターフェースである. したがって、ポリモーフィズムを実装しない場合は、関数を仮想関数として定義しないでください。

 第四に、ポリモーフィズムの原理

 1. 仮想関数テーブル

 上の図からわかるように、Base クラスには 8 バイトあります.b に加えて、__vfptr ポインターがオブジェクトの前に配置されます (プラットフォームによっては、オブジェクトの最後に配置される場合があることに注意してください。オブジェクト内のポインタは、仮想関数テーブル ポインタと呼ばれます(vはvirtualを表しfは functionを表します)仮想関数を含むクラスには、少なくとも 1 つの仮想関数テーブル ポインターがあります。これは、仮想関数のアドレスを仮想関数テーブルに配置する必要があり、仮想関数テーブルは仮想テーブルとも呼ばれるためです。

コードを少し変更して読み続けます。

class Base
{
public:
	virtual void Func1(){cout << "Base::Func1()" << endl;}
	virtual void Func2(){cout << "Base::Func2()" << endl;}
	void Func3(){cout << "Base::Func3()" << endl;}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;

	return 0;
}

観察とテストを通じて、次の問題が見つかりました。

1. 派生クラス オブジェクト d にも仮想テーブル ポインターがあります. d オブジェクトは 2 つの部分で構成されます. 1 つは親クラスから継承されたメンバーであり、仮想テーブル ポインターは既存の部分です.独自のメンバー。

2. 基底クラス b オブジェクトの仮想テーブルと派生クラス d オブジェクトの仮想テーブルが異なる. ここで Func1 が書き換えられていることがわかる. つまり、書き換えられた Derive::Func1が d の仮想テーブルに 格納 されている. 仮想関数 カバレッジ とも呼ばれるカバレッジは、仮想テーブル内の仮想関数のカバレッジを指します。書き直すことをシンタックス、カバレッジをプリンシパルレイヤーと呼びます。
3. また、 Func2 は継承後の仮想関数なので仮想テーブルに入れ、 Func3 も継承しますが仮想関数ではないので仮想テーブルには入れません。
4. 仮想関数テーブルの本質は、仮想関数ポインタを格納するポインタ配列であり、通常、この配列の最後に nullptrが配置されます
5. 派生クラスの仮想テーブル生成を要約する: a. まず、基底クラスの仮想テーブルの内容を派生クラスの仮想テーブルにコピーする b. 派生クラスが基底クラスの仮想関数を書き換える場合は、使用する派生クラス 自身の仮想関数は、仮想テーブルの基底クラスの仮想関数をオーバーライドします c. 派生クラスの新しく追加された仮想関数は、派生クラスの宣言の順序に従って、派生クラスの仮想テーブルの末尾に追加されますクラス。
ここでもう 1 つの紛らわしい質問があります。仮想関数はどこに存在するのでしょうか。仮想テーブルはどこにありますか?
        仮想テーブルには、 virtual functions ではなく、 virtual function pointers が格納されます.仮想関数は、通常の関数と同じであり、すべてコード セグメント内に存在します。
彼のポインターは再び仮想テーブルに格納されます。また、オブジェクトに格納されるのは仮想テーブルではなく、仮想テーブル ポインタです。下の図を確認すると、VS にコード セグメントがあることがわかります。

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

上記で長い間分析されてきたポリモーフィズムの原理とは?

1.上の図の赤い矢印を観察すると、 p がmikeオブジェクトを指している場合p->BuyTicket がmikeの仮想テーブルで仮想関数 Person::BuyTicket を見つけること がわかります
2. 上の図の青い矢印を見ると、p がjohnsonオブジェクト を指している場合 p->BuyTicketがjohnsonの仮想テーブルにあることがわかります。
見つかった仮想関数は Student::BuyTicketです
3. このように、異なるオブジェクトが同じ動作を完了すると、異なるフォームが表示されます。
class Base
{
public:
	virtual void Func1(){cout << "Base::Func1()" << endl;}
	virtual void Func2(){cout << "Base::Func2()" << endl;}
	void Func3(){cout << "Base::Func3()" << endl;}
private:
	int _b = 1;
	char _ch;
};

class Derive : public Base
{
public:
	virtual void Func1(){cout << "Derive::Func1()" << endl;}

	void Func3(){cout << "Derive::Func3()" << endl;}
private:
	int _d = 2;
};
int main()
{
	cout << sizeof(Base) << endl;
	Base b;
	Derive d;

	// 普通调用 -- 编译时/静态 绑定
	//谁的指针就去调用谁的函数
	Base* ptr = &b;
	ptr->Func3();
	ptr = &d;
	ptr->Func3();

	// 多态调用 -- 运行时/动态 绑定
	//指向谁就去调用谁的函数
	ptr = &b;
	ptr->Func1();
	ptr = &d;
	ptr->Func1();

	return 0;
}

  ポリモーフィズムには 2 つの条件があり、1 つは仮想関数の書き換えであり、もう 1 つは仮想関数を呼び出すための親クラスのポインタまたは参照です。
ポリモーフィズムの原則は、親クラスのポインタまたは参照です. 親クラスのオブジェクトを指す場合は、親クラスのオブジェクト内の仮想テーブルに移動して、仮想関数を見つけて実行します. サブクラスのオブジェクトを指す場合、サブクラス オブジェクト内の親クラスの仮想テーブルに移動し、実行する仮想関数を見つけます。 要約すると、1 つの文は、親クラス ポインターが指す関数を呼び出すことです。
ポリモーフィズムを満たした後の関数呼び出しは、コンパイル時に決定されるのではなく、実行後にオブジェクト内で検出される ことがわかります。ポリモーフィズムを満たさない関数呼び出しは、コンパイル時に確認されます

 3. 動的バインディングと静的バインディング

  1. アーリー バインディング(アーリー バインディング)とも呼ばれる静的バインディングは、プログラムのコンパイル中のプログラムの動作を決定します静的ポリモーフィズムとも呼ばれます
  2. ダイナミック バインディングは、レイト バインディング(レイト バインディング)とも呼ばれ、プログラムの実行中に取得された特定の型に従ってプログラムの特定の動作を決定し、特定の関数を呼び出すことであり、動的ポリモーフィズムとも呼ばれます

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

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

下図のように、監視ウィンドウのサブクラスオブジェクトの仮想テーブルに func3 が表示されないのは、vs が監視ウィンドウを設定したためですが、仮想関数 func3 のメモリ上のアドレスは確認できます。ウィンドウ。メモリ ウィンドウを介して、vs の仮想テーブルの終了フラグが null ポインターで終了していることがわかります。しかし、これは少し面倒に思えます。仮想関数のアドレスを出力する関数を書くことができます。

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; }
	void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
}; 
typedef void(*VFptr)();//函数指针
void PrintVFptr(VFptr ptr[ ])
{
	for (int i = 0; ptr[i] != nullptr; ++i)
	{
		printf("[%d] 虚函数:%p ->", i, ptr[i]);
		ptr[i]();
	}
}
int main()
{
	Base b;
	PrintVFptr((VFptr*)(*(int*)&b));
    //PrintVFptr((VFptr*)(*(void**)&b));//同时适应32位和64位机器
	cout << endl;
	Derive d;
	PrintVFptr((VFptr*)(*(int*)&d));

	return 0;
}
  •         アイデア: 仮想テーブルのポインタであるオブジェクト b と d の最初の 4 バイトを取り出します.仮想関数テーブルは本質的に仮想関数ポインタを格納するポインタの配列であると前に述べました.nullptr は最後に配置されますこの配列。
  •         最初に b のアドレスを取得し、それを int* ポインターに強制し、次に値を逆参照し、b オブジェクト ヘッダーの 4 バイトの値を取得します. この値は、仮想テーブルへのポインターです。 VFptr* は、仮想テーブルであるため VFPTR 型 (仮想関数ポインタ型) を格納する配列です。仮想テーブル ポインターを PrintVTptr に渡して、仮想テーブルを印刷します。
  •         コンパイラが仮想テーブルをきれいに処理しない場合があり、仮想テーブルの最後に nullptr が配置されないため、仮想テーブルを出力するコードが頻繁にクラッシュすることに注意してください。 、これはコンパイラの問題です。ディレクトリ バーの [Generate] - [Clean Solution] をクリックして、コンパイルするだけです。

 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;
};
typedef void(*VFptr)();
void PrintVFptr(VFptr ptr[ ])
{
	for (int i = 0; ptr[i] != nullptr; ++i)
	{
		printf("[%d] 虚函数:%p ->", i, ptr[i]);
		ptr[i]();
	}
	cout << endl;
}
int main()
{
	Base1 b1;
	PrintVFptr((VFptr*)(*(void**)&b1));
	Base2 b2;
	PrintVFptr((VFptr*)(*(void**)&b2));

	Derive d;
	PrintVFptr((VFptr*)(*(int*)&d));
    //跳过Base1,指向第二个虚表
	//PrintVFptr((VFptr*)(*(int*)((char*)&d+sizeof(Base1))));
	//切片,直接指向第二个虚表
	Base2* de = &d;
	PrintVFptr((VFptr*)(*(int*)de));
	return 0;
}

        サブクラスの複数の継承の後、2 つの仮想テーブルがあることがわかりますが、2 つの仮想テーブルが必要であるという意味ではありません。たとえば、親クラスには仮想関数がありません。サブクラス オブジェクトの仮想関数は、通常、最初に継承された仮想テーブルにあります。

いくつかのクイズの質問:

  1. ポリモーフィズムとは 回答: 1.一般的に言えば、さまざまな形態があります.特定のポイントは、特定の動作を完了することです. さまざまなオブジェクトが完了すると、さまざまな状態が生成されます. 2. ポリモーフィズムとは、同じ関数を呼び出す異なる継承関係を持つクラス オブジェクトを指し、異なる動作をもたらします。静的多型と動的多型
  2. オーバーロード、書き換え覆い、再定義隠蔽)とは回答:                                                                 オーバーロード: 同じスコープ内にあり、同じ関数名とパラメーターを持つ 2 つの関数は、オーバーロードを構成します。書き換え: 仮想関数の継承 + トリプル (関数名、パラメーター、戻り値の型)。非表示: サブクラスは親クラスを継承し、2 つの関数は同じ名前を持ち、2 つの親クラスとサブクラスで同じ名前の関数は書き換えを構成しないか、または非表示になります。
  3. ポリモーフィズムの実現原理?回答:親クラス オブジェクトを指している親クラスのポインタまたは参照は、親クラス オブジェクト内の仮想テーブルに移動して仮想関数を見つけて実行し、サブクラス オブジェクトを指して、その仮想テーブルに移動します。サブクラス オブジェクト内の親クラスを呼び出して、実行する仮想関数を見つけます。要約すると、1 つの文は、親クラス ポインターが指す関数を呼び出すことです。
  4. インライン関数を仮想関数にすることはできますか? 回答: はい。ただし、仮想関数は仮想テーブルに配置する必要があるため、コンパイラはinline属性を無視し、この関数はinlineではなくなります。ポリモーフィック呼び出しにはインライン属性がなく、通常の呼び出しは引き続きインライン属性を維持できます。
  5. 静的メンバーを仮想関数にすることはできますか? 回答: いいえ、静的メンバー関数にはthisポインターがなく、 type ::メンバー関数を呼び出して仮想関数テーブルにアクセスできないため、静的メンバー関数を仮想関数テーブルに配置することはできません。
  6. コンストラクターを仮想にすることはできますか? 回答: いいえ、オブジェクトの仮想関数テーブル ポインターは、コンストラクターの初期化リストの段階で初期化されるためです。
  7. デストラクタは仮想にできますか? デストラクタが仮想関数になるのはどのような状況ですか? 回答: はい。基本クラスのデストラクタを仮想関数として定義するのが最善です。親クラスのポインタはサブクラスのオブジェクトを指しているのですが、このとき削除に行ってしまうと、ポリモーフィックでないと正しく呼び出すことができず、問題が発生します。
  8. 通常の関数へのオブジェクトアクセスは速いですか、それとも仮想関数へのアクセスは速いですか? 回答:まず、普通の物ならそれなりに速いです。ポインター オブジェクトまたは参照オブジェクトである場合、呼び出される通常の関数は高速です。これは、ポリモーフィズムを構成するためであり、実行時に仮想関数を呼び出すには、仮想関数テーブルを参照する必要があります。
  9. 仮想関数テーブルはどの段階で生成され、どこに存在しますか? 回答: 仮想関数テーブルはコンパイル段階で生成され、仮想テーブル ポインターはコンストラクターの初期化リストで初期化されます. 通常、コード セグメント (定数領域)があります
  10. C++ ダイヤモンドの継承に問題がありますか? 仮想継承の原則?回答:ダイアモンド継承には、データの冗長性とあいまいさの問題があります.共通オブジェクト メンバーを一番下に置き、継承された 2 つのメンバーのオフセットを仮想ベース テーブルから見つけ、パブリック メンバーをオフセットから見つけます. ないことに注意してください.仮想関数テーブルと仮想ベース テーブルを混同しました。
  11.  抽象クラスとは?抽象クラスの役割? 回答:純粋仮想関数を含むクラスは抽象クラスと呼ばれ、派生クラスは継承後にオブジェクトをインスタンス化できません. 純粋仮想関数を書き換えることによってのみ、派生クラスはオブジェクトをインスタンス化できます. 抽象クラスは仮想関数を強制的に書き換え、抽象クラスはインターフェースの継承関係を反映します。

おすすめ

転載: blog.csdn.net/weixin_68993573/article/details/128874712