邢臺(tái)網(wǎng)站網(wǎng)頁(yè)設(shè)計(jì)公司江西百度推廣公司
前言
作者:小蝸牛向前沖
名言:我可以接受失敗,但我不能接受放棄
??如果覺的博主的文章還不錯(cuò)的話,還請(qǐng)
點(diǎn)贊,收藏,關(guān)注👀支持博主。如果發(fā)現(xiàn)有問(wèn)題的地方歡迎?大家在評(píng)論區(qū)指正。
本期學(xué)習(xí)目標(biāo):認(rèn)識(shí)什么是多態(tài),?認(rèn)識(shí)抽象類,理解多態(tài)的原理,理解多承和多態(tài)常見的面試問(wèn)題。?
目錄
一、認(rèn)識(shí)多態(tài)
1、什么是多態(tài)
2、虛函數(shù)
3、多態(tài)的定義
4、多態(tài)中虛函數(shù)的二種特殊情況
二、抽象類
1、概念
2、?接口繼承和實(shí)現(xiàn)繼承
三、多態(tài)的原理
1、虛函數(shù)表
2、多態(tài)原理剖析
3、單繼承和多繼承關(guān)系的虛函數(shù)表
四、多態(tài)的其他知識(shí)
1、C++11 override 和 final
2、重載、覆蓋(重寫)、隱藏(重定義)的對(duì)比
3、動(dòng)態(tài)綁定與靜態(tài)綁定
五、分享繼承和多態(tài)常見的面試問(wèn)題
一、認(rèn)識(shí)多態(tài)
1、什么是多態(tài)
多態(tài)的概念:通俗來(lái)說(shuō),就是多種形態(tài),具體點(diǎn)就是去完成某個(gè)行為,當(dāng)不同的對(duì)象去完成時(shí)會(huì)
產(chǎn)生出不同的狀態(tài)。
舉個(gè)例子來(lái)說(shuō):某寶常年到大學(xué)的開學(xué)季就會(huì)舉辦掃描領(lǐng)紅包活動(dòng),但是我們發(fā)現(xiàn)每個(gè)領(lǐng)到的金額都不同,而且相對(duì)來(lái)是非活躍用戶紅包金額更大,這種我們不同角色,分到吧同金額的紅包我們就可以稱為多態(tài)。
2、虛函數(shù)
在認(rèn)識(shí)多態(tài)前,我們首先要認(rèn)識(shí)一一下什么是虛函數(shù):
在類中被關(guān)鍵字virtual修飾的函數(shù)我們就稱為虛函數(shù),就如red_packet函數(shù),在繼承中我用關(guān)鍵字virtual解決的菱形繼承問(wèn)題,這里要注意區(qū)分二則的用法,一個(gè)是在繼承這作用于類,一個(gè)是在多態(tài)中作用于函數(shù)。
class Peson
{virtual void red_packet(){cout << "小額紅包" << endl;}
protected:string _name;int _age;
};
3、多態(tài)的定義
那么在繼承中要構(gòu)成多態(tài)還有兩個(gè)條件:
1. 必須通過(guò)基類的指針或者引用調(diào)用虛函數(shù)
2. 被調(diào)用的函數(shù)必須是虛函數(shù),且派生類必須對(duì)基類的虛函數(shù)進(jìn)行重寫
?虛函數(shù)的重寫(覆蓋):派生類中有一個(gè)跟基類完全相同的虛函數(shù)(即派生類虛函數(shù)與基類虛函數(shù)的
返回值類型、函數(shù)名字、參數(shù)列表完全相同),稱子類的虛函數(shù)重寫了基類的虛函數(shù)。
下面我們來(lái)看一段代碼:
class Person
{
public://虛函數(shù)virtual void red_packet(){cout << "普通紅包" << endl;}
protected:string _name;int _age;
};class active_people:public Person
{
public:virtual void red_packet(){cout << "小額紅包" << endl;}
};void fun(Peson& p)
{p.red_packet();
}
int main()
{Person p;active_people ap;fun(p);fun(ap);return 0;
}
?
這里我們通過(guò)父類的引用形成了多態(tài)。
特別注意:子類函數(shù)可以不加victual,但父類必須加victual才構(gòu)成虛函數(shù)。
4、多態(tài)中虛函數(shù)的二種特殊情況
我們都知道,要構(gòu)成多態(tài)就用就要滿足構(gòu)成多態(tài)的二個(gè)條件:其中被調(diào)用的函數(shù)必須是虛函數(shù),也就是說(shuō)必須加是virtual。
但是存在二種特殊情況也構(gòu)成多態(tài):
析構(gòu)函數(shù)(函數(shù)名字不同構(gòu)成的重寫)
class Person
{
public:~Person(){cout << " Person delete" << endl;delete[]_p;}
protected:int* _p = new int[10];};class active_people:public Person
{
public:~active_people(){cout << " active_people delete" << endl;delete[]_d;}
protected:int* _d = new int[15];};int main()
{Person p;active_people ap;return 0;
}
通過(guò)這段代碼我們可以看出,父子類在調(diào)用析構(gòu)函數(shù)的時(shí)候,是先調(diào)用子類的析構(gòu)函數(shù),子類的析構(gòu)函數(shù)調(diào)用完成后,子類會(huì)自動(dòng)調(diào)用父類的析構(gòu)函數(shù)。?
但是如果我對(duì)代碼進(jìn)行一下更改:
int main()
{Person* ptr1 = new Person;Person* ptr2 = new active_people;delete ptr1;delete ptr2;return 0;
}
這里我們發(fā)現(xiàn)子類并沒有被析構(gòu)掉。為什么呢?
這就得提一下delete的特性:
1:使用指針調(diào)用析構(gòu)
2:operator delete(ptr)?
也就是說(shuō)這時(shí)候是一個(gè)普通調(diào)用?(是什么類型就調(diào)用什么用的析構(gòu)函數(shù)),所以才只會(huì)調(diào)用父類的析構(gòu),但這樣的行為不是我們期望的(存在內(nèi)存泄露),其實(shí),我們期望應(yīng)該是一個(gè)多態(tài)調(diào)用,指向父類調(diào)用父類的析構(gòu),指向子類調(diào)用子類的析構(gòu)函數(shù),只要我們?cè)诟割惖奈鰳?gòu)函數(shù)上加上virtual就可以解決這個(gè)問(wèn)題了。
virtual ~Person()
{cout << " Person delete" << endl;delete[]_p;
}
就是為了讓父類的析構(gòu)函數(shù)為虛函數(shù):?
如果基類的析構(gòu)函數(shù)為虛函數(shù),此時(shí)派生類析構(gòu)函數(shù)只要定義,無(wú)論是否加virtual關(guān)鍵字,
都與基類的析構(gòu)函數(shù)構(gòu)成重寫,雖然基類與派生類析構(gòu)函數(shù)名字不同。雖然函數(shù)名不相同,
看起來(lái)違背了重寫的規(guī)則,其實(shí)不然,這里可以理解為編譯器對(duì)析構(gòu)函數(shù)的名稱做了特殊處
理,編譯后析構(gòu)函數(shù)的名稱統(tǒng)一處理成destructor。
協(xié)變(基類與派生類虛函數(shù)返回值類型不同)
派生類重寫基類虛函數(shù)時(shí),與基類虛函數(shù)返回值類型不同。即基類虛函數(shù)返回基類對(duì)象的指
針或者引用,派生類虛函數(shù)返回派生類對(duì)象的指針或者引用時(shí),稱為協(xié)變。
舉例:
class A
{};class B : public A
{};
class Person
{
public:virtual A* red_packet(){cout << "普通紅包" << endl;return nullptr;}
};class active_people :public Person
{
public:virtual B* red_packet(){cout << "小額紅包" << endl;return nullptr;}
};void fun(Person& p)
{p.red_packet();
}
int main()
{Person p;active_people ap;fun(p);fun(ap);return 0;
}
?這里我們只要簡(jiǎn)單知道在協(xié)變這要求:三同中,返回值可以不同,但是要求返回值必須是一個(gè)父子類關(guān)系的指針或者引用
二、抽象類
1、概念
在虛函數(shù)的后面寫上 =0 ,則這個(gè)函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口
類)。
抽象類不能實(shí)例化出對(duì)象。派生類繼承后也不能實(shí)例化出對(duì)象,只有重寫純虛函數(shù),派生
類才能實(shí)例化出對(duì)象。純虛函數(shù)規(guī)范了派生類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。
那這有什么用呢?
其實(shí)就是強(qiáng)制我們重寫子類的虛函數(shù)
2、?接口繼承和實(shí)現(xiàn)繼承
普通函數(shù)的繼承是一種實(shí)現(xiàn)繼承,派生類繼承了基類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實(shí)
現(xiàn)。
虛函數(shù)的繼承是一種接口繼承,派生類繼承的是基類虛函數(shù)的接口,目的是為了重寫,達(dá)成
多態(tài),繼承的是接口。所以如果不實(shí)現(xiàn)多態(tài),不要把函數(shù)定義成虛函數(shù)。
三、多態(tài)的原理
1、虛函數(shù)表
在了解虛函數(shù)表前,我們先來(lái)看一道筆試題
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}private:int _b = 1;char _ch;
};
我想不少同學(xué)會(huì)認(rèn)為是8,可能認(rèn)為?成員函數(shù)存放在公共的代碼段,我們就只要計(jì)算成員變量大小就可以了,根據(jù)內(nèi)存對(duì)齊就可以得到大小為8個(gè)字節(jié)。
但真的是這樣嗎?
我們打印出類的大小:
cout << sizeof(Base) << endl;
為什么是16個(gè)字節(jié)呢?
int main()
{Base b;cout << sizeof(b) << endl;
}
這時(shí)我們進(jìn)行調(diào)試打開監(jiān)視窗口?
發(fā)現(xiàn)b對(duì)象中存放了_vfptr的東西,這又是什么呢?
其實(shí)是一個(gè)指針, 對(duì)象中的這個(gè)指針我們叫做虛函數(shù)表指針(v代表virtual,f代表function)。一個(gè)含有虛函數(shù)的類中都至少都有一個(gè)虛函數(shù)表指針,因?yàn)?strong>虛函數(shù)的地址要被放到虛函數(shù)表中,虛函數(shù)表也簡(jiǎn)稱虛表。
所以說(shuō)根據(jù)內(nèi)存對(duì)其很容易得到類的大小為16。
那我們?cè)谒伎家粋€(gè)問(wèn)題虛函數(shù)表到底存放在哪里呢?
下面我們通過(guò)一份代碼大致推斷一下:
int main()
{int a = 0;printf("棧:%08lxp \n",& a);const char* str = "hello world";printf("代碼段 / 常量區(qū):%08lxp \n", str);static int b = 0;printf("靜態(tài)區(qū)/數(shù)據(jù)段: %08lxp\n", &b);//取類這前4個(gè)字節(jié)的地址,也就是虛表地址Base be;printf("虛表: %08lxp\n", *((int*)&be));}
通過(guò)打印我們發(fā)現(xiàn)虛表位于代碼段/常量區(qū)中。?
class Base
{
public://父類函數(shù)virtual void Func1(){cout << "Base::Func1()" << endl;}//父類虛函數(shù)virtual void Func2(){cout << "Base::Func2()" << endl;}//父類非虛函數(shù)void Func3(){cout << "Base::Func3()" << endl;}private:int _b = 1;char _ch;
};class Derive : public Base
{
public://子類重寫的虛函數(shù)virtual void Func1(){cout << "Derive::Func1()" << endl;}//非虛函數(shù),重定義void Func3(){cout << "Derive::Func3()" << endl;}
private:int _d = 2;
};
虛表當(dāng)中存儲(chǔ)的就是虛函數(shù)的地址,因?yàn)楦割惍?dāng)中的Func1和Func2都是虛函數(shù),所以父類對(duì)象b的虛表當(dāng)中存儲(chǔ)的就是虛函數(shù)Func1和Func2的地址。
而子類雖然繼承了父類的虛函數(shù)Func1和Func2,但是子類對(duì)父類的虛函數(shù)Func1進(jìn)行了重寫,因此,子類對(duì)象d的虛表當(dāng)中存儲(chǔ)的是父類的虛函數(shù)Func2的地址和重寫的Func1的地址。這就是為什么虛函數(shù)的重寫也叫做覆蓋,覆蓋就是指虛表中虛函數(shù)地址的覆蓋,重寫是語(yǔ)法的叫法,覆蓋是原理層的叫法。
其次需要注意的是:Func2是虛函數(shù),所以繼承下來(lái)后放進(jìn)了子類的虛表,而Func3是普通成員函數(shù),繼承下來(lái)后不會(huì)放進(jìn)子類的虛表。此外,虛函數(shù)表本質(zhì)是一個(gè)存虛函數(shù)指針的指針數(shù)組,一般情況下會(huì)在這個(gè)數(shù)組最后放一個(gè)nullptr。
那我們不由的思考到底虛表是那個(gè)階段就開始初始化?
- 其實(shí)虛表指針是在構(gòu)造函數(shù)初始化列表的時(shí)候填入對(duì)象的,虛表是在編譯的時(shí)候就生成了。
- 虛表里面存放是虛函數(shù)地址,同普通函數(shù)一樣編譯完成就放在代碼段這
- 一個(gè)類這所有的虛函數(shù),都會(huì)放在虛表這。
- 子類會(huì)將父類的虛表拷貝一份,然后用重寫的虛函數(shù)地址覆蓋到原來(lái)虛表中的函數(shù)地址因此虛函數(shù)的重寫,也叫虛函數(shù)的覆蓋。
2、多態(tài)原理剖析
前面我們說(shuō)了那么多,但是虛表是如何實(shí)現(xiàn)多態(tài)的呢?
class Person {
public://父類虛函數(shù)virtual void BuyTicket() { cout << "買票-全價(jià)" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價(jià)" << endl; }
};
void Func(Person* p)
{//通過(guò)調(diào)用父類的引用/指針,指向父類調(diào)用父類,指向子類調(diào)用子類p->BuyTicket();
}
int main()
{Person Mike;Student Johnson;Person* p1 = &Mike;Person* p2 = &Johnson;Func(p1);Func(p2);return 0;
}
- ?p1指針指向Mike對(duì)象,當(dāng)我們調(diào)用Func函數(shù),且將p1傳給Func時(shí),p1->BuyTicket在mike的虛表中找到虛函數(shù)是Person::BuyTicket。
- p2指針指向johnson對(duì)象時(shí)p2->BuyTicket在johson的虛表中找到虛函數(shù)是Student::BuyTicket.
- ?這樣就實(shí)現(xiàn)出了不同對(duì)象去完成同一行為時(shí),展現(xiàn)出不同的形態(tài)
?我們都知道達(dá)到多態(tài),有兩個(gè)條件,一個(gè)是虛函數(shù)覆蓋,一個(gè)是對(duì)象的指針或引用調(diào)
用虛函數(shù),但這是為什么呢?下面我們通過(guò)反匯編了解一下。
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person mike;
Func(&mike);
mike.BuyTicket();
return 0;
}
// 以下匯編代碼中跟你這個(gè)問(wèn)題不相關(guān)的都被去掉了
void Func(Person* p)
{
...
p->BuyTicket();
// p中存的是mike對(duì)象的指針,將p移動(dòng)到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的內(nèi)容,這里相當(dāng)于把mike對(duì)象頭4個(gè)字節(jié)(虛表指針)移動(dòng)到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的內(nèi)容,這里相當(dāng)于把虛表中的頭4字節(jié)存的虛函數(shù)指針移動(dòng)到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虛函數(shù)的指針。這里可以看出滿足多態(tài)的調(diào)用,不是在編譯時(shí)確定的,是運(yùn)行起來(lái)
以后到對(duì)象的中取找的。
001940EA call eax
00頭1940EC cmp esi,esp
}
int main()
{
...
// 首先BuyTicket雖然是虛函數(shù),但是mike是對(duì)象,不滿足多態(tài)的條件,所以這里是普通函數(shù)的調(diào)
用轉(zhuǎn)換成地址時(shí),是在編譯時(shí)已經(jīng)從符號(hào)表確認(rèn)了函數(shù)的地址,直接call 地址
mike.BuyTicket();
00195182 lea ecx,[mike]
00195185 call Person::BuyTicket (01914F6h)
...
}
看出滿足多態(tài)以后的函數(shù)調(diào)用,不是在編譯時(shí)確定的,是運(yùn)行起來(lái)以后到對(duì)象的中取找的。不滿足多態(tài)的函數(shù)調(diào)用時(shí)編譯時(shí)確認(rèn)好的?.
3、單繼承和多繼承關(guān)系的虛函數(shù)表
單繼承的虛函數(shù)表
這里沒什么可以過(guò)多分析,上面我們都是多單繼承虛表的分析,但是我們要注意一個(gè)現(xiàn)象。
class Base
{
public://父類虛函數(shù)virtual void fun1(){cout << "Base::fun1" << endl;}virtual void fun2(){cout << "Base::fun2" << endl;}
private:int a;
};class Derive :public Base
{//重寫的虛函數(shù)virtual void fun1(){cout << "Derive::fun1" << endl;}//虛函數(shù)virtual void fun3(){cout << "Derive::fun3" << endl;}//普通函數(shù)void fun4(){cout << "DERIVE::fun4" << endl;}
private:int b;
};int main()
{Base b;Derive d;return 0;
}
這里雖然從監(jiān)視窗口我們并沒有從虛表中看到fun3虛函數(shù)的地址,但是我們通過(guò)查詢虛表指針的地址,因?yàn)?strong>虛表本質(zhì)是一個(gè)函數(shù)指針數(shù)組,我們可以發(fā)現(xiàn)fun3函數(shù)的地址是存在的,也就說(shuō)明,只要是虛函數(shù)就會(huì)進(jìn)虛表,但是vs編譯器可能會(huì)對(duì)非重寫的虛函數(shù)優(yōu)化,從而在監(jiān)視窗口中我們不能發(fā)現(xiàn)他。
為了更好的驗(yàn)證,我們對(duì)多繼承關(guān)系的虛函數(shù)表的討論,我們寫一個(gè)函數(shù)來(lái)打印虛函數(shù)的地址:
//為函數(shù)指針數(shù)組重新取個(gè)名字
// 寫法1函數(shù)指針數(shù)組
//void PrintVFTbale(VFPtr vft[], int n)
//{
// for (int i = 0; i < n; ++i)
// {
// printf("[%d]:%p\n", i, vft[i]);
// }
//}
typedef void(*VFPtr)();
//寫法2
void PrintVFTbale(VFPtr vft[])
{for (int i = 0; vft[i] != nullptr; ++i){printf("[%d]:%p->", i, vft[i]);vft[i]();}cout << endl;
?
這里我們要注意,要想打印虛表,就要取類這前4個(gè)字節(jié)(32位平臺(tái))/前8個(gè)字節(jié)(64位平臺(tái)),?
我們上面的取法是通用的,(void*)在32位平臺(tái)4個(gè)字節(jié),64位平臺(tái)8個(gè)字節(jié)。而我們*(void**)就自然的找到了void*從而在不同平臺(tái)上形成自適應(yīng)。
多繼承虛表函數(shù)(在vs2013上測(cè)試)
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://父類重寫了func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};// 函數(shù)指針數(shù)組
typedef void(*VFPtr)();
void PrintVFTbale(VFPtr vft[])
{for (int i = 0; vft[i] != nullptr; ++i){printf("[%d]:%p->", i, vft[i]);vft[i]();}cout << endl;
}int main()
{Base1 b1;Base2 b2;PrintVFTbale((VFPtr*)(*(void**)&b1));PrintVFTbale((VFPtr*)(*(void**)&b2));Derive d;PrintVFTbale((VFPtr*)(*(void**)&d));//打印父類的第二張?zhí)摫? 寫法1//PrintVFTbale((VFPtr*)(*(void**)((char*)&d+sizeof(Base1))));//寫法2Base2* ptr2 = &d;PrintVFTbale((VFPtr*)(*(void**)ptr2));return 0;
}
- ?base1h和base2都有一張?zhí)摫?#xff0c;都被Derive繼承(有二張?zhí)摫?,但是沒有被重寫的func3通常是放在第一張?zhí)摫碇械摹?/li>
- 我們?cè)谡腋割惖牡诙執(zhí)摫淼臅r(shí)候,可以通過(guò)字節(jié)偏遠(yuǎn)的方法找到,也可以直接用?? ?Base2* ptr2 = &d;切片的方式自動(dòng)偏移。
- 這里我們打印虛函數(shù)地址結(jié)束調(diào)條件是最后一個(gè)元素為nullptr,但是這種情況在大部分情況下是適應(yīng)的,要注意的是,虛表的具體實(shí)現(xiàn)方式可能因編譯器而異,不同的編譯器可能會(huì)有不同的實(shí)現(xiàn)細(xì)節(jié)。因此,在特定的編譯器和環(huán)境中,虛表的最后一個(gè)元素是否為nullptr可能會(huì)有所不同。但根據(jù)常見的編譯器實(shí)現(xiàn),將最后一個(gè)元素設(shè)為nullptr是一種常見的做法。
四、多態(tài)的其他知識(shí)
1、C++11 override 和 final
在學(xué)習(xí)override和final時(shí),我們先思考一個(gè)問(wèn)題如何實(shí)現(xiàn)一個(gè)不能被繼承的類:
方法1:將構(gòu)造函數(shù)私有(c++98)
class A
{
private:A(){}
};class B : public A
{};
這時(shí)候因?yàn)閷?duì)象對(duì)無(wú)法建立,自然就無(wú)法被繼承
方法2 類定義的時(shí)候加final
這時(shí)候我們稱類為最終類,類不能被繼承。
class A final
{};
其實(shí)final還有一個(gè)功能修飾函數(shù),這該函數(shù)就不能被重寫?
?override: 檢查派生類虛函數(shù)是否重寫了基類某個(gè)虛函數(shù),如果沒有重寫編譯報(bào)錯(cuò)。
2、重載、覆蓋(重寫)、隱藏(重定義)的對(duì)比
重載 | 二個(gè)函數(shù)在同一個(gè)作用域 |
函數(shù)名相同/參數(shù)不同 | |
重定義(隱藏) | 二個(gè)函數(shù)分別在父類和子類的作用域 |
函數(shù)名相同 | |
二個(gè)父類和子類的同名函數(shù)不構(gòu)成重寫就是重定義 | |
重寫(覆蓋) | 二個(gè)函數(shù)分別在父類和子類的作用域 |
函數(shù)名/參數(shù)/返回值必須相同(協(xié)變除外) | |
二個(gè)函數(shù)必須是虛函數(shù) |
3、動(dòng)態(tài)綁定與靜態(tài)綁定
- 靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態(tài)多態(tài),比如:函數(shù)重載.
- 動(dòng)態(tài)綁定又稱后期綁定(晚綁定),是在程序運(yùn)行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動(dòng)態(tài)多態(tài)。
五、分享繼承和多態(tài)常見的面試問(wèn)題
1. 什么是多態(tài)
它指的是同一種操作或接口可以被不同類型的對(duì)象以不同的方式實(shí)現(xiàn)和處理的能力。具體來(lái)說(shuō),多態(tài)性使得我們可以使用基類(父類)的指針或引用來(lái)引用子類(派生類)的對(duì)象,并且根據(jù)具體的對(duì)象類型執(zhí)行對(duì)應(yīng)的方法或操作。
多態(tài)有兩種表現(xiàn)形式:靜態(tài)多態(tài)(編譯時(shí)多態(tài))和動(dòng)態(tài)多態(tài)(運(yùn)行時(shí)多態(tài))。
-
靜態(tài)多態(tài):通過(guò)函數(shù)重載和運(yùn)算符重載實(shí)現(xiàn),編譯器在編譯階段根據(jù)參數(shù)的靜態(tài)類型決定調(diào)用哪個(gè)函數(shù)或操作符。
-
動(dòng)態(tài)多態(tài):通過(guò)虛函數(shù)和基類指針/引用實(shí)現(xiàn),運(yùn)行時(shí)根據(jù)對(duì)象的動(dòng)態(tài)類型來(lái)確定調(diào)用哪個(gè)函數(shù)。即使使用基類指針或引用,也能夠在運(yùn)行時(shí)確定實(shí)際調(diào)用的是子類的方法。
2. 什么是重載、重寫(覆蓋)、重定義(隱藏)?
重載:同一作用域內(nèi),函數(shù)名相同參數(shù)不同
重寫:子類和父類的虛函數(shù),名稱、返回值、參數(shù)都相同,稱子類重寫了父類的虛函數(shù)
重定義:子類和父類的函數(shù)名相同,稱子類隱藏了父類的某個(gè)函數(shù)。
3. 多態(tài)的實(shí)現(xiàn)原理?
父類和子類之中保存的虛表指針是不一樣的,通過(guò)傳入指針或者引用(本質(zhì)也是指針)確定去子類還是父類之中去尋找虛表指針,最后達(dá)到調(diào)用不同虛函數(shù)的目的。?
4. inline函數(shù)可以是虛函數(shù)嗎
?可以,不過(guò)編譯器就忽略inline屬性,這個(gè)函數(shù)就不再是inline,因?yàn)樘摵瘮?shù)要放到虛表中去,如果不構(gòu)成多態(tài)直接調(diào)用,則內(nèi)聯(lián)展開。在類里面定義的函數(shù)默認(rèn)內(nèi)聯(lián)。
5. 靜態(tài)成員可以是虛函數(shù)嗎
?不能,因?yàn)殪o態(tài)成員函數(shù)沒有this指針,使用類型::成員函數(shù)的調(diào)用方式無(wú)法訪問(wèn)虛函數(shù)表,所以靜態(tài)成員函數(shù)無(wú)法放進(jìn)虛函數(shù)表。
6. 構(gòu)造函數(shù)可以是虛函數(shù)嗎
?不能,因?yàn)閷?duì)象中的虛函數(shù)表指針是在構(gòu)造函數(shù)初始化列表階段才初始化的。
?7. 析構(gòu)函數(shù)可以是虛函數(shù)嗎?什么場(chǎng)景下析構(gòu)函數(shù)是虛函數(shù)?
?可以,并且最好把基類的析構(gòu)函數(shù)定義成虛函數(shù)。
通過(guò)基類指針或引用來(lái)管理派生類對(duì)象。如果基類中的析構(gòu)函數(shù)不是虛函數(shù),當(dāng)通過(guò)基類指針或引用刪除派生類對(duì)象時(shí),可能只會(huì)調(diào)用到基類的析構(gòu)函數(shù),而不會(huì)調(diào)用派生類的析構(gòu)函數(shù),從而導(dǎo)致派生類可能存在資源泄漏或未被正確清理的問(wèn)題
8. 對(duì)象訪問(wèn)普通函數(shù)快還是虛函數(shù)更快?
?先如果是普通對(duì)象,是一樣快的。如果是指針對(duì)象或者是引用對(duì)象,則調(diào)用的普通函數(shù)快,因?yàn)闃?gòu)成多態(tài),運(yùn)行時(shí)調(diào)用虛函數(shù)需要到虛函數(shù)表中去查找。
9. 虛函數(shù)表是在什么階段生成的,存在哪的?
?虛函數(shù)表是在編譯階段就生成的,一般情況下存在代碼段(常量區(qū))的。
10. C++菱形繼承的問(wèn)題?虛繼承的原理??
?菱形虛擬繼承因?yàn)樽宇悓?duì)象當(dāng)中會(huì)有兩份父類的成員,因此會(huì)導(dǎo)致數(shù)據(jù)冗余和二義性的問(wèn)題。
虛繼承對(duì)于相同的虛基類在對(duì)象當(dāng)中只會(huì)存儲(chǔ)一份,若要訪問(wèn)虛基類的成員需要通過(guò)虛基表獲取到偏移量,進(jìn)而找到對(duì)應(yīng)的虛基類成員,從而解決了數(shù)據(jù)冗余和二義性的問(wèn)題。
11.什么是抽象類?抽象類的作用?
在虛函數(shù)的后面寫上 =0 ,則這個(gè)函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類?。
抽象類強(qiáng)制重寫了虛函數(shù),另外抽象類體現(xiàn)出了接口繼承關(guān)系。