99偷拍视频精品区一区二,口述久久久久久久久久久久,国产精品夫妇激情啪发布,成人永久免费网站在线观看,国产精品高清免费在线,青青草在线观看视频观看,久久久久久国产一区,天天婷婷久久18禁,日韩动漫av在线播放直播

C++知識點--多態-創新互聯

C++知識點 – 多態

河西ssl適用于網站、小程序/APP、API接口等需要進行數據傳輸應用場景,ssl證書未來市場廣闊!成為成都創新互聯的ssl證書銷售渠道,可以享受市場價格4-6折優惠!如果有意向歡迎電話聯系或者加微信:18980820575(備注:SSL證書合作)期待與您的合作!文章目錄
  • C++知識點 -- 多態
  • 一、多態概念
    • 1.概念
  • 二、多態的定義及實現
    • 1、多態的構成條件
    • 2、虛函數
    • 3、虛函數的重寫
    • 4.虛函數重寫的特例
    • 5、不符合多態的場景
    • 6、C++11的override和final
    • 7、重載、覆蓋(重寫)、隱藏(重定義)的對比
    • 8、例題
  • 三、抽象類
  • 四、多態的原理
    • 1、虛函數表
    • 2、多態的原理
  • 五、單繼承和多繼承關系的虛函數表
    • 1、單繼承中的虛表
    • 2、多繼承中的虛表
  • 六、多態常見面試問題
    • 1.例題
    • 2.inline函數可以是虛函數嗎
    • 3.靜態成員函數可以是虛函數嗎
    • 4.構造函數可以是虛函數嗎
    • 5.析構函數可以是虛函數嗎
    • 6.拷貝構造和賦值可以是虛函數嗎
    • 7.對象訪問普通函數快還是虛函數快
    • 8.虛函數表是在什么階段生成的,存在哪里


一、多態概念 1.概念

多態就是完成某個行為時,不同對象去完成時會產生不同的狀態。
比如買票,普通人買全價票,學生買半價票,軍人優先買票。

二、多態的定義及實現 1、多態的構成條件

多態是在不同繼承關系的類對象,去調用同一函數,產生不同的行為。
繼承中構成多態還有兩個條件:
1.必須通過基類的指針或引用去調用虛函數;
2.被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫;

2、虛函數

用virtual關鍵字修飾的成員函數就是虛函數。
代碼如下:

class Person
{public:
	virtual void BuyTicket()
	{cout<< "買票 - 全價"<< endl;
	}
};
3、虛函數的重寫

派生類中的虛函數構成重寫(覆蓋)的條件有:函數名、參數和返回值類型相同,但是函數的實現不同;
如果不構成重寫,就是隱藏關系;
代碼如下:

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

class Student : public Person
{public:
	virtual void BuyTicket()
	{cout<< "買票 - 半價"<< endl;      //重寫
	}
};

class Soldier : public Person
{public:
	virtual void BuyTicket()
	{cout<< "買票 - 優先"<< endl;      //重寫
	}
};

void Func(Person& p)           //使用父類的引用調用虛函數
{p.BuyTicket();
}

void Test()
{Person p;
	Func(p);

	Student st;
	Func(st);

	Soldier sd;
	Func(sd);
}

以上代碼就完整的構成了多態,其運行效果為:
在這里插入圖片描述
不同的對象調用同一個虛函數,呈現出了不同的效果。

4.虛函數重寫的特例

1.將子類中虛函數的virtual去掉

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

class Student : public Person
{public:
	void BuyTicket()
	{cout<< "買票 - 半價"<< endl;
	}
};

這樣依然構成重寫,子類中依然是虛函數,編譯器認為先把父類的虛函數繼承下來了,而且是接口繼承,將函數接口完整繼承下來了,子類中只是將函數的實現進行重寫。

2.重寫的協變
返回值類型可以不同,要求必須是父子關系的指針或引用;
代碼如下:

class Person
{public:
	virtual Person* BuyTicket()
	{cout<< "買票 - 全價"<< endl;
	}
};

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

以上的代碼是會報錯的,因為只滿足了返回值類型不同,并不是父子關系的指針或引用,下面的代碼才是協變:

class Person
{public:
	virtual Person* BuyTicket()
	{cout<< "買票 - 全價"<< endl;
		return this;
	}
};

class Student : public Person
{public:
	virtual Student* BuyTicket()       //返回值是有父子關系的指針或引用
	{cout<< "買票 - 半價"<< endl;
		return this;
	}
};

運行結果為:
在這里插入圖片描述
上面的代碼依然能夠構成多態。

3.析構函數的重寫
建議在繼承中將析構函數定義為虛函數;

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

class Student : public Person
{public:
	virtual ~Student()      //子類的析構函數與父類的析構函數的函數名并不相同
	{cout<< "~Student()"<< endl;
	}
};
int main()
{Person* p1 = new Person;
	delete p1;

	Person* p2 = new Student;//父類指針指向子類對象,符合多態調用
	delete p2;
	
	return 0;
}

以上代碼的運行結果為:
在這里插入圖片描述
子類和父類的析構函數,參數類型和返回值類型都相同,編譯器為了讓他們構成重寫,將析構函數名改寫為destructor,所以,上述代碼中析構函數完成了重寫。
只有子類析構函數重寫了父類的析構函數,這里才能正確調用,指針指向父類對象,調用父類的析構函數,指向子類對象就調用子類的析構函數。

如果析構函數不是虛函數:

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

class Student : public Person
{public:
	~Student()      //子類的析構函數與父類的析構函數的函數名并不相同
	{cout<< "~Student()"<< endl;
	}
};
int main()
{Person* ptr1 = new Person;
	delete ptr1;

	Person* ptr2 = new Student;//父類指針指向子類對象,符合多態調用
	delete ptr2;
	
	return 0;
}

在這里插入圖片描述
在子類delete時,調用的還是父類的析構函數:
在這里插入圖片描述
這里是普通調用,不符合多態,在編譯時就決定了;
ptr2是Person*類型的指針,call的是Person的析構函數;
ptr1希望調用父類的析構,ptr2希望調用子類的析構,所以把析構設計成符合多態的函數名。

5、不符合多態的場景

1.不是父類的指針或引用調用虛函數
代碼如下:

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

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

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

運行結果為:
在這里插入圖片描述
上述代碼是不構成多態的。

2.不符合虛函數重寫
2.1將父類虛函數的virtual去掉

class Person
{public:
	void BuyTicket()
	{cout<< "買票 - 全價"<< endl;
	}
};

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

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

運行結果為:
在這里插入圖片描述
是不符合虛函數重寫的,自然就不構成多態。

2.2參數類型不同

class Person
{public:
	void BuyTicket(char)
	{cout<< "買票 - 全價"<< endl;
	}
};

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

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

運行結果為:
在這里插入圖片描述
同樣不符合多態。

6、C++11的override和final

1.final:修飾虛函數,表示其不能再被重寫(用的很少)

class Person
{public:
	virtual void BuyTicket() final
	{cout<< "買票 - 全價"<< endl;
	}
};

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

編譯之后會報錯:
在這里插入圖片描述
2.override:檢查派生類虛函數是否重寫了某個基類的虛函數,若沒有重寫編譯報錯(常用)

class Person
{public:
	virtual void BuyTicket(int)
	{cout<< "買票 - 全價"<< endl;
	}
};

class Student : public Person
{public:
	virtual void BuyTicket(char) override//參數類型不一致,未完成重寫
	{cout<< "買票 - 全價"<< endl;
	}
};

上述代碼子類的虛函數未完成重寫,在后面加了override后,編譯器就會報錯:
在這里插入圖片描述
override常用于檢查子類虛函數重寫的語法是否正確。

7、重載、覆蓋(重寫)、隱藏(重定義)的對比

在這里插入圖片描述

8、例題

代碼如下:

#includeusing namespace std;

class A
{public:
	virtual void func(int val = 1)
	{cout<< "A ->"<< val<< endl;
	}

	virtual void test()
	{func();
	}
};

class B : public A
{public:
	virtual void func(int val = 0)
	{cout<< "B ->"<< val<< endl;
	}
};

int main()
{B* p = new B;
	p->test();

	return 0;
}

以上代碼的輸出結果為:
在這里插入圖片描述
分析:

  1. A為父類,B公有繼承A,繼承了A的func和test函數,其中A和B的func函數構成了虛函數重寫(不要求參數的缺省值相同),因此構成了多態;
  2. main函數中,B指針p指向B對象,用p調用了test函數,p的類型是B,而test中this指針的類型是A*,p傳給this,這里用父類指針指向子類對象,構成了切片;
  3. 這里this指針是A*類型的,用this調用func函數,符合父類指針調用虛函數,符合多態調用,多態調用時,指針指向那個類對象,就調用哪個類中的虛函數,顯然p和this指向的都是子類對象,所以這里調用的是子類中的虛函數;
  4. 虛函數是接口繼承,普通函數數實現繼承;虛函數繼承時,直接將父類的函數接口繼承下來,與子類的接口是無關的,子類重寫的是實現,這里的接口是父類的接口,val的缺省值是1,因此函數最終的輸出結果為:B ->1,選B。

如果將代碼改成以下形式:

class A
{public:
	virtual void func(int val)//去掉缺省值
	{cout<< "A ->"<< val<< endl;
	}

	virtual void test()
	{func(1);
	}
};

class B : public A
{public:
	void func(int val)
	{cout<< "B ->"<< val<< endl;
	}
};

int main()
{//Test();

	A* p = new B;//用父類的指針指向子類對象
	p->test();

	return 0;
}

子類和父類的func依然構成虛函數重寫;
這里用父類的指針指向子類對象,發生了切片,但指向的還是子類的對象,所以調用的函數還是子類中的虛函數,結果還是:B ->1;
在這里插入圖片描述
最終結果與p的指針類型無關,只與它指向的對象有關。

三、抽象類

在虛函數的后面寫上 = 0,這個函數就是純虛函數,包含純虛函數的類叫做抽象類(接口類),抽象類不能實例化出對象,派生類繼承抽象類后也不能實例化出對象,只有派生類重寫了虛函數,才能實例化對象,純虛函數規范了派生類必須重寫,更好的體現出了接口繼承。
代碼如下:

class Car             //把不想實例化出對象的父類定義為抽象類
{public:
	virtual void Drive() = 0;
};

class Benz : public Car
{public:
	virtual void Drive(int)     //如果子類繼承了抽象類卻未完成虛函數重寫,就會報錯
	{cout<< "Benz - 舒適"<< endl;
	}
};

class BMW : public Car
{public:
	virtual void Drive()
	{cout<< "BMW - 操控"<< endl;
	}
};

int main()
{Car c1;
	Benz c2;
	BMW c3;

	return 0;
}

在這里插入圖片描述
1.抽象類一般用于定義接口,將不想實例化出對象的類定義為抽象類;
2.抽象函數強制子類完成虛函數的重寫,不重寫就無法實例化,而override是檢查語法是否完成重寫;

四、多態的原理 1、虛函數表

創建如下對象:

class Base
{public:
	virtual void func()
	{cout<< "func"<< endl;
	}
private:
	int _b = 0;
};

int main()
{Base b;
	cout<< sizeof(b)<< endl;

	return 0;
}

我么可以發現sizeof(b)的結果是8,再看b對象實例化后的成員
在這里插入圖片描述
可以發現在成員_b的上面還有一個_vfptr的成員,這叫做虛函數表指針;帶有虛函數的類對象,其成員中都有一個虛函數表指針,因為選虛函數要放到虛函數表中,也簡稱虛表。
將Base繼承給子類,代碼如下:

class Base
{public:
	virtual void func1()
	{cout<< "Base::func1"<< endl;
	}
	virtual void func2()             //加一個虛函數func2
	{cout<< "Base::func2"<< endl;
	}
	void func3()                     //加一個普通函數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;
	cout<< sizeof(b)<< endl;

	Derive d;

	return 0;
}

通過監視窗口我們可以看到:
在這里插入圖片描述
1.子類對象d中也有一個虛函數表指針,且和父類對象b的虛表指針不同,由于子類對func1完成了重寫,虛表中的func1就是子類重寫后的Detive::func1;
2.func2是虛函數,繼承下來也會放進子類的虛表,而func3不是虛函數,不會放進虛表;
3.虛表本身是一個放函數指針的數組,一般情況最后會放一個nullptr(vs環境下);
4.虛表存放的是虛函數的函數指針,不是虛函數,虛函數跟普通函數一樣,都存放在代碼段。

2、多態的原理

在這里插入圖片描述
在這里插入圖片描述
通過對匯編代碼的分析,我們可以總結出:
1.滿足多態以后的函數調用,不是在編譯時確定的,是運行起來以后再到對象中找的,程序運行時取對象中的虛表指針找到函數地址,再去調用;
2.普通函數的調用,是在編譯鏈接時就確定函數的地址,運行時直接調用。

五、單繼承和多繼承關系的虛函數表 1、單繼承中的虛表

代碼如下:

class Base
{public:
	virtual void func1()
	{cout<< "Base::func1"<< endl;
	}
	virtual void func2()
	{cout<< "Base::func2"<< endl;
	}

private:
	int _b = 1;
};

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 _d = 2;
};

在這里插入圖片描述
通過監視窗口看不見func3和func4,我們可以使用代碼打印虛表中的函數:

class Base
{public:
	virtual void func1()
	{cout<< "Base::func1"<< endl;
	}
	virtual void func2()
	{cout<< "Base::func2"<< endl;
	}

private:
	int _b = 1;
};

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 _d = 2;
};

typedef void(*VFPTR) ();     //將指向返回值為void、沒有參數的類型的函數的指針重定義為VFPTR
void PrintVTable(VFPTR vTable[])
{//依次取虛表中的指針打印并調用,調用就可以看出存的是哪個函數
	cout<< "虛表地址>"<< vTable<< endl;
	for (int i = 0; vTable[i] != nullptr; i++)
	{printf("第%d個虛函數地址:0x%x", i, vTable[i]);//打印地址
		VFPTR f = vTable[i];//用函數指針取出虛函數地址
		f();//調用
	}
	cout<< endl;
}

int main()
{Base b1;
	Base b2;
	Derive d;

	VFPTR* vTableb1 = (VFPTR*)(*((int*)&b1));//將b對象的地址取出,強轉成int*,再解引用,就取出了b的頭四個字節的數據,這個就是指向虛表的指針
										   //再強轉成VFPTR*,因為虛表就是VFPTR類型的數組
	PrintVTable(vTableb1);

	VFPTR* vTableb2 = (VFPTR*)(*((int*)&b2));
	PrintVTable(vTableb2);


	VFPTR* vTabled = (VFPTR*)(*((int*)&d));
	PrintVTable(vTabled);

	return 0;
}

在這里插入圖片描述
在這里插入圖片描述
我么可以看出,在vs下:
1.同一個類型的對象,共用一個虛表(b1和b2);
2.不管是否完成重寫名子類虛表和父類虛表都不是同一個;
3.單繼承中,子類的所有虛函數,包括重寫父類的虛函數和未重寫的虛函數,都放在同一個虛表中。

2、多繼承中的虛表

代碼如下:

class Base1
{public:
	virtual void func1()
	{cout<< "Base1::func1"<< endl;
	}
	virtual void func2()
	{cout<< "Base1::func2"<< endl;
	}

private:
	int _b1 = 1;
};

class Base2
{public:
	virtual void func1()
	{cout<< "Base2::func1"<< endl;
	}
	virtual void func2()
	{cout<< "Base2::func2"<< endl;
	}

private:
	int _b2 = 2;
};


class Derive : public Base1, public Base2
{public:
	virtual void func1()
	{cout<< "Derive::func1"<< endl;
	}
	virtual void func3()
	{cout<< "Derive::func3"<< endl;
	}

private:
	int _d = 3;
};

typedef void(*VFPTR) ();     //將指向返回值為void、沒有參數的類型的函數的指針重定義為VFPTR
void PrintVTable(VFPTR vTable[])
{//依次取虛表中的指針打印并調用,調用就可以看出存的是哪個函數
	cout<< "虛表地址>"<< vTable<< endl;
	for (int i = 0; vTable[i] != nullptr; i++)
	{printf("第%d個虛函數地址:0x%x", i, vTable[i]);//打印地址
		VFPTR f = vTable[i];//用函數指針取出虛函數地址
		f();//調用
	}
	cout<< endl;
}

int main()
{Derive d;

	VFPTR* vTabled1 = (VFPTR*)(*((int*)&d));
	PrintVTable(vTabled1);

	VFPTR* vTabled2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));//從Base2的虛表中取虛函數地址
	PrintVTable(vTabled2);


	return 0;
}

Derive多繼承Base1和Base2,其中Derive重寫了func1,而func1既是Base1的虛函數,也是Base2的虛函數,func3是Derive自己的虛函數,運行結果如下:
在這里插入圖片描述
在這里插入圖片描述

可以看出在多繼承下:
1.子類中每一個繼承的父類都有自己的虛表,存放父類中的虛函數;
2.子類中重寫的虛函數會覆蓋子類中父類虛表對應的虛函數,Base1和Base2中的func1都沒覆蓋為了Derive::func1;
3.子類中繼承的Base1中的func1和Base2中的func1的地址不同,但它們都是Derive重寫后的虛函數,最終調用的是同一個func1,只是中間多了一個步驟;
4.子類未重寫的的虛函數放在第一個繼承的父類的虛表中;
在這里插入圖片描述

六、多態常見面試問題 1.例題

以下程序的輸出結果是:
在這里插入圖片描述
B和C都是虛繼承A,D多繼承B和C,所以B和C在D中共享一個A,所以B和C都不能去初始化D中的A對象,只能在D中單獨進行A的初始化;
初始化是按照類聲明的順序來的,不是按照初始化列表的順序,所以在D中先初始化A對象,在初始化B和C,這事就不會重復初始化A了,最后初始化D,所以答案選A。

2.inline函數可以是虛函數嗎

可以,inline函數是沒有地址的,而且inline只是對編譯器的一個建議,當一個inline函數是虛函數時,在多態調用以后,inline就失效了,因為虛函數要放進虛表中。

3.靜態成員函數可以是虛函數嗎

不可以,static函數沒有this指針,可以直接使用類名::函數名()的方式調用,而使用類名::函數名()的方式無法訪問對象的虛表,因此靜態成員函數無法放進虛表,虛函數是為了實現多態,多態運行時都是去虛表中找決議,靜態成員函數都是在編譯時就決議了,因此它是虛函數沒有價值。

4.構造函數可以是虛函數嗎

不可以,因為虛函數是為了實現多態調用,運行時去虛表中找對應的虛函數進行調用,對象中的虛表指針都是在構造函數初始化列表階段才初始化的,構造函數是虛函數沒有意義。

5.析構函數可以是虛函數嗎

可以,并且最好把基類的析構函數定義為虛函數,詳情參考 二-5-3。

6.拷貝構造和賦值可以是虛函數嗎

拷貝構造不可以,因為拷貝構造也是構造函數,參考上面的構造函數;
賦值重載operator==()可以,但是沒有實際價值。

7.對象訪問普通函數快還是虛函數快

如果虛函數不構成多態,是一樣快的;
如果虛函數構成多態,調用普通函數比較快,因為構成多態調用虛函數時,運行中需要到虛表中去查找。

8.虛函數表是在什么階段生成的,存在哪里

虛函數表是在編譯階段就生成好的,存在代碼段(常量區);
構造函數初始化列表階段初始化的是虛函數表指針,對象中存的也是虛函數表指針。

你是否還在尋找穩定的海外服務器提供商?創新互聯www.cdcxhl.cn海外機房具備T級流量清洗系統配攻擊溯源,準確流量調度確保服務器高可用性,企業級服務器適合批量采購,新人活動首月15元起,快前往官網查看詳情吧

當前名稱:C++知識點--多態-創新互聯
鏈接地址:http://www.yijiale78.com/article10/ddoego.html

成都網站建設公司_創新互聯,為您提供定制網站虛擬主機關鍵詞優化軟件開發外貿網站建設品牌網站設計

廣告

聲明:本網站發布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創新互聯

小程序開發