C++学習記録 - ポリモーフィズム


1. 理解する

ポリモーフィズムはさまざまな形式であり、オブジェクトが異なれば、特定の動作を完了したときの状態や結果も異なります。

関数タイプの前に virtual を追加して、これが仮想関数であることを示します

class Person
{
    
    
public:
	virtual void Buy() {
    
     cout << "买票-全价" << endl; }
};

class Student : public Person
{
    
    
public:
	//发生了重写/覆盖
	virtual void Buy() {
    
     cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
    
    
	p.Buy();
}

int main()
{
    
    
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

結果は全額と半額となり、上書きは発生しますが最終結果には影響せず、渡されたクラスのオブジェクトが出力されます。

ポリモーフィズムの条件は、仮想関数と呼び出す親クラスのポインタまたは参照の書き換えです。

先ほどのコードの場合、参照を削除するか書き直すか、あるいはその両方を行うと、2 つの正規価格が適用されます。

そんなシーン

class Person {
    
    
public:
	~Person() {
    
     cout << "~Person()" << endl; }
};

class Student : public Person {
    
    
public:
	~Student() {
    
     cout << "~Student()" << endl; }
};

int main()
{
    
    
	Person* ptr1 = new Person;
	Person* ptr2 = new Student;
	delete ptr1;
	delete ptr2;
	return 0;
}

ここに画像の説明を挿入

ptr1 が解放されると親クラスのデストラクタが呼び出されますが、ptr2 も親クラスのデストラクタを呼び出しますが、これは隠れた関係があり、このときにポリモーフィズムが役立つためです。

2. 多態性条件

仮想関数の書き換え - 関数名、パラメータ、戻り値はすべて同じであり、
親クラスのポインタまたは参照を使用して呼び出します

ポリモーフィズムが満たされない場合は、呼び出し元の型に依存し、この型のメンバー関数を呼び出します。 ポリモーフィズムが満たされる場合は、
ポイントされたオブジェクトの型に依存し、この型のメンバー関数を呼び出します

class Person
{
    
    
public:
	void Buy() {
    
     cout << "买票-全价" << endl; }
};

class Student : public Person
{
    
    
public:
	virtual void Buy() {
    
     cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
    
    
	p.Buy();
}

このように、親クラスは virtal を持たず、ポリモーフィックではなく、hidden ですが、サブクラス オブジェクトが呼び出された場合にのみ、hidden を反映できます。

	Student st;
	st.Buy();

参照を使用する場合、外部にオブジェクトを作成してそこに Func(st) を渡すことはできますが、ポインターは使用できません。Student 型を Person* 型に変換できないことが表示されるため、Func(new学生)は必須です。

親クラスの関数に virtual があるが、子クラスにない場合は、全額価格と半額が出力されますが、ここでもポリモーフィズムが存在します。

サブクラスを記述する必要はありませんが、親クラスがサブクラスを記述すると、コンパイラはサブクラスがこの仮想関数を書き換えたとみなします。インターフェースの継承, サブクラスは親クラスの機能を継承しますが、内部の実装はサブクラスに書き換えられます。

1. デストラクタの書き換え

class Person
{
    
    
public:
    virtual void Buy() {
    
     cout << "买票-全价" << endl; }

    ~Person()
    {
    
    
        cout << "~Person()" << endl;
    }
};

class Student : public Person
{
    
    
public:
    //发生了重写/覆盖
    void Buy() {
    
     cout << "买票-半价" << endl; }

    ~Student()
    {
    
    
        cout << "~Student()" << endl;
    }
};

void Func(Person* p)
{
    
    
    p->Buy();
    delete p;
}

int main()
{
    
    
    Func(new Person);
    Func(new Student);
    return 0;
}

ここに画像の説明を挿入
delete には 2 つの部分があり、デストラクター部分を呼び出してから、operator delete を呼び出します。デストラクターは destructot (ptr1->destructotctor()) に処理されるため、2 つのオブジェクトは隠しオブジェクトを形成し、両方とも親クラスです。 pointers を指すので、上記に従って、親クラスを呼び出します。

現在、デストラクターは多態性ではないため、別の条件を追加する必要があります。また、親クラスのデストラクターを仮想として記述して多態性を形成することもできます。仮想〜人。

ここに画像の説明を挿入

サブクラスが終了すると、サブクラスのデストラクタが呼び出され、最後に親クラスのポインタが破棄されるため、最後に ~person() が存在します。

2. 共変動

仮想関数の書き換えには、関数名、パラメータ、戻り値の 3 つの類似点がありますが、戻り値が異なるという 1 つの例外があります。しかし、それが異なることはできません。親子関係へのポインターまたは参照が必要です。

class Person
{
    
    
public:
	virtual Person* Buy()
	{
    
    
		cout << "买票-全价" << endl;
		return this;
	}

};

class Student : public Person
{
    
    
public:
	virtual Student* Buy()
	{
    
    
		cout << "买票-半价" << endl;
		return this;
	}
};

このとき、一方は全額を印刷し、もう一方は半額を印刷します。既存のクラスへのポインター以上のものにすることができます。たとえば、親クラスとその前に B のサブクラスを定義した場合、A と B へのポインターを使用できます。戻り値の型も対応している必要がありますが、戻りクラス オブジェクトは対応していません。

仮想関数の書き換えには 2 つの例外があり、1 つはインターフェイスの継承で、もう 1 つは共分散です。インターフェイスの継承とは、親クラスが virtual を書くことを意味し、サブクラスは virtual を書かずにポリモーフィズムを構成することもできます。

3. キーワード「final」と「override」

最後の

仮想関数を変更した後は、仮想関数を書き換えることはできません

class Car
{
    
    
public:
	virtual void Drive() final {
    
    }
};

class Benz : public Car
{
    
    
public:
	virtual void Drive() {
    
     cout << "Benz-舒适" << endl; }
};

int main()
{
    
    
	return 0;
}

オーバーライド

書き換えが完了したかどうかを確認し、完了していない場合はエラーを報告します

class Car
{
    
    
public:
	virtual void Drive() {
    
    }
};

class Benz : public Car
{
    
    
public:
	virtual void Drive() override {
    
     cout << "Benz-舒适" << endl; }
};

int main()
{
    
    
	return 0;
}

4. リライト(上書き)、オーバーロード、非表示(再定義)の比較

オーバーロード: 2 つの関数が同じスコープ内にあり、関数名は同じですが、パラメーターが異なります。

オーバーライド: 2 つの関数はそれぞれ基本クラスと派生クラスのスコープ内にあります。関数名/パラメーター/戻り値は同じである必要があります (共分散を除く)。2 つの関数は仮想関数である必要があります

再定義: 2 つの関数がそれぞれ基本クラスと派生クラスのスコープ内にあり、関数名は同じです。基本クラスと派生クラスで同じ名前を持つ 2 つの関数は、書き換えまたは再定義を構成しません。

3. 抽象クラス

仮想関数の後に =0 を書き込みます。この関数は純粋仮想関数であり、純粋仮想関数を含むクラスは抽象クラスであり、このクラスはオブジェクトをインスタンス化できません。

ここに画像の説明を挿入

ここに画像の説明を挿入

抽象クラスを継承するクラスも、純粋仮想関数を含むため、抽象クラスです。純粋仮想関数が書き換えられた場合、サブクラスの関数は純粋仮想関数ではなくなり、この時点でサブクラスはオブジェクトをインスタンス化できます。

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

1. 仮想機能テーブル

class Base
{
    
    
public:
	virtual void Func1()
	{
    
    
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
	char _ch;
};

int main()
{
    
    
	cout << sizeof(Base) << endl;
	Base bb;
	return 0;
}

32 ビット未満では、結果は 12 になります。メモリのアライメントによれば、2 つの変数が 8 バイトを占有するので、余分な 4 バイトは何でしょうか?

ここに画像の説明を挿入

追加のポインタは仮想関数テーブル ポインタです。

ここに画像の説明を挿入

2. 原則

class Person {
    
    
public:
	virtual void BuyTicket() {
    
     cout << "买票-全价" << endl; }
};

class Student : public Person {
    
    
public:
	virtual void BuyTicket() {
    
     cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
    
    
	p.BuyTicket();
}

int main()
{
    
    
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

ここに画像の説明を挿入

func関数では、親クラスのオブジェクトを渡せば親クラスの仮想関数が、サブクラスのオブジェクトを渡せばサブクラスの仮想関数が存在します。親クラス仮想テーブルには親クラス仮想関数が格納され、子クラス仮想テーブルには子クラス仮想関数が格納される。多態性を構成しない場合は、呼び出されたクラスの仮想関数を使用します。多態性である場合は、ポイントされたオブジェクトの仮想テーブルに移動してそれを見つけます。したがって、p ポインタは仮想テーブルのみを参照し、それがどのクラス オブジェクトであるかを知りません。

仮想関数テーブルがクラス オブジェクト内に配置されるだけでなく、仮想関数テーブル用の別の領域に配置されるのはなぜですか? 複数の仮想関数が存在する可能性があるためです。仮想関数テーブルは本質的には仮想関数ポインタの配列であり、複数の仮想関数がある場合は、複数の仮想関数テーブルが存在します。書き換えが発生した場合は仮想関数テーブルが上書きされ、書き換えが発生しなかった場合は上書きされません。仮想関数テーブルはコンパイル時に準備されます。仮想関数テーブルは、配列の添字を宣言順に決定します。

親クラスのポインターまたは参照は使用できるのに、インスタンス化されたオブジェクトはポリモーフィズムを形成できないのはなぜですか?

親クラスのポインターまたは参照は仮想関数テーブルを指すことができ、サブクラスは親クラスの一部を切り取って、サブクラスの仮想関数テーブルに対応するオブジェクトを指すようにします。オブジェクトであれば親クラスのオブジェクトで良いのですが、サブクラスのオブジェクトだとコピーが発生しますが、親クラスの内容をサブクラスにコピーした場合、仮想テーブルはどうなるでしょうか?いいえ、このリスクは高く、コピー後に多くのマッピング関係が台無しになる可能性があります。

-------------------------------------------------- -------------------------------------------------- -----------

通常の関数と同様に、仮想関数にはコード セグメントがあります。

メイン スタック フレームでは、クラスがオブジェクトをインスタンス化し、オブジェクトは仮想関数のアドレスを格納する仮想関数テーブルへのポインターを持ちます。

3. 印刷仮想テーブル

class Base {
    
    
public:
	virtual void func1() {
    
     cout << "Base::func1" << endl; }
	virtual void func2() {
    
     cout << "Base::func2" << endl; }
	void func3() {
    
     cout << "Derive::func3" << endl; }
private:
	int _b = 1;
};

class Derive :public Base {
    
    
public:
	virtual void func1() {
    
     cout << "Derive::func1" << endl; }
	virtual void func4() {
    
     cout << "Derive::func4" << endl; }
private:
	int _d = 2;
};

int main()
{
    
    
    Base b;
    Derive d;
    return 0;
}

親クラスに存在しない関数をサブクラスに書いて、その前にvirtualを追加した場合、この時に監視ウィンドウを呼び出すと仮想テーブルにないことが分かるのですが、本当にないのでしょうか?そこには?メモリ ウィンドウでは表示できない場合があるため、表示するには仮想テーブルを印刷する必要があります。

typedef void(*VF_PTR)();//函数指针的typedef需要把新定义的名字放在括号里,所以VF_PTR是void(*)()
void PrintVFTable(VF_PTR table[])
{
    
    
	for (int i = 0; table[i] != nullptr; ++i)
	{
    
    
		printf("[%d]:%p\n", i, table[i]);
	}
	cout << endl;
}

int main()
{
    
    
	Base b;
	Derive d;
	//要找到这个虚表,根据之前所写,虚表指针在这个对象的前4/8个字节
	PrintVFTable((VF_PTR*)(*(int*)&b));
	PrintVFTable((VF_PTR*)(*(int*)&d));
	return 0;
}

クラス オブジェクトのアドレスを int* 型に強制し、それを逆参照してアドレスの最初の 4 バイトを取得し、この結果を VF_PTR 型に変換して渡すことができます。

ここに画像の説明を挿入

別の書き方はセカンダリポインタです

	PrintVFTable((*(VF_PTR**)&b));
	PrintVFTable((*(VF_PTR**)&d));

ただし、最初のタイプは 32 ビット未満のみに対応し、2 番目のタイプは両方を実行できます。最初のタイプの int は、64 ビットに適応するためにlonglong に変更できます。

補完・印刷仮想テーブル機能

void PrintVFTable(VF_PTR* table)
{
    
    
	for (int i = 0; table[i] != nullptr; ++i)
	{
    
    
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}

2 つのオブジェクトの仮想テーブルを印刷できます。func4 もその中にあります。

仮想テーブルはコンパイル フェーズ中に生成されます。オブジェクト内の仮想テーブル ポインターはコンストラクターの初期化リストで初期化されます。仮想テーブルはどこに存在しますか?

このように見ることができます。

	int x = 0;
	static int y = 0;//静态区
	int* z = new int;
	const char* p = "asdasd asda";//常量区
	printf("栈对象: %p\n", &x);
	printf("堆对象: %p\n", z);
	printf("静态区对象: %p\n", &y);
	printf("常量区对象: %p\n", p);
	printf("b对象虚表: %p\n", *((int*)&b));
	printf("d对象虚表: %p\n", *((int*)&b));

ここに画像の説明を挿入

定数領域に近いので定数領域に置くべきですが、静的領域に置くコンパイラもあります。

4. 多重継承

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

d には 2 つの仮想テーブル (base1 とbase2) があり、func1 は書き換えられており、両方の仮想テーブルには独自の func2 があります。func3 はどこにありますか? 仮想テーブルを印刷できますが、仮想テーブルが 2 つあり、base1 が前に配置されているため、前の方法によれば、base1 のみが印刷され、2 は印刷されず、base2 は +sizeof(Base1) またはで印刷できます。オフセット。

	//PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));
	Base2* ptr2 = &d;//用Base2类的指针指向Derive的对象,指针就会指向d中Base2的部分,自然而然就发生了偏移
	PrintVFTable((VF_PTR*)(*(int*)(ptr2)));

最終結果は最初の仮想テーブルに配置されます。

上記のコードでは、func1 が書き換えられていますが、アドレスは同じではありませんが、確かに書き換えられています。

	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
	ptr1->func1();
	ptr2->func1();

逆アセンブルされたコードでは、ptr1->func1() は Base1 の仮想テーブル アドレスを呼び出します。呼び出しは jmp 命令であり、jmp の後の括弧は関数の実際のアドレスであり、スタック フレームの構築を開始します。 etc; ptr2->func1() 呼び出しのアドレスが異なり、jmp 以降の関数のアドレスも異なり、この jmp の後に次の逆アセンブルでは sub の文、jmp の文、および次に、実際に実行される関数のアドレスに到達する前に jmp の文が続きます。このようになっている理由は、その中に sub があり、そのコード行の後に ecx があるためです。ecx は this ポインタです。ptr1 によって呼び出される func はサブクラスの関数であり、ptr1 は先頭を指します。オブジェクトの先頭を指し、ecx はオブジェクトの先頭を指しているため、追加の処理は必要ありません。ただし、ptr2 はそうではありません。現時点では、これはオブジェクトの先頭を指していないため、追加の手順は次のとおりです。このポインタを修正してください。

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

  1. 静的バインディングは、アーリー バインディング (早期バインディング) とも呼ばれ、関数のオーバーロード、cin、
    cout などの静的多態性とも呼ばれる、プログラムのコンパイル中のプログラムの動作を決定します。
  2. 動的バインディングは、遅延バインディング (遅延バインディング) とも呼ばれ、プログラムの実行中に取得された特定の型に従ってプログラムの特定の動作を決定し、特定の関数を呼び出すことであり、動的ポリモーフィズムとも呼ばれます
  3. チケットを購入する前の Buy() のアセンブリ コードは、静的 (コンパイラー) バインディングと動的 (ランタイム) バインディングが何であるかをよく説明しています

6. その他

class A
{
    
    
public:
    virtual void func()
    {
    
    }
public:
    int _a;
};

class B : virtual public A
{
    
    
public:
    virtual void func()
    {
    
    }
public:
    int _b;
};

class C : virtual public A
{
    
    
public:
    virtual void func()
    {
    
    }
public:
    int _c;
};

class D : public B, public C
{
    
    
public:
    int _d;
};

int main()
{
    
    
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
    return 0;
}

ABCには関数があり、Dが書かないと、Aのfunc関数の書き換えが明確ではないという警告が表示されます。Aの仮想テーブルにはBかCで書き換えた関数を置くべきでしょうか?よくわかりませんが、現時点では最終的な書き換えを完了するには D が必要です。初期化についても同様です。コードは D で記述され、BCA が A を初期化しますが、A は独自の初期化を使用する必要があります。A が独自の初期化を記述しない場合、エラーが報告されます。

BC に新しい仮想関数がある場合、それは A の仮想テーブルには入れられません。A を確認しない場合、D は BC を継承し、D には B と C に属する 2 つの仮想テーブルがあるはずです。次に A を追加すると、A のパブリック仮想テーブルが作成されます。

ここに画像の説明を挿入

D には、A の仮想テーブル、フルネーム仮想関数テーブル (関数ポインタを格納する仮想関数を指すテーブル)、および B と C の 2 つの仮想ベース テーブル (オフセット付き) を含む 3 つの仮想テーブルがあります。e4 f4 b4 は仮想テーブル ポインタ、38 と ac は仮想実テーブル ポインタです。

5. まとめ

1. ポリモーフィズムは動的ポリモーフィズムと静的ポリモーフィズムに分けられ、静的ポリモーフィズムはコンパイル時のポリモーフィズム、関数のオーバーロードやテンプレートによって実現されるポリモーフィズム、動的ポリモーフィズムは仮想関数によって実現される実行時のポリモーフィズムです。

2. インライン関数を仮想関数にすることはできません。インライン関数にはアドレスがありません。これは、リンクする必要がなく、呼び出しサイトで直接展開され、個別に宣言および定義することができないためです。したがって、仮想関数のアドレスは仮想テーブルに配置する必要があるため、仮想関数にすることはできません。しかし、実際には、このように inline virtual を書いても合格する可能性があります。インライン化は、インライン + 仮想関数というコンパイラーへの単なる提案であり、ポリモーフィズムに準拠しているため、ポリモーフィズムに従い、コンパイラーは inline 属性を無視します。それ以外の場合は、まだインラインです。

3. 静的関数を仮想関数にすることはできません。仮想関数はオブジェクトを通じて呼び出されますが、静的関数にはそのような制限は必要なく、誰でも呼び出すことができ、this ポインターもありません。type::member 関数を使用して静的メンバーを呼び出すことはできますが、この方法で仮想関数テーブルにアクセスすることはできません。静的と仮想を一緒にすると、直接エラーが報告されます。

4. コンストラクターを仮想関数にすることはできません。仮想関数のアドレスは仮想テーブル内にあり、リストの初期化時に仮想テーブル ポインターが生成されるため、論理エラーが発生し、コードが正常に実行できなくなります。サブクラスは親クラスのコンストラクターを明示的に呼び出す必要があり、親クラスの構築を妨げることはできないため、コンストラクターは仮想関数になることはできません。

5. コピー構築と代入は仮想関数にはできない コピー構築と代入の理由は似ており、代入関数は仮想的に書くことができますが、代入関数を書き換えると演算子が変更したい変数に影響を及ぼします。

6. デストラクターは仮想関数として記述することをお勧めします。

7. 通常のオブジェクトの場合、通常の関数と仮想関数は同じ速度ですが、ポインタ オブジェクトまたは参照オブジェクトの場合は、実行時に仮想関数の呼び出しを仮想関数テーブルで見つける必要があるため、ポリモーフィズムが形成され、通常の関数の方が高速になります。 。

8. 仮想関数テーブルはコンパイル段階で生成され、通常はコードセグメント(定数領域)または静的領域が存在します。

9. 仮想関数テーブルはポリモーフィズムであり、仮想ベース テーブルは多重継承を解決するためのものであり、仮想関数テーブルは仮想関数のアドレスを持ち、仮想ベース テーブルはデータの冗長性と曖昧さを解決するためのオフセットを持ちます。

仕上げる。

おすすめ

転載: blog.csdn.net/kongqizyd146/article/details/129824998