多態(tài)就是完成某個行為時,不同對象去完成時會產(chǎn)生不同的狀態(tài)。
比如買票,普通人買全價票,學生買半價票,軍人優(yōu)先買票。
多態(tài)是在不同繼承關系的類對象,去調(diào)用同一函數(shù),產(chǎn)生不同的行為。
繼承中構成多態(tài)還有兩個條件:
1.必須通過基類的指針或引用去調(diào)用虛函數(shù);
2.被調(diào)用的函數(shù)必須是虛函數(shù),且派生類必須對基類的虛函數(shù)進行重寫;
用virtual關鍵字修飾的成員函數(shù)就是虛函數(shù)。
代碼如下:
class Person
{public:
virtual void BuyTicket()
{cout<< "買票 - 全價"<< endl;
}
};
3、虛函數(shù)的重寫派生類中的虛函數(shù)構成重寫(覆蓋)的條件有:函數(shù)名、參數(shù)和返回值類型相同,但是函數(shù)的實現(xiàn)不同;
如果不構成重寫,就是隱藏關系;
代碼如下:
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<< "買票 - 優(yōu)先"<< endl; //重寫
}
};
void Func(Person& p) //使用父類的引用調(diào)用虛函數(shù)
{p.BuyTicket();
}
void Test()
{Person p;
Func(p);
Student st;
Func(st);
Soldier sd;
Func(sd);
}
以上代碼就完整的構成了多態(tài),其運行效果為:
不同的對象調(diào)用同一個虛函數(shù),呈現(xiàn)出了不同的效果。
1.將子類中虛函數(shù)的virtual去掉
class Person
{public:
virtual void BuyTicket()
{cout<< "買票 - 全價"<< endl;
}
};
class Student : public Person
{public:
void BuyTicket()
{cout<< "買票 - 半價"<< endl;
}
};
這樣依然構成重寫,子類中依然是虛函數(shù),編譯器認為先把父類的虛函數(shù)繼承下來了,而且是接口繼承,將函數(shù)接口完整繼承下來了,子類中只是將函數(shù)的實現(xiàn)進行重寫。
2.重寫的協(xié)變
返回值類型可以不同,要求必須是父子關系的指針或引用;
代碼如下:
class Person
{public:
virtual Person* BuyTicket()
{cout<< "買票 - 全價"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket()
{cout<< "買票 - 半價"<< endl;
}
};
以上的代碼是會報錯的,因為只滿足了返回值類型不同,并不是父子關系的指針或引用,下面的代碼才是協(xié)變:
class Person
{public:
virtual Person* BuyTicket()
{cout<< "買票 - 全價"<< endl;
return this;
}
};
class Student : public Person
{public:
virtual Student* BuyTicket() //返回值是有父子關系的指針或引用
{cout<< "買票 - 半價"<< endl;
return this;
}
};
運行結果為:
上面的代碼依然能夠構成多態(tài)。
3.析構函數(shù)的重寫
建議在繼承中將析構函數(shù)定義為虛函數(shù);
class Person
{public:
virtual ~Person()
{cout<< "~Person()"<< endl;
}
};
class Student : public Person
{public:
virtual ~Student() //子類的析構函數(shù)與父類的析構函數(shù)的函數(shù)名并不相同
{cout<< "~Student()"<< endl;
}
};
int main()
{Person* p1 = new Person;
delete p1;
Person* p2 = new Student;//父類指針指向子類對象,符合多態(tài)調(diào)用
delete p2;
return 0;
}
以上代碼的運行結果為:
子類和父類的析構函數(shù),參數(shù)類型和返回值類型都相同,編譯器為了讓他們構成重寫,將析構函數(shù)名改寫為destructor,所以,上述代碼中析構函數(shù)完成了重寫。
只有子類析構函數(shù)重寫了父類的析構函數(shù),這里才能正確調(diào)用,指針指向父類對象,調(diào)用父類的析構函數(shù),指向子類對象就調(diào)用子類的析構函數(shù)。
如果析構函數(shù)不是虛函數(shù):
class Person
{public:
~Person()
{cout<< "~Person()"<< endl;
}
};
class Student : public Person
{public:
~Student() //子類的析構函數(shù)與父類的析構函數(shù)的函數(shù)名并不相同
{cout<< "~Student()"<< endl;
}
};
int main()
{Person* ptr1 = new Person;
delete ptr1;
Person* ptr2 = new Student;//父類指針指向子類對象,符合多態(tài)調(diào)用
delete ptr2;
return 0;
}
在子類delete時,調(diào)用的還是父類的析構函數(shù):
這里是普通調(diào)用,不符合多態(tài),在編譯時就決定了;
ptr2是Person*類型的指針,call的是Person的析構函數(shù);
ptr1希望調(diào)用父類的析構,ptr2希望調(diào)用子類的析構,所以把析構設計成符合多態(tài)的函數(shù)名。
1.不是父類的指針或引用調(diào)用虛函數(shù)
代碼如下:
class Person
{public:
virtual void BuyTicket()
{cout<< "買票 - 全價"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket()
{cout<< "買票 - 半價"<< endl;
}
};
void Func(Person p)
{p.BuyTicket();
}
運行結果為:
上述代碼是不構成多態(tài)的。
2.不符合虛函數(shù)重寫
2.1將父類虛函數(shù)的virtual去掉
class Person
{public:
void BuyTicket()
{cout<< "買票 - 全價"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket()
{cout<< "買票 - 半價"<< endl;
}
};
void Func(Person& p)
{p.BuyTicket();
}
運行結果為:
是不符合虛函數(shù)重寫的,自然就不構成多態(tài)。
2.2參數(shù)類型不同
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();
}
運行結果為:
同樣不符合多態(tài)。
1.final:修飾虛函數(shù),表示其不能再被重寫(用的很少)
class Person
{public:
virtual void BuyTicket() final
{cout<< "買票 - 全價"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket()
{cout<< "買票 - 半價"<< endl;
}
};
編譯之后會報錯:
2.override:檢查派生類虛函數(shù)是否重寫了某個基類的虛函數(shù),若沒有重寫編譯報錯(常用)
class Person
{public:
virtual void BuyTicket(int)
{cout<< "買票 - 全價"<< endl;
}
};
class Student : public Person
{public:
virtual void BuyTicket(char) override//參數(shù)類型不一致,未完成重寫
{cout<< "買票 - 全價"<< endl;
}
};
上述代碼子類的虛函數(shù)未完成重寫,在后面加了override后,編譯器就會報錯:
override常用于檢查子類虛函數(shù)重寫的語法是否正確。
代碼如下:
#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;
}
以上代碼的輸出結果為:
分析:
如果將代碼改成以下形式:
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依然構成虛函數(shù)重寫;
這里用父類的指針指向子類對象,發(fā)生了切片,但指向的還是子類的對象,所以調(diào)用的函數(shù)還是子類中的虛函數(shù),結果還是:B ->1;
最終結果與p的指針類型無關,只與它指向的對象有關。
在虛函數(shù)的后面寫上 = 0,這個函數(shù)就是純虛函數(shù),包含純虛函數(shù)的類叫做抽象類(接口類),抽象類不能實例化出對象,派生類繼承抽象類后也不能實例化出對象,只有派生類重寫了虛函數(shù),才能實例化對象,純虛函數(shù)規(guī)范了派生類必須重寫,更好的體現(xiàn)出了接口繼承。
代碼如下:
class Car //把不想實例化出對象的父類定義為抽象類
{public:
virtual void Drive() = 0;
};
class Benz : public Car
{public:
virtual void Drive(int) //如果子類繼承了抽象類卻未完成虛函數(shù)重寫,就會報錯
{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.抽象函數(shù)強制子類完成虛函數(shù)的重寫,不重寫就無法實例化,而override是檢查語法是否完成重寫;
創(chuàng)建如下對象:
class Base
{public:
virtual void func()
{cout<< "func"<< endl;
}
private:
int _b = 0;
};
int main()
{Base b;
cout<< sizeof(b)<< endl;
return 0;
}
我么可以發(fā)現(xiàn)sizeof(b)的結果是8,再看b對象實例化后的成員
可以發(fā)現(xiàn)在成員_b的上面還有一個_vfptr的成員,這叫做虛函數(shù)表指針;帶有虛函數(shù)的類對象,其成員中都有一個虛函數(shù)表指針,因為選虛函數(shù)要放到虛函數(shù)表中,也簡稱虛表。
將Base繼承給子類,代碼如下:
class Base
{public:
virtual void func1()
{cout<< "Base::func1"<< endl;
}
virtual void func2() //加一個虛函數(shù)func2
{cout<< "Base::func2"<< endl;
}
void func3() //加一個普通函數(shù)func3
{cout<< "Base::func3"<< endl;
}
private:
int _b = 1;
};
class Derive : public Base
{public:
virtual void func1() //重寫父類虛函數(shù)
{cout<< "Derive::func1"<< endl;
}
private:
int _d = 2;
};
int main()
{Base b;
cout<< sizeof(b)<< endl;
Derive d;
return 0;
}
通過監(jiān)視窗口我們可以看到:
1.子類對象d中也有一個虛函數(shù)表指針,且和父類對象b的虛表指針不同,由于子類對func1完成了重寫,虛表中的func1就是子類重寫后的Detive::func1;
2.func2是虛函數(shù),繼承下來也會放進子類的虛表,而func3不是虛函數(shù),不會放進虛表;
3.虛表本身是一個放函數(shù)指針的數(shù)組,一般情況最后會放一個nullptr(vs環(huán)境下);
4.虛表存放的是虛函數(shù)的函數(shù)指針,不是虛函數(shù),虛函數(shù)跟普通函數(shù)一樣,都存放在代碼段。
通過對匯編代碼的分析,我們可以總結出:
1.滿足多態(tài)以后的函數(shù)調(diào)用,不是在編譯時確定的,是運行起來以后再到對象中找的,程序運行時取對象中的虛表指針找到函數(shù)地址,再去調(diào)用;
2.普通函數(shù)的調(diào)用,是在編譯鏈接時就確定函數(shù)的地址,運行時直接調(diào)用。
代碼如下:
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;
};
通過監(jiān)視窗口看不見func3和func4,我們可以使用代碼打印虛表中的函數(shù):
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、沒有參數(shù)的類型的函數(shù)的指針重定義為VFPTR
void PrintVTable(VFPTR vTable[])
{//依次取虛表中的指針打印并調(diào)用,調(diào)用就可以看出存的是哪個函數(shù)
cout<< "虛表地址>"<< vTable<< endl;
for (int i = 0; vTable[i] != nullptr; i++)
{printf("第%d個虛函數(shù)地址:0x%x", i, vTable[i]);//打印地址
VFPTR f = vTable[i];//用函數(shù)指針取出虛函數(shù)地址
f();//調(diào)用
}
cout<< endl;
}
int main()
{Base b1;
Base b2;
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*((int*)&b1));//將b對象的地址取出,強轉(zhuǎn)成int*,再解引用,就取出了b的頭四個字節(jié)的數(shù)據(jù),這個就是指向虛表的指針
//再強轉(zhuǎn)成VFPTR*,因為虛表就是VFPTR類型的數(shù)組
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*((int*)&b2));
PrintVTable(vTableb2);
VFPTR* vTabled = (VFPTR*)(*((int*)&d));
PrintVTable(vTabled);
return 0;
}
我么可以看出,在vs下:
1.同一個類型的對象,共用一個虛表(b1和b2);
2.不管是否完成重寫名子類虛表和父類虛表都不是同一個;
3.單繼承中,子類的所有虛函數(shù),包括重寫父類的虛函數(shù)和未重寫的虛函數(shù),都放在同一個虛表中。
代碼如下:
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、沒有參數(shù)的類型的函數(shù)的指針重定義為VFPTR
void PrintVTable(VFPTR vTable[])
{//依次取虛表中的指針打印并調(diào)用,調(diào)用就可以看出存的是哪個函數(shù)
cout<< "虛表地址>"<< vTable<< endl;
for (int i = 0; vTable[i] != nullptr; i++)
{printf("第%d個虛函數(shù)地址:0x%x", i, vTable[i]);//打印地址
VFPTR f = vTable[i];//用函數(shù)指針取出虛函數(shù)地址
f();//調(diào)用
}
cout<< endl;
}
int main()
{Derive d;
VFPTR* vTabled1 = (VFPTR*)(*((int*)&d));
PrintVTable(vTabled1);
VFPTR* vTabled2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));//從Base2的虛表中取虛函數(shù)地址
PrintVTable(vTabled2);
return 0;
}
Derive多繼承Base1和Base2,其中Derive重寫了func1,而func1既是Base1的虛函數(shù),也是Base2的虛函數(shù),func3是Derive自己的虛函數(shù),運行結果如下:
可以看出在多繼承下:
1.子類中每一個繼承的父類都有自己的虛表,存放父類中的虛函數(shù);
2.子類中重寫的虛函數(shù)會覆蓋子類中父類虛表對應的虛函數(shù),Base1和Base2中的func1都沒覆蓋為了Derive::func1;
3.子類中繼承的Base1中的func1和Base2中的func1的地址不同,但它們都是Derive重寫后的虛函數(shù),最終調(diào)用的是同一個func1,只是中間多了一個步驟;
4.子類未重寫的的虛函數(shù)放在第一個繼承的父類的虛表中;
以下程序的輸出結果是:
B和C都是虛繼承A,D多繼承B和C,所以B和C在D中共享一個A,所以B和C都不能去初始化D中的A對象,只能在D中單獨進行A的初始化;
初始化是按照類聲明的順序來的,不是按照初始化列表的順序,所以在D中先初始化A對象,在初始化B和C,這事就不會重復初始化A了,最后初始化D,所以答案選A。
可以,inline函數(shù)是沒有地址的,而且inline只是對編譯器的一個建議,當一個inline函數(shù)是虛函數(shù)時,在多態(tài)調(diào)用以后,inline就失效了,因為虛函數(shù)要放進虛表中。
3.靜態(tài)成員函數(shù)可以是虛函數(shù)嗎不可以,static函數(shù)沒有this指針,可以直接使用類名::函數(shù)名()的方式調(diào)用,而使用類名::函數(shù)名()的方式無法訪問對象的虛表,因此靜態(tài)成員函數(shù)無法放進虛表,虛函數(shù)是為了實現(xiàn)多態(tài),多態(tài)運行時都是去虛表中找決議,靜態(tài)成員函數(shù)都是在編譯時就決議了,因此它是虛函數(shù)沒有價值。
4.構造函數(shù)可以是虛函數(shù)嗎不可以,因為虛函數(shù)是為了實現(xiàn)多態(tài)調(diào)用,運行時去虛表中找對應的虛函數(shù)進行調(diào)用,對象中的虛表指針都是在構造函數(shù)初始化列表階段才初始化的,構造函數(shù)是虛函數(shù)沒有意義。
5.析構函數(shù)可以是虛函數(shù)嗎可以,并且最好把基類的析構函數(shù)定義為虛函數(shù),詳情參考 二-5-3。
6.拷貝構造和賦值可以是虛函數(shù)嗎拷貝構造不可以,因為拷貝構造也是構造函數(shù),參考上面的構造函數(shù);
賦值重載operator==()可以,但是沒有實際價值。
如果虛函數(shù)不構成多態(tài),是一樣快的;
如果虛函數(shù)構成多態(tài),調(diào)用普通函數(shù)比較快,因為構成多態(tài)調(diào)用虛函數(shù)時,運行中需要到虛表中去查找。
虛函數(shù)表是在編譯階段就生成好的,存在代碼段(常量區(qū));
構造函數(shù)初始化列表階段初始化的是虛函數(shù)表指針,對象中存的也是虛函數(shù)表指針。
你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調(diào)度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧
當前名稱:C++知識點--多態(tài)-創(chuàng)新互聯(lián)
鏈接地址:http://m.rwnh.cn/article10/ddoego.html
成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供定制網(wǎng)站、虛擬主機、關鍵詞優(yōu)化、軟件開發(fā)、外貿(mào)網(wǎng)站建設、品牌網(wǎng)站設計
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)