C++ オブジェクト指向の 3 つの主要な機能 -- ポリモーフィズム (強調)

1. ポリモーフィズムとは何ですか?

ポリモーフィズムとは複数の形式を意味し、同じものを参照すると、人によって影響が異なります。たとえば、Tencent Video で映画を開こうとしていますが、この映画を視聴するには VIP 権限が必要であり、あなたは VIP ではありません。ビデオを視聴しようとすると、あなたが VIP メンバーではないことが検出され、視聴できなくなります。ただし、他の人が VIP アカウントである場合は、クリックしてビデオのフルバージョンを視聴してください。これをポリモーフィズムと呼びます。

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

2.1 仮想機能

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

2.2 仮想関数の書き換え

仮想関数の書き換え(カバー): サブクラス内に親クラスと全く同じ仮想関数が存在する(つまり、サブクラス仮想関数の戻り値の型、関数名、パラメータリストが全く同じ)親クラスの仮想関数)、サブクラスと呼ばれます。親クラスの仮想関数は、親クラスの仮想関数をオーバーライドします。

2.3 多型の構成条件

継承システムでポリモーフィズムを構成するには、次の 2 つの条件を満たす必要があります。
1. 仮想関数は、親クラスのポインターまたは参照によって呼び出されなければなりません。
2. 呼び出される関数は仮想関数である必要があり、サブクラスは親クラスの仮想関数を書き換える必要があります。
仮想関数の書き換え条件:
仮想関数の書き換え条件は本来「仮想関数+3つ同一」と定められていますが、一部例外があります。
1. 親クラスと子クラスの関数は同時に仮想関数である必要がある 原則として virtual キーワードを追加する必要がある 仮想関数とみなされるには、親クラスの関数の前に virtual キーワードを追加する必要がある, しかし、子クラスの仮想関数は仮想化できない、その理由は、サブクラスが親クラスのメンバ変数やメンバ関数を継承しており、親クラスの関数が仮想関数であるためであると考えられます。サブクラスのこの関数も仮想関数ですが、親クラスと子クラスの関数に仮想を追加することをお勧めします。
2. 原則として、3 つの同一性が満たされなければなりません: 関数名、パラメータ (まったく同じ)、戻り値が同じである必要がありますが、共分散は例外で、共分散では戻り値が異なっていてもよいと規定されていますが、同じである必要があります。親子関係のポインタまたは参照。両方ともポインタまたは参照でなければなりません。一方がポインタを返し、もう一方が参照を返すことはできません。

1. ポリモーフィズムを構成しない

//不构成多态
class Person
{
    
    
public:
	void func()
	{
    
    
		cout << "Person::func()" << endl;
	}
};
class Student: public Person
{
    
    
public:
	virtual void func()
	{
    
    
		cout << "Student::func()" << endl;
	}
};

int main()
{
    
    
	//父类的引用调用函数,父类的引用引用父类对象
	Person p;
	Person& rp = p;
	p.func();

	//父类的指针调用函数,父类的指针指向父类对象
	Person* pp = &p;
	pp->func();
	
	//父类的引用调用函数,父类的引用引用子类对象
	Student s;
	Person& rs = s;
	rs.func();

	//父类的指针调用函数,父类的指针指向子类对象
	Person* sp = &s;
	sp->func();

	return 0;
}

ここに画像の説明を挿入

第二に、ポリモーフィズムの構成

//构成多态
class Person
{
    
    
public:
	virtual void func()
	{
    
    
		cout << "Person::func()" << endl;
	}
};
class Student: public Person
{
    
    
public:
	//虚函数的重写
	virtual void func()
	{
    
    
		cout << "Student::func()" << endl;
	}
};

int main()
{
    
    
	//父类的引用调用函数,父类的引用引用父类对象
	Person p;
	Person& rp = p;
	p.func();

	//父类的指针调用函数,父类的指针指向父类对象
	Person* pp = &p;
	pp->func();
	
	//父类的引用调用函数,父类的引用引用子类对象
	Student s;
	Person& rs = s;
	rs.func();

	//父类的指针调用函数,父类的指针指向子类对象
	Person* sp = &s;
	sp->func();

	return 0;
}

ここに画像の説明を挿入

デストラクタの書き換えには特に注意が必要です(基底クラスと派生クラスのデストラクタの関数名が異なります) 基底クラスのデストラクタが仮想関数の場合、派生クラスのデストラクタ
のみ仮想キーの追加の有無に関わらず、この時点で定義する必要がある単語は、基底クラスと派生クラスのデストラクター名は異なりますが、基底クラスのデストラクターで書き換えられます。関数名が異なるため、書き換え規則に違反しているように見えますが、実際にはコンパイラがデストラクタ名を特別に処理しています。コンパイル後、デストラクタ名は一律にデストラクタとして処理されるため、親のデストラクタはと子クラス 関数の関数名は同じであり、パラメータも戻り値もないため、書き換えとなります。
デストラクターは仮想である必要がありますか? 必要。
なぜ?その理由は次のとおりです。

デストラクターが仮想関数でない場合、それはポリモーフィズムを構成しません。


class Person
{
    
    
public:
	//析构函数不构成多态
	/*virtual */~Person()
	{
    
    
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
    
    
public:
	~Student()
	{
    
    
		delete p;
		p = nullptr;
		cout << "~Student()" << endl;
	}

public:
	int* p = new int[100];
};

int main()
{
    
    
	Person* pp = new Person;
	//等价于pp->destructor(),不构成多态,
	//调用函数的时候按照pp的类型调用
	delete pp;

	Person* sp = new Student;
	//等价于sp->destructor(),不构成多态,
	//调用函数的时候按照sp的类型调用
	delete sp;

	return 0;
}

ここに画像の説明を挿入

デストラクターは仮想関数であり、ポリモーフィズムを構成します。

class Person
{
    
    
public:
	//析构函数构成多态
	virtual ~Person()
	{
    
    
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
    
    
public:
	~Student()
	{
    
    
		delete p;
		p = nullptr;
		cout << "~Student()" << endl;
	}

public:
	int* p = new int[100];
};

int main()
{
    
    
	Person* pp = new Person;
	//等价于pp->destructor(),构成多态,
	//调用函数的时候按照pp指向的对象调用
	delete pp;

	Person* sp = new Student;
	//等价于sp->destructor(),构成多态,
	//调用函数的时候按照sp的指向的对象调用
	delete sp;

	return 0;
}

ここに画像の説明を挿入
このようにして、デストラクターを正しく呼び出してリソースを解放できます。

概要: ポリモーフィズムとは、さまざまなオブジェクトがメンバー関数に渡されることであり、さまざまな関数が呼び出されます。ポリモーフィックな場合は、関数を呼び出すときにポインターまたは参照が指すオブジェクトを調べます。ポリモーフィズムを構成しない場合は、関数を呼び出すときにポインターまたは参照自体の型のみを確認してください。

C++11 の 2.4 オーバーライドとファイナル

上記のことから、C++ では関数の書き換えに対する要件が比較的厳しいことがわかりますが、場合によっては、過失により関数名が間違っていたり、パラメーターの戻り値が間違っていたりして書き換えが発生する可能性があり、このエラーはコンパイル中に発生します。これは報告されず、プログラムの実行中に期待した結果が得られない場合にのみデバッグする価値はありません。そのため、C++11 には、override と Final という 2 つのキーワードが用意されており、ユーザーがこれを検出するのに役立ちます。リライト。

1. オーバーライドは、サブクラスの仮想関数を変更するために使用されます。サブクラスの仮想関数が基本クラスの仮想関数をオーバーライドしたかどうかを確認するために使用されます。オーバーライドされていない場合は、エラーが報告されます。
ここに画像の説明を挿入
2. Final は仮想関数を変更するために使用され、仮想関数が書き換えられなくなったことを示します。
ここに画像の説明を挿入

2.5 書き換え(オーバーライド)、オーバーロード、再定義(隠蔽)の比較

ここに画像の説明を挿入

3、ポリモーフィズムの原理

3.1 仮想機能テーブル

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
    
    
public:
	virtual void Func1()
	{
    
    
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
int main()
{
    
    
	cout << sizeof(Base) << endl;

	return 0;
}

答えは8ですが、なぜですか?明らかに、整数メンバー変数は 1 つだけですが、それは 4 であるべきではないでしょうか。Base クラスには仮想関数があるため、仮想関数テーブル ポインターがメンバー変数に追加され、ポイントされた仮想関数テーブルにはすべての仮想関数のアドレスが格納されます。
ここに画像の説明を挿入
観察とテストを通じて、b オブジェクトは 8 バイトであることがわかりました。_b メンバーに加えて、オブジェクトの前にもう 1 つの _vfptr が配置されています (プラットフォームによってはオブジェクトの最後に配置される場合があることに注意してください。これはプラットフォームに関連する)、オブジェクト内のこれ ポインタは仮想関数テーブル ポインタと呼ばれます (v は仮想、f は関数を表します)。仮想関数を含むクラスには、少なくとも 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:
	//子类重写Func1
	virtual void Func1()
	{
    
    
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
    
    
	Base b;
	Derive d;

	return 0;
}

ここに画像の説明を挿入

  1. 派生クラス オブジェクト d にも仮想テーブル ポインターがあり、d オブジェクトは 2 つの部分で構成され、1 つの部分は親クラスから継承されたメンバーであり、もう 1 つの部分は独自のメンバーです。
  2. 基底クラス b のオブジェクトと派生クラス d のオブジェクトの仮想テーブルは異なりますが、ここでは Func1 が書き換えられていることがわかり、書き換えられた Derive::Func1 は d の仮想テーブルに格納されるので、仮想関数の書き換えは完了です。カバレッジとも呼ばれます。カバレッジの場合、カバレッジは仮想テーブル内の仮想関数のカバレッジを指します。書き換えは構文層の名前であり、カバレッジは原則層の名前です。
  3. また、Func2は仮想関数として継承されているので仮想テーブルに入れられ、Func3も継承されていますが仮想関数ではないので仮想テーブルには入れられません。
  4. 仮想関数テーブルの本質は、仮想関数ポインタを格納するポインタの配列であり、通常、この配列の最後には nullptr が配置されます。
  5. 派生クラスの仮想テーブルの生成を要約します: a. まず、基本クラスの仮想テーブルの内容を派生クラスの仮想テーブルにコピーします b. 派生クラスが基本クラスの仮想関数を書き換える場合は、派生クラスを使用します。クラス自身の仮想関数は、仮想テーブルの基本クラスの仮想関数をオーバーライドします c. 新しく追加された派生クラスの仮想関数は、派生クラスの宣言順序に従って、派生クラスの仮想テーブルの末尾に追加されます。

仮想テーブルには、仮想関数ではなく仮想関数ポインタが格納されます。仮想関数は通常の関数と同じで、すべてコードセグメント内に存在しますが、そのポインタは仮想テーブルに格納されます。なお、オブジェクトに格納されるのは仮想テーブルではなく、仮想テーブルポインタである。では、仮想テーブルはどこに存在するのでしょうか? 次に、コードを使用してそれを検証します。
ここに画像の説明を挿入
上記のことから、仮想テーブルが定数領域に格納されていることが推測できます。

3.2 多態性条件の再検討

1. 親クラスのポインタまたは参照が仮想関数を呼び出す
質問 1: サブクラスのポインタまたは参照が仮想関数を呼び出せないのはなぜですか?
親クラスのポインタまたは参照は、親クラスのポインタまたは参照と、サブクラスのポインタまたは参照(スライス)の両方を受け取ることができるためです。
質問 2: 親クラス オブジェクトが仮想関数を呼び出せないのはなぜですか?
説明は次のとおりです。
ここに画像の説明を挿入
2. 仮想関数の書き換え
なぜサブクラスは親クラスの仮想関数を書き換えてポリモーフィズムを形成する必要があるのでしょうか。
書き換えが完了して初めて、サブクラスの仮想関数テーブルの対応する関数アドレスが、サブクラスの書き換え後の仮想関数のアドレスで上書きされるため、指定されたオブジェクトに応じて、対応する仮想関数を呼び出すことができます。呼び出し時に親クラスのポインタによって親クラスのオブジェクトをポイントすると、親クラスの仮想関数テーブルに移動して仮想関数ポインタを見つけて関数を呼び出し、サブクラスのオブジェクトをポイントすると、サブクラスの仮想関数に移動しますtable を使用して仮想関数ポインターを見つけて関数を呼び出すことで、ポリモーフィズムを完了できます。

ここに画像の説明を挿入

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

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

3.4 単一継承および多重継承関係の仮想関数テーブル

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

vtable を印刷します:

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;
};

//函数指针
typedef void (*FUNC_PTR) ();

void PrintVFT(FUNC_PTR table[])
{
    
    
	//虚表是以nullptr为结束标志的,所以
	// 可以借助这个来当循环的结束条件
	for (int i = 0; table[i] != nullptr; i++)
	{
    
    
		printf("[%d]->%p\n", i, table[i]);
	}
	printf("\n");
}

int main()
{
    
    
	Base b;
	Derive d;
	int ptr = *(int*)&d;
	PrintVFT((FUNC_PTR*)ptr);

	return 0;
}

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

3.4.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 (*FUNC_PTR) ();

void PrintVFT(FUNC_PTR table[])
{
    
    
	//虚表是以nullptr为结束标志的,所以
	// 可以借助这个来当循环的结束条件
	for (int i = 0; table[i] != nullptr; i++)
	{
    
    
		printf("[%d]:%p->", i, table[i]);
		FUNC_PTR func = table[i];
		func();
	}
	printf("\n");
}

int main()
{
    
    
	Derive d;
	cout << sizeof(d) << endl;

	int vft1 = *((int*)&d);
	Base2* ptr = &d;
	int vft2 = *((int*)ptr);

	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}

以下の図を観察すると、多重継承派生クラスのオーバーライドされていない仮想関数が、最初に継承された基本クラス部分の仮想関数テーブルに配置されていることがわかります。
ここに画像の説明を挿入

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

4. 練習する

最初の質問、
ここに画像の説明を挿入

2番目の質問、
ここに画像の説明を挿入

3番目の質問、
ここに画像の説明を挿入
4番目の質問、
ここに画像の説明を挿入

5. 次の質問について考えてみましょう

  1. ポリモーフィズムとは何ですか? 回答: 静的ポリモーフィズム (関数のオーバーロード)、動的ポリモーフィズム (継承における仮想関数の書き換え + 親クラス ポインターの呼び出し)。さまざまな形式で呼び出すことがより便利で柔軟になります。
  2. オーバーロード、書き換え(カバー)、再定義(隠蔽)とは何ですか? 回答: 記事の内容を参照してください。
  3. ポリモーフィズムの実現原理は?回答: 静的ポリモーフィズム (関数名変更規則)、動的ポリモーフィズム (仮想関数テーブル)。
  4. インライン関数を仮想関数にすることはできますか? 回答: はい。ただし、コンパイラは自動的に inline 属性を無視します。また、仮想関数は仮想テーブルに配置する必要があり、インライン関数はコンパイル時に展開され、インライン関数は存在しないため、この関数はインライン関数ではなくなります。したがって、仮想関数アドレスを仮想テーブルに配置する場合は、関数アドレスが必要です。
  5. 静的メンバーを仮想関数にすることはできますか? 回答: いいえ、静的メンバー関数には this ポインターがなく、type:: メンバー関数を呼び出して仮想関数テーブルにアクセスできないため、静的メンバー関数を仮想関数テーブルに配置できません。
  6. コンストラクターは仮想にできますか? 回答: いいえ、オブジェクト内の仮想関数テーブル ポインターはコンストラクターの初期化リストの段階で初期化されるためです。
  7. デストラクターは仮想化できますか? どのような状況でデストラクターは仮想関数になりますか? 回答: はい。基本クラスのデストラクターを仮想関数として定義するのが最善です。記事の内容を参照してください。
  8. 通常の関数へのオブジェクト アクセスは高速ですか?それとも仮想関数へのアクセスは高速ですか? 回答: 通常のオブジェクトであれば、同じ速度です。ポインタオブジェクトまたは参照オブジェクトの場合、多態性が形成されると実行時に呼び出されるときに仮想関数テーブル内で仮想関数を検索する必要があるため、通常の関数の呼び出しは高速です。
  9. 仮想関数テーブルはどの段階で生成され、どこに存在しますか? 回答: 仮想関数テーブルはコンパイル段階で生成され、通常はコードセグメント (定数領域) に存在します。(記事検証内容参照)
  10. C++ ダイヤモンド継承に問題がありますか? 仮想継承の原理? 回答: データの冗長性とあいまいさ (継承に関する前回の記事を参照)。仮想関数テーブルと仮想ベーステーブルを混同しないように注意してください。

以上が今日皆さんにお伝えしたい内容ですが、学習できましたか?ポリモーフィズムは非常に重要な内容であり、実務でもよく使われるので必ずマスターしてください。役に立ったと感じたら、いいね、クリック、フォローをお願いします。今後も C++ 関連の知識を更新していきます。次回お会いしましょう!

おすすめ

転載: blog.csdn.net/weixin_70056514/article/details/131901298