[C ++]ポリモーフィズムの一般的な問題の分析、できないものがあるかどうかを確認します


1.まとめ


静的ポリモーフィズム:関数のオーバーロード、同じ関数の呼び出し、異なるパラメーターの受け渡し、異なる動作があります
動的ポリモーフィズム:仮想関数の呼び出し、異なるオブジェクトは異なる動作/形状を持ちます

ポリモーフィズム条件:
a。親クラスの仮想関数はサブクラスでオーバーライドされます。
b。オーバーライドされた仮想関数を呼び出すには、親クラスのポインターまたは参照である必要があります。

仮想関数の書き換え:
a。親クラスと子クラスの両方が仮想関数である必要があります
b。関数名、パラメーター、および戻り値が同じである必要があります

例外:
a。共分散(親クラスと子クラスのポインターまたは参照)
b。親クラスのポインターが子クラスのオブジェクトを指している可能性があるため、デストラクタには特別な処理が必要です。子クラス
c。仮想を追加せずに仮想関数を書き換えます

オブジェクトの仮想テーブルポインタはコンストラクタの初期化リストで生成されますが、仮想関数テーブルはコンパイル時に生成されます。理由:実行時の動的生成には通常、スペースが必要であり、ヒープに割り当てる必要があります。



2.演習


以下のコードが実行されないのはなぜですか?

class A
{
    
    
public:
	virtual void func1()
	{
    
    }

	int a = 0x10;
};
class B:virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    }
	int b = 0x20;
};
class C :virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    }
	int c = 0x30;
};
class D:public B,public C
{
    
    
public:
	/*virtual void func1()
	{}*/
	int d = 0x40;
};

int main()
{
    
    
	D d;
	return 0;
}

オブジェクトモデルを分析すると、BとCは実質的にAを継承するため、dのオブジェクトモデルにはAのコピーが1つだけありますが、BとCの両方がAを書き換えたため、func1には2つの異なる実装バージョンがあるというdの見解になります。 Dでfunc1をオーバーライドしないと、コンパイルエラーが発生します。ただし、func1がBとCで書き換えられない場合、Dにはfunc1のコピーが2つありますが、それらは同じであり、現時点ではコンパイルエラーは報告されません。
ここに画像の説明を挿入


仮想ベーステーブルポインタの内容の最初の4バイトの機能:
仮想テーブルポインタから仮想ベーステーブルポインタまでの距離を計算するために使用されます。
次のコードでは、dのオブジェクトモデルを分析できます。

class A
{
    
    
public:
	virtual void func1()
	{
    
    }

	int a = 0x10;
};
class B:virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    }
	virtual void func2()
	{
    
    }
	int b = 0x20;
};
class C :virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    }
	virtual void func2()
	{
    
    }
	int c = 0x30;
};
class D:public B,public C
{
    
    
public:
	virtual void func1()
	{
    
    }
	virtual void func2()
	{
    
    }
	int d = 0x40;
};

int main()
{
    
    
	D d;
	
	return 0;
}

メモリウィンドウとモニタリングウィンドウから、dオブジェクトのB部分の最初のアドレスが仮想関数テーブルへのポインタであり、2番目のフィールドが仮想ベーステーブルを指していることがわかります。前のブログでは、仮想baseテーブルの最初のフィールドは常に0x000000 00でしたが、現在は0xff ff ff fc、つまり-4です。仮想ベーステーブルポインターのアドレスから4バイト上がると、まさに仮想関数テーブルポインターになります。したがって、仮想ベーステーブルの最初のフィールドは、仮想関数テーブルを取得するために使用される場所でもあります。
ここに画像の説明を挿入



複数の選択肢

  1. 次のオブジェクト指向メソッドのどれがあなたを豊かにすることができますか()
    A:継承B:カプセル化C:ポリモーフィズムD:抽象化

A.継承は、一種の再利用です

  1. ()は、オブジェクト指向プログラミング言語のメカニズムです。このメカニズムは、メソッドの定義が特定のオブジェクトとは関係がなく、メソッドの呼び出しを特定のオブジェクトに関連付けることができることを認識しています。
    A:継承B:テンプレートC:オブジェクト自体の参照D:動的バインディング

D.親クラスのポインターまたは参照は、子クラスの仮想関数テーブルを指すことができます

  1. オブジェクト指向設計における継承と構成、次のステートメントのどれが間違っていますか?()
    A:継承により、親クラスの実装の詳細をオーバーライドできます。親クラスの実装は子クラスに表示されます。これは一種の静的再利用であり、
    ホワイトボックス再利用とも呼ばれます
    。B:結合されたオブジェクトそれぞれを気にする必要はありません実装の詳細間の関係は実行時に決定されます。これは一種の動的再利用であり
    、ブラックボックス再利用とも呼ばれます
    。C:構成ではなく継承の使用を優先します。これは2番目です。オブジェクト指向設計の原則
    D:継承により、サブクラスは親クラスのインターフェースを自動的に継承できますが、設計モードでは、これは親クラスのカプセル化を破壊すると見なされます。

C.最初に組み合わせを使用し、継承は親クラスの静的再利用であり、組み合わせは動的再利用です。

  1. 純粋仮想関数に関する次の記述は正しいです()A:純粋仮想関数を宣言するクラスはオブジェクトをインスタンス化できませんB:純粋仮想関数を宣言するクラスは仮想基本クラスですC:サブクラスは純粋仮想関数を実装する必要があります基本クラスDの:純粋仮想関数は空の関数でなければなりません

AおよびCサブクラスは純粋仮想関数を実装できませんが、オブジェクトをインスタンス化することはできません。

  1. 仮想関数の説明は正しいです()A:派生クラスの仮想関数と基本クラスの仮想関数のパラメーターの数とタイプが異なりますB:インライン関数を仮想関数にすることはできませんC:派生クラスを再定義する必要があります基本クラス仮想関数D:仮想関数は静的関数にすることができます

B.インライン化はコンパイラへの提案です。仮想を宣言した後、インライン関数にはアドレスがなく、呼び出された場所で展開されるため、インライン属性は無視されます。仮想関数は仮想関数テーブルに配置されます。注:vs2013とliunxはどちらも正常に実行できます。

  1. 仮想テーブルに関する正しいステートメントは次のとおりです。()A:クラスは1つの仮想テーブルのみを持つことができますB:基本クラスに仮想関数があります。サブクラスが基本クラスの仮想関数をオーバーライドしない場合、サブクラスと基本クラスは同じものを共有します仮想テーブルC:仮想テーブルは実行時に動的に生成されますD:クラスの異なるオブジェクトがクラスの仮想テーブルを共有します

D. Cでの動的生成には通常、スペースが必要であり、ヒープに割り当てる必要があります。したがって、コンパイル時に生成されると推測できます。

  1. クラスAに仮想関数があるとすると、BはAから継承し、BはAの仮想関数を書き換え、仮想関数を定義しません。次に()
    A:クラスAオブジェクトの最初の4バイトは、のアドレスを格納します。仮想テーブル、クラスBオブジェクトの最初の4バイトは仮想テーブルアドレスではありませんB:AタイプオブジェクトとBタイプオブジェクトの最初の4バイトは、仮想ベーステーブルのアドレスを格納します
    C:最初の4バイトA型オブジェクトとB型オブジェクトの仮想テーブルを格納しますテーブルアドレスは同じですD:クラスAとクラスBの仮想テーブル内の仮想関数の数は同じですが、クラスAとクラスBは同じです同じ仮想テーブルを使用しない

D、仮想関数テーブルはポリモーフィックであり、仮想ベーステーブルはスーパークラスのメンバー変数を見つけるための仮想継承です


  1. 次のプログラムの出力は何ですか?()
#include<iostream>
using namespace std;
class A{
    
    
public:
	A(char *s) {
    
     cout << s << endl; }
	~A(){
    
    }
};
class B :virtual public A
{
    
    
public:
	B(char *s1, char*s2) :A(s1) {
    
     cout << s2 << endl; }
};
class C :virtual public A
{
    
    
public:
	C(char *s1, char*s2) :A(s1) {
    
     cout << s2 << endl; }
};
class D :public B, public C
{
    
    
public:
	D(char *s1, char *s2, char *s3, char *s4) :C(s1, s3), B(s1, s2), A(s1)
	{
    
    
		cout << s4 << endl;
	}
};
int main() {
    
    
	D *p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

回答:A
まず、除外方法です。継承の順序は初期化の順序であると宣言します。これは初期化リストの順序ではないことに注意してください。BはCの前にある必要があるため、CDを除外できます。 。次に、最初に親クラスを初期化する必要があります。次に、Aが先頭になります。そしてコンパイラは最適化し、各オブジェクトは一度だけ初期化されます。


9.以下の結果

class A
{
    
    
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{
    
    }

	void Print() {
    
    
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
    
    
	A aa(1);
	aa.Print();
	return 0;
}

回答:1つのランダムな値を出力
するメンバー変数の宣言の順序は初期化リストの順序であるため、最初に_a2(_a1)、次に_a1(a)になります。


  1. 多重継承におけるポインタオフセットの問題?次のステートメントは正しいです()
class Base1 {
    
     public: int _b1; };
class Base2 {
    
     public: int _b2; };
class Derive : public Base1, public Base2 {
    
     public: int _d; };
int main(){
    
    
 Derive d;
 Base1* p1 = &d;
 Base2* p2 = &d;
 Derive* p3 = &d;
 return 0;
}

A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

ここに画像の説明を挿入
オプションC


  1. 次のprogram()の出力は何ですか
class A
{
    
    
public:
 virtual void func(int val = 1){
    
     std::cout<<"A->"<< val <<std::endl;}
 virtual void test(){
    
     func();}
};
class B : public A
{
    
    
public:
 void func(int val=0){
    
     std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
    
    
 B*p = new B;
 p->test();
 return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

testはBで書き換えられないため、 Bを選択
してから、親クラスのtest()を呼び出し、testでfuncを呼び出します。ここでのこのポインターはタイプBであり、Bはfuncを書き換えています。書き換えは一種のインターフェース継承です。 、パラメータ、関数名、戻り値が同じである限り書き換えます。デフォルトのパラメータは親クラスを使用するため、最終的な出力はB->1になります。
通常の関数は実装の継承です。
仮想関数の継承は、一種のインターフェース継承です。
ここに画像の説明を挿入

class A
{
    
    
public:
	virtual void func(int val = 1)
	{
    
    
	}

	void test()
	{
    
     
	}
};


// A 编译报错  B运行崩溃  C 正常运行
int main()
{
    
    
	// 1、 
	A* p1 = nullptr;
	p1->func();

	// 2、 
	A* p2 = nullptr;
	p2->test();

	return 0;
}

1B 2C、理由は、通常のメンバー関数がコードセグメントに配置されているため、呼び出すために間接参照動作が必要ないため、2番目の関数が失敗することはなく、最初の関数は、ポリモーフィズムの2つの条件が満たされているためです。 p1が指すオブジェクトヘッダーの4バイトにアクセスするため、エラーが報告されます。


質問と回答:

静的メンバーを仮想関数にすることはできますか?
いいえ、静的メンバー関数にはこのポインターがなく、該当するタイプ:メンバー関数の呼び出しメソッドは仮想関数テーブルにアクセスできません。

コンストラクターを仮想関数にすることはできますか?
コンストラクターはポリモーフィズムを実装する必要がありますか?子クラスのオブジェクトをインスタンス化するときに親クラスのコンストラクターも呼び出すため、意味がありません。オブジェクトの仮想関数テーブルはコンパイル時に初期化され、仮想テーブルポインターは、コンストラクターの初期化リストを通過した後、実行時に生成されます。
オブジェクトの仮想テーブルポインターは、コンストラクター初期化リストで初期化されます。コンストラクターが仮想関数になっている場合、オブジェクトにはコンストラクターを呼び出すための仮想テーブルポインターが必要です。これは矛盾しているため、親クラスコンストラクターがvirtualに設定されている場合この関数は、コンパイルエラーを直接報告します。
ここに画像の説明を挿入

デストラクタには仮想関数が必要ですか?
(ポインタ)を削除すると、ポインタが指すオブジェクトが親クラスのオブジェクトなのかサブクラスのオブジェクトなのかわからないため、動的検出を実行し、ポリモーフィズムを介して解放できます。

オブジェクトが通常の関数または仮想関数にアクセスする方が速いですか?
回答:まず第一に、それが普通のオブジェクトである場合、それは同じくらい速いです。ポインタオブジェクトまたは参照オブジェクトの場合、呼び出される通常の関数の方が高速です。ポリモーフィズムのため、実行時に仮想関数を呼び出すには、仮想関数テーブルを検索する必要があります。
ポリモーフィズムを実現するためのオブジェクト呼び出しはできません


知識のポイントが少ない

1.テンプレートはコンパイル時のポリモーフィズムに属しています。逆アセンブルを行うと、実行中に呼び出されると、対応する関数にジャンプすることがわかります。
ここに画像の説明を挿入
2.親クラスのメソッドは、常に親クラスオブジェクトを使用して呼び出されます。

3.インターフェース継承の実施形態ここに画像の説明を挿入
4.静的メンバー関数を仮想関数として設定できない理由は、静的メンバー関数にこのポインターがないためです。このポインタがないと、仮想テーブルを取得できず、ポリモーフィズムを実現できません。

5.書き換えが成功したとすると、ポインターまたは参照を介してポリモーフィズムを実現できますか?
間違っています!親クラスへのポインタまたは参照である必要があります。

6.ポリモーフィズムとコンポジションの両方を使用できる場合、ポリモーフィック呼び出しには仮想関数テーブルポインター、仮想関数テーブル、ランタイム解決などの追加のオーバーヘッドがあるため、コンポジションが推奨されます。

7.フレンド関数はメンバー関数ではないため、フレンド関数を仮想関数として設定することはできません。

要約する

これでポリモーフィズムのセクションは終わりです。

おすすめ

転載: blog.csdn.net/weixin_52344401/article/details/122812549