[C++ スキルツリー] ポリモーフィック解析

ここに画像の説明を挿入します
こんにちは、プペウアです。私は通常、C++、データ構造アルゴリズム、Linux、ROS を更新しています...興味がある場合は、フォローしてください bua!

ここに画像の説明を挿入します

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

このシナリオを想像してください。異なるアイデンティティを持つ人々がチケットを購入します。同じ関数が異なる動作を実行します。これを完了するにはポリモーフィズムが必要です。

ポリモーフィズム: 名前が示すように、クラス内の関数の複数の状態。

まず次の例を見てみましょう。

class Person{
    
    
public:
    virtual void BuyTicket()
    {
    
    
        cout<<"买票全价"<<endl;
    }
    virtual ~Person()
    {
    
    
        cout<<"~person()"<<endl;
    }
};
class Student:virtual public Person
{
    
    
public:
    virtual void BuyTicket()
    {
    
    
        cout<<"买票半价"<<endl;
    }
    virtual ~Student()
    {
    
    
        cout<<"~student()"<<endl;
    }
};
void buyticket(Person *p1)
{
    
    
    p1->BuyTicket();   
}

学生タイプのアドレスが buyticket に渡される場合と、人物タイプのアドレスが渡される場合、異なる動作が実行されます。

直接渡し: チケットの全額が出力されます

Pass in Student: チケットの半額が出力されます

これはポリモーフィズムの特定の動作であり、渡されたさまざまなオブジェクトに基づいてさまざまな動作が実行されます。

0.1 ポリモーフィズムの定義

基本クラスでオーバーライドする必要がある関数の前に virtual を追加します。

派生クラスで、書き換える関数の前に virtual を追加し (追加してもしなくても構いません)、基本クラスの関数と同じ戻り値、関数名、パラメーター リストを維持して書き換えを完了します。

呼び出す際は、基底クラスのポインタまたは参照を介して呼び出す必要があります(呼び出したいクラスを親クラスのポインタまたは参照に代入し、同じ関数を呼び出します。ポリモーフィズムが完了できます)

例外があります。戻り値は必ずしも同じである必要はなく、親クラスまたはサブクラス オブジェクトへのポインターまたは参照 (ポインターまたは参照の両方) にすることができます。これは共分散と呼ばれます

つまり、ポリモーフィズムとは、異なるオブジェクトが渡され、異なる関数が呼び出されるということを意味します。ポリモーフィック呼び出しでは、現在の型ではなく、指定されたオブジェクトとその特定のコンテンツが参照されます。

48410c494a8f224dac0bd406a27a6dd

1.書き換える

デストラクタは、virtual が追加されているかどうかに関係なく書き換えられます。

class Person{
    
    
public:
    virtual Person& BuyTicket()
    {
    
    
        cout<<"买票全价"<<endl;
    }
     ~Person()
    {
    
    
        cout<<"~person()"<<endl;
    }
};
class Student:virtual public Person
{
    
    
public:
    virtual Student& BuyTicket() 
    {
    
    
        cout<<"买票半价"<<endl;
    }
     ~Student()
    {
    
    
        cout<<"~student()"<<endl;
    }
};

これは、コンパイラーがコンパイル段階でデストラクターを処理し、その名前を Destructor に変更したため、これらは同じ名前の関数であるためです。

なぜこれを行う必要があるのでしょうか?

親クラスのポインタを使用してサブクラスのオブジェクトを呼び出す場合、deleteを使用する場合、ポリモーフィズムがなければ親クラスのメンバ属性のみが削除され、サブクラスは削除されません。

Person* p=new Person;
delete p;
p=new Student;
//析构错误 不多态则没有调到派生类的析构
delete p;  //p->destructor+operator delete p

2.ファイナルとオーバーライド

関数をオーバーライドしたくない場合は、関数の後にFinal を追加すると、関数がオーバーライドされているかどうかを構文からチェックします。

関数が書き換えの条件を満たしているかどうかを確認するには、関数の後に override を追加して、書き換えの条件を満たしているかどうかを確認できます。

class Person{
    
    
public:
    // virtual void BuyTicket() final
    // {
    
    
    //     cout<<"买票全价"<<endl;
    // }
    virtual Person& BuyTicket()
    {
    
    
        cout<<"买票全价"<<endl;
    }
     ~Person()
    {
    
    
        cout<<"~person()"<<endl;
    }
};
class Student:virtual public Person
{
    
    
public:

    virtual Student& BuyTicket() override
    {
    
    
        cout<<"买票半价"<<endl;
    }
     ~Student()
    {
    
    
        cout<<"~student()"<<endl;
    }
};

3.抽象クラス

ポリモーフィズムはインターフェース継承とも呼ばれ、基底クラスの関数インターフェースのみを継承し、内容を自分で書き換えるという意味で、通常の関数の継承は実装継承の一種です

次に、インターフェイスのみを提供するクラスを設計することもできます。その場合、それは抽象クラスになります。

仮想関数の最後に =0 を追加すると純粋仮想関数となり、純粋仮想関数を含むクラスを抽象クラスと呼びます。

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

抽象クラスはオブジェクトのインスタンス化には使用できず、インターフェイスを提供する基本クラスとしてのみ使用できます。

4. ポリモーフィズムにおけるメモリ分散。

上記の紹介によると、ポリモーフィズムの使用についてはすでに予備的な理解が得られています。

つまり、再定義に基づいて仮想を追加し、3 つの類似点 (同じ名前、同じパラメータ、同じ戻り値)を満たすようにします。

では、ポリモーフィズムはどのようにしてメモリに保存されるのでしょうか?

class Person{
    
    
public:
	virtual void fun()
	{
    
    
		cout << "hello person";
	}
};
class Student :public Person{
    
    
	virtual void fun()override {
    
    
		cout << "hello student";
	}
	virtual void fun3() {
    
    
		cout << "hello student3";
	}
};
int main()
{
    
    
	Person* p1;
	Student s1;
	p1 = &s1;
	p1->fun();
}

これはポリモーフィック呼び出しの例です。vs2022 を使用してメモリにどのように格納されるかを見てみましょう。

画像-20230903123709063

ご覧のとおり、vfptr (virtual fun ptr) 仮想テーブル ポインタを持っています。以前継承した仮想ベース テーブルに似ており、書き換えられた関数が格納されています。

このアドレスをメモリに入力すると、2 つのアドレスが格納されていることがわかります。

画像-20230903124449931

  1. 1枚目は書き換えられたfunのアドレス

  2. 2 番目は独自の仮想関数 fun3 のアドレスですが、これは上の図の構造モデルには見られないため、構造モデルが完全に信頼できるものではない場合があります。

    したがって、独自の仮想関数は最初の仮想テーブルの最後に直接配置されます。

  3. 3 番目は仮想テーブルの終わりを表します (これは vs2022 で表現されます)

    要約すると、インスタンス化中に基底クラスの仮想テーブルが派生クラスにコピーされ、書き換えられた関数がある場合、仮想テーブル内の元の関数のアドレスが書き換えられた関数で上書きされることがわかります。アドレス. したがって、原則レイヤーでは、カバレッジとも呼ばれます。

4.1 仮想テーブルはどこに存在しますか?

仮想テーブルはスタック領域、ヒープ領域、静的領域、定数領域のいずれに格納されていますか?

これは次の関数で確認できます

int main()
{
    
    
	Person p1;
	Student s1;
	int a = 0;
	printf("栈:%p", &a);
	cout << endl;
	int* b = new int[10];
	printf("堆:%p", b);
	cout << endl;

	const char* c = "hello world";
	printf("常量区%p", c);
	cout << endl;

	static int d = 10;
	printf("静态区%p", &d);
	cout << endl;

	printf("虚表1:%p", *((int*)&p1));
	cout << endl;
	printf("虚表2:%p", *((int*)&s1));
	cout << endl;
}

なぜこのように領域が仮想テーブルのアドレスを取得できるのでしょうか?対応するオブジェクトのアドレスを取得して int にキャストすることで、アクセス幅は最初の 4 バイトになります (ポインタのサイズが 4 バイトであるため)。つまり、This ポインタは仮想テーブルのアドレスに逆参照されます*

このコードを実行すると、仮想テーブルが定数領域に格納されていることがわかります。

画像-20230903125353911

5. ポリモーフィック呼び出しの原理

ポリモーフィズムの呼び出し方法と仮想テーブル ストレージのモデルはすでに上記で説明しましたが、次はポリモーフィズム呼び出しの例です。

class Person{
    
    
public:
	virtual void fun()
	{
    
    
		cout << "hello person";
	}
};
class Student :public Person{
    
    
	virtual void fun()override {
    
    
		cout << "hello student";
	}
};
int main()
{
    
    
	Person* p1;
	Student s1;
	p1 = &s1;
	p1->fun();
}

以前に学んだことによると、p1->fun()を呼び出すと、s1のアドレスがp1に格納されているため、ここでs1の仮想テーブル内のf1のアドレスが取得され、多態性呼び出しが完了します。

したがって、ポリモーフィズムは、実行時に実行する必要がある関数を動的に決定します。

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

  1. 静的バインディングは、アーリー バインディング (初期バインディング) とも呼ばれます。プログラムのコンパイル中にプログラムの動作を決定します。静的ポリモーフィズムとも呼ばれます。例: 関数のオーバーロード

  2. 動的バインディングは、レイトバインディング(遅延バインディング)とも呼ばれ、プログラムの実行中に、取得した特定の型に基づいてプログラムの特定の動作が決定され、特定の関数が呼び出されることで、動的多態性とも呼ばれます

6. 継承時の仮想関数テーブル

以下は、多重継承と単一継承の仮想関数テーブルの 2 つの部分に分かれます

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

上記からわかるように、定義した仮想関数は書き換えられる前に仮想テーブルに格納されないため、このストレージを調べたい場合は、関数のアドレスに手動でアクセスする必要があります。

最初に、後の呼び出しを容易にするために関数ポインターの名前を変更しましょう。

typedef void(*FUNC_PTR)();

注: ここでの関数の戻り値は void で、パラメーターは空です。名前を FUNC_PTR に変更します。

そこで、このメソッドを使用して、クラス内の仮想テーブルに格納されている関数に強制的にアクセスします。

class Person{
    
    
public:
	virtual void fun1()
	{
    
    
		cout << "Person::fun1";
	}
	virtual void fun2()
	{
    
    
		cout << "Person::fun2";
	}
	virtual void fun3()
	{
    
    
		cout << "Person::fun3" ;
	}
};
class Student :public Person{
    
    
	virtual void fun1()override {
    
    
		cout << "Student::fun1()";
	}
	

	virtual void fun3()
	{
    
    
		cout << "Student::fun3()";
	}
	virtual void fun4()
	{
    
    
		cout << "Student::fun4()";
	}
};
typedef void(*FUNC_PTR)();
void printvft(FUNC_PTR* table)
{
    
    
	for (int i = 0; table[i] != nullptr; i++)
	{
    
    
		printf("%d->%p", i, table[i]);
		FUNC_PTR f=table[i];
		f();
		cout << endl;
	} 
}
int main()
{
    
    
	Student s1;
	Person p1;
	cout << "person:" << endl;
	int vft = *((int*)&p1);
	printvft((FUNC_PTR*)vft);
	cout << "student:" << endl;
	vft = *((int*)&s1);
	printvft((FUNC_PTR*)vft);
}

vft の割り当て原理は、仮想テーブルのアドレスがオブジェクトの最初の 4 ビットであることがわかっているので、int* を使用してそれを取得し、その後、逆参照はそのアドレスであり、int に格納されます。このとき、vftはストレージアドレスです

操作結果:

画像-20230905161534648

上記の関数では、fun1 と fun3 のみが Student によって書き換えられています。

したがって、次のような結論を導き出すことができます。

単一継承モデルでは、派生クラスは基本クラスの仮想テーブルのコピーをそれ自体にコピーします。オーバーライドされた関数がある場合は、基本クラスの仮想テーブルを直接変更するのではなく、元の関数のアドレスを新しいオーバーライドされた関数のアドレスで上書きします。 table . 、独自の新しい仮想関数が続きます

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

class Person{
    
    
public:
	virtual void fun1()
	{
    
    
		cout << "Person::fun1";
	}
	virtual void fun2()
	{
    
    
		cout << "Person::fun2";
	}
	virtual void fun3()
	{
    
    
		cout << "Person::fun3" ;
	}
};
class People {
    
    
public:
	virtual void fun1()
	{
    
    
		cout << "People::fun1";
	}
	virtual void fun2()
	{
    
    
		cout << "People::fun2";
	}
	virtual void fun3()
	{
    
    
		cout << "People::fun3";
	}
};

class Student :public Person,public People
{
    
    
	virtual void fun1() {
    
    
		cout << "Student::fun1()";
	}
	
	virtual void fun4()
	{
    
    
		cout << "Student::fun4()";
	}
};
typedef void(*FUNC_PTR)();
void printvft(FUNC_PTR* table)
{
    
    
	for (int i = 0; table[i] != nullptr; i++)
	{
    
    
		printf("%d->%p", i, table[i]);
		FUNC_PTR f=table[i];
		f();
		cout << endl;
	} 
}
int main()
{
    
    
	Student s1;
	Person p1;
	People peo;
	cout << "person:" << endl;
	int vft = *((int*)&p1);
	printvft((FUNC_PTR*)vft);
	cout << "people:" << endl;
	vft = *((int*)&peo);
	printvft((FUNC_PTR*)vft);
	cout << "student:" << endl;
	vft = *((int*)&s1);
	printvft((FUNC_PTR*)vft);
}

操作結果:

画像-20230905163221242

メモリ内のモデルを見てみましょう。

学生の中の人:

画像-20230905163302648

学生の人々:

画像-20230905163312561

学生が person を書き換えて、書き換えられていない仮想関数を最初の仮想テーブルの最後に配置したことが簡単にわかります

つまり、多重継承モデルでは、派生クラスは最初に継承された基本クラスを書き換えて、書き換えられていない独自の仮想関数をテーブルの最後に置きます。
画像-20230905164632777

おすすめ

転載: blog.csdn.net/qq_62839589/article/details/132695534