一、多態的概念
? ? ? ?多態的概念:通俗來說,就是多種形態,具體點就是去完成某個行為,當不同的對象去完成時會產生出不同的狀態。
? ? ? ?那究竟多態的實際價值體現在哪里呢??
1、舉個例子比如說購買高鐵票這個行為,如果是普通人就是原價購買,如果是學生的話就是半價購買,如果是軍人的話,可以優先走綠色通道購買……
2、再舉個例子比如說大數據殺熟(個人看法,不一定正確,只是為了方便解釋多態)
(1)在線支付市場,如果你平時經常用微信而很少用支付寶,那么在支付寶舉辦一些類似領取紅包的活動的時候,他可能會有相關的算法去分析你的賬戶信息,對于很少用支付寶的用戶,可能相對來說得到的紅包金額就會更大,這是為了鼓勵你去使用支付寶,可能你某一天在商場購物的時候,想起來自己有個紅包沒用,就會放棄使用微信而轉而使用支付寶。同樣是掃碼動作,不同的用戶掃得到的不一樣的紅包,這也是一種多態行為。
(2)游戲抽卡相信大家也體驗過,充錢充的越少的可能反而抽卡的運氣會更好,這樣會使得你不至于跟氪佬的差距特別大,鼓勵你繼續玩游戲。而充值充得多可能運氣就會越不好,因為你不缺錢,同樣是抽卡,不同的玩家抽卡概率不同,這也是一種多態行為。
總而言之就是,我們生活中一件事情不同的群體去做需要有不同的反饋,那這就是多態!!
?二、多態的定義和實現
2.1 構成多態的條件
首先多態現象的產生是在繼承的基礎上產生的。
? ? ? ?多態是在不同繼承關系的類對象,去調用同一函數,產生了不同的行為。比如Student繼承了Person。Person對象買票全價,Student對象買票半價。
? ?構成多態需要以下兩個條件(重點):
(1)對父類虛函數的重寫->三同(函數名、參數、返回值)
(2)必須是父類的指針或者引用去調用
?是否構成多態的不同表現:
1、不滿足多態 -- 看調用者的類型,調用這個類型的成員函數
2、滿足多態 -- 看指向的對象的類型,調用這個類型的成員函數
?滿足多態:
?不滿足多態:
思考:你可能會有這樣的疑惑->我直接用在函數體里面用if……else不也可以達到這樣的效果嗎??為什么非得用多態來完成呢????
——>答:有些場景下必須得用多態才能解決,比如父類的指針或者引用調用析構函數
? ? ? ? 但是由于父類的指針或引用是可以指向子類的對象的,甚至在某些場景下子類的指針或引用也可以指向父類的對象(前提是父類的對象被子類對象給賦值過) ,如果沒有發生多態的話,那么就會去看調用者的類型而不是去看指向對象的類型,從而導致指向對象沒有被析構,造成內存泄露。
加了virtual之后,就可以解決這個問題了。
?綜上我們可以發現,if……else并不能替代多態!!!
2.2 虛函數的重寫
虛函數:即被virtual修飾的類成員函數稱為虛函數
虛函數的重寫(覆蓋):派生類中有一個跟基類完全相同的虛函數(即派生類虛函數與基類虛函數的返回值類型、函數名字、參數列表完全相同),稱子類的虛函數重寫了基類的虛函數。
?虛函數重寫有兩個例外:
(1)協變(返回值可以不同->但是必須是父子關系的指針或者引用(其實不一定是自己的父子類,其他的父子類也行))
class Person {
public:virtual Person* f() { return this; }
};
class Student : public Person {
public:virtual Student* f() { return this; }
};int main()
{return 0;
}
返回值也可以是其他父子類的指針或者引用,也可以是協變。
class A {};
class B : public A {};
class Person {
public:virtual A* f() { return nullptr; }
};
class Student : public Person {
public:virtual B* f() { return nullptr; }
};int main()
{return 0;
}
(2)子類的virtual可以省略 ?
? ? ? ? 因為虛函數的重寫本身就是接口繼承 ? ?我把除函數體以外的全部部分都可以繼承下來,然后再去重寫繼承父類的這個函數的實現 ?這樣只要父類寫了virtual就可以了。
? ? ? ?我們來看一道經典的題目
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;
}
該題的變形:
class A
{
public:virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }
};class B : public A
{
public:void func(int val = 0){ std::cout << "B->" << val << std::endl; }virtual void test(){ func(); }
};int main(int argc, char* argv[])
{B*p = new B;p->test();return 0;
}
(3)析構函數的重寫(基類與派生類析構函數的名字不同)
? ? ? ? 如果基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。雖然函數名不相同,看起來違背了重寫的規則,其實不然,這里可以理解為編譯器對析構函數的名稱做了特殊處理,編譯后析構函數的名稱統一處理成destructor。
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生類Student的析構函數重寫了Person的析構函數,下面的delete對象調用析構函
//數,才能構成多態,才能保證p1和p2指向的對象正確的調用析構函數。
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}
?2.3?C++11 override 和 final
? ? ? ?C++對函數重寫的要求比較嚴格,但是有些情況下由于疏忽,可能會導致函數名字母次序寫反而無法構成重載,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有得到預期結果才來debug會得不償失,因此:C++11提供了override和final兩個關鍵字,可以幫助用戶檢測是否重寫。
1. final:修飾虛函數,表示該虛函數不能再被重寫(實際上這樣的應用場景很少,因為我們建立虛函數的目的基本上都是為了重寫)
2. override: 檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯。
下面這種場景下,我們父類漏寫了virtual,但是這樣并不會報錯
?
?但是我們加了override 他就會提示你
? ? ? ?總的來說,override就相當于是對前面語法的一個填坑,因為按道理來說虛函數的意義就是要為了重寫而生的,而沒有重寫就失去了意義,最好的方法其實是讓編譯器對沒重寫的虛函數進行報錯,但是之前在這方面沒有去嚴格地限制說不重寫就會報錯,所以這邊做了一個妥協就是你可以通過增加override來幫助你檢查,防止你寫漏,
2.4 重載、覆蓋(重寫)、隱藏(重定義)的對比
三、抽象類
3.1 什么是抽象類
? ? ? ? 在虛函數的后面寫上 =0 ,則這個函數為純虛函數。包含純虛函數的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生類才能實例化出對象。純虛函數規范了派生類必須重寫,另外純虛函數更體現出了接口繼承。
? ? ? ? 在生活中一個類型在現實中沒有對應的實體,我們就可以一個類定義為抽象類!
1、抽象類不能實例化出對象?
2、子類必須重新父類的虛函數(不重寫的話自己也無法實例化)
? ? ? ?總的來說,純虛函數強制子類必須重寫虛函數!相當于是一種強制性的要求! 如果不重寫的話,代價就是自己也和抽象類父類一樣無法實例化出對象!!
?3.2 理解接口繼承和實現繼承
? ? ? ? ?普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數(充分說明了多態的意義就是為了重寫虛函數而生的!!!)
四、多態的底層原理? ?
4.1 虛函數表
? ? ? ? ?接下來我們要從原理層去剖析多態具體是如何構成的。
在這之前,我們來看一道題目:sizeof(Base)是多少?
class Base{public:virtual void Func1(){cout << "Func1()" << endl;}private:int _b = 1;char _ch;};int main(){cout << sizeof(Base) << endl;Base bb;return 0;}
根據以前對于內存對齊的學習,你可以很快就猜到是8,但是答案是12(x86環境)
?原因就是由于有了虛函數,所以就出現了一個虛函數表指針!!
? ?? ? 除了_b成員,還多一個__vfptr放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關),對象中的這個指針我們叫做虛函數表指針(v代表virtual,f代表function)。一個含有虛函數的類中都至少都有一個虛函數表指針,因為虛函數的地址要被放到虛函數表中,虛函數表也簡稱虛表。那么派生類中這個表放了些什么呢?我們接著往下分析:
// }
class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }virtual void Func() { cout << "買票-全價" << endl; }int _a = 0;};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }int _b = 1;
};
void Func(Person*p)
{p->BuyTicket();
}int main()
{Person* p1 = new Person;Person* p2 = new Student;return 0;
}
?從上的分析我們可以總結出:
1、虛函數表(簡稱虛表)本質上是一個虛函數指針數組
2、子類在創建對象的時候會拷貝父類的虛函數表,然后如果沒有發生重寫(比如Func())?那虛函數表存的就是父類的虛函數,如果發生了重寫(比如BuyTicket()),那么子類首先會繼承父類的接口,然后重寫父類的實現,然后用這個虛函數的地址覆蓋掉原先拷貝父類虛函數的位置。
3、通過對2的分析,我們可以知道(1)重寫是語法層的概念,如果發生了多態,那么子類會繼承父類的接口,然后重寫父類的實現。(2)覆蓋是原理層的概念,當子類重寫了父類的實現后,會將虛表中對應的該函數的地址更新成新的虛函數地址。
4.2 多態的原理
? ? ? 那么虛函數表究竟是如何幫助我們實現多態的呢???在研究這個問題之前,我們來看一下這個匯編
class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }virtual void Func() { cout << "買票-全價" << endl; }int _a = 0;};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }int _b = 1;
};
void Func(Person&p)
{p.BuyTicket();
}
int main()
{Person mike;Func(mike);Student johnson;Func(johnson);return 0;
}
? ? ?通過上圖的分析我們可以知道:
(1)在沒有發生多態的時候,相當于一個普通函數的調用,是在編譯時就確定了的。?
(2)如果發生了多態,那么編譯器只能通過找到對應的虛函數表的位置,然后虛函數表里面存的函數是什么,就調用什么。??
? ? ? 這充分說明了一個道理就是,多態并不是在編譯時就確定的,編譯器只是知道自己需要調用的函數在什么地方,但是具體這個函數是什么,他并不知道,只有等到匯編代碼轉成二進制代碼后執行了那么才會知道調用的是什么!!!
?我們來看看下面代碼的運行結果:
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;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}virtual void Func4(){cout << "Derive::Func4()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;
}
? ? ? ?b按道理來說應該有三個虛函數,但是在監視窗口只能看到兩個,原因是監視窗口其實是被處理過的,所以我們看到的并不是真的。既然監視窗口看不了,我們就去看看內存窗口
? ? ? 所以我們可以得到一個結論,監視窗口其實是被加以修飾的,不一定能夠準確地看到底層信息,相比之下內存更為純粹?,可以看到更加真實的情況。
? ? 但是內存函數也是也不太方便我們去觀察,所以最好的方法是想辦法把虛函數表打印出來!!
?4.3 實現虛函數表的打印
1、首先我們要先封裝一個打印虛表的函數PrintVFTable
? ? ? ?虛函數表本質上是一個虛函數指針數組,存的都是void(*)()類型的函數指針,為了方便我們書寫函數指針以及可讀性,我們用typedef去幫助我們重命名這個void(*)()變成VF_PTR。虛函數表里面存的是一個個VF_PTR類型,那么我們虛表指針就是VF_PTR*類型。這就是我們要傳遞的參數。
? ? ? 接下來就是打印這個虛函數的指針數組,但是我們并不知道這個指針數組有多大,但是在VS下,虛函數表的指針末尾默認都有一個nullptr,所以我們可以用這個來充當一個結束條件。
? ? ? 那么根據上面的分析,我們就可以封裝出一個通過傳虛函數指針來打印虛表的一個函數如下:
typedef void(*VF_PTR)(); //將函數指針重命名為VF_PTR
void PrintVFTable(VF_PTR* table)
{for (int i = 0; table[i] != nullptr; ++i)printf("[%d]:%p->", i, table[i]);cout << endl;
}
?2、那么接下來的問題就是我們如何在這個對象去找到這個虛函數指針??
? ? ? 在學習大小端的時候,我們解決判斷機器大小端的問題有2個方法,一個是用聯合體,另一個是暴力取到第一個字節,這個方法本質上就是將一個int*類型強轉成char*來得到的。因為指針的類型決定解引用看多大字節。
? ? ?類比這題,假設是32位的環境,那么我們通過調試可以知道虛函數指針在對象的頭四個字節,所以首先我們要取得這個地址,然后再將他強轉成int類型,然后再解引用,就可以拿到該位置的函數指針了!!即? *(int*)&d? 但是還有一個問題就是我們的接口是VF_PTR*類型,所以我們還得將其強轉成VF_PTR*才能傳過去? ? ? 所以調用的方法為PrintVFTable((VF_PTR*)(*(int*)&d))
? ? ? ?但是這個只有在32位的環境下才有效,如果換成64位,代碼就不成立了。那么還有一種調用方法可以解決這個問題。?
通過上圖分析可以得到的調用方式:?PrintVFTable(*(VF_PTR**)&d)
?3、如何查看對應地址的函數是什么???
? ? ? ? ? 我們可以在打印虛表的時候順便通過這個函數指針去調用一下這個函數,只需要對PrintVFTable函數修改一下。
typedef void(*VF_PTR)(); //將函數指針重命名為VF_PTR
void PrintVFTable(VF_PTR* table)
{for (int i = 0; table[i] != nullptr; ++i){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}
?通過打印虛表,我們可以更好地觀察結果。
總結一下派生類的虛表生成過程:
a.先將基類中的虛表內容拷貝一份到派生類虛表中
b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數
c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后
?在C++中,常常通過聲明順序來代表實際順序,比如說
1、初始化列表的初始化順序取決于成員變量定義的先后順序
2、一個子類繼承多個父類的時候,內存中先繼承的父類的成員變量在前面,后繼承的父類的成員變量再后面,然后才是子類自己新的成員變量。
3、一個類有多個虛函數的時候,虛函數表的存儲也是先聲明的虛函數在前面,然后子類繼承后創建的虛表也是先是父類的虛函數,然后才是自己新增的虛函數
4.4 多態的思考
1、虛表是什么階段生成的??——>編譯階段
? ? ? 一個類是否存在虛表關鍵就在于是否有虛函數,而是否有虛函數取決于是否有virtual關鍵字,所以是在編譯階段生成。
2、對象中的虛表指針是什么時候初始化的??——>初始化列表階段
通過代碼觀察:
? ? ? ?b還沒初始化的時候就已經存在虛表了,說明虛表是早于構造函數的。那么如果我們將_b的初始化放在初始化列表階段。
? ? ? ?上圖可以證明虛表指針是在初始化列表階段初始化的。?
3、虛表是存在哪里的??——>代碼段(常量區)
? ? ? ? ? 易錯點:有虛函數的對象里面存的其實是指向虛表的指針,并不是虛表!!!
通過監視窗口其實可以大致推斷出來
因為我們知道虛函數是存在?代碼段(常量區)而虛表的地址和虛函數的地址其實挺接近的。
但是這樣其實不夠嚴謹,所以我們自己來寫一個代碼判斷虛表具體是存在哪個區域。
int main()
{Base b;Derive d;int x = 0;static int y = 0;int* z = new int;const char* p = "xxxxxxxxxxxxxxxxxx";printf("棧對象:%p\n", &x);printf("堆對象:%p\n", z);printf("靜態區對象:%p\n", &y);printf("常量區對象:%p\n", p);printf("b對象虛表:%p\n", *((VF_PTR**)&b));printf("d對象虛表:%p\n", *((VF_PTR**)&d));return 0;
}
因此可以得出虛表是存在??代碼段(常量區)。
?4、反向分析規則:子類也可以賦值給父類,但為什么這樣不能構成多態??——>因為父類對象不敢隨意拷貝子類虛表
? ? ? ? 無論是父類的指針或者引用,其本質上指代子類中所繼承的父類的那一塊區域,子類有子類的虛表,父類有父類的虛表,互不影響。但是如果是賦值,雖然也可以把父類那部分切片切過去,但是編譯器不敢把子類的虛表也拷貝過去,因為父類自己也有一個虛表,如果隨意拷貝的話我們就分不清這究竟是一個person類還是student類!!
4.5?動態綁定與靜態綁定
1. 靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態(編譯時),比如:函數重載
2. 動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體行為,調用具體的函數,也稱為動態多態(運行時)。
五、多繼承的虛函數表
5.1 子類新增的虛函數
我們來看看這樣一段代碼
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:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};
? ? ? ? 如果我們實例化出Derive對象d,那么d必然繼承了兩個虛表,那在這種情況下,如上圖我們在子類多增加了一個func3()的一個虛函數,那這個新增的虛函數是存在第一個虛表還是第二個虛表呢?????
? ? ?我們可以通過內存窗口來觀察
? ? ? 接下里我們通過打印虛表來觀察
? ? ? ?第一張虛表可以沿用第單繼承的虛表打印的調用,使用PrintVFTable((VF_PTR*)(*(int*)&d))或者?PrintVFTable(*(VF_PTR**)&d)??
? ? ? ?而第二張虛表則需要先跳過base1對象大小的字節,才能找到base2的虛表,但是如果我們在d的地址上加上sizeof(Base1)的話,由于d是一個Derive對象,那么+1會跳過Derive對象對象的大小,并不符合我們的要求,因此我們需要先將&b強轉成char*類型去進行指針偏移,然后再進行操作。因此調用方法可以是PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))))或者是PrintVFTable(*(VF_PTR**)((char*)&d+sizeof(Base1)))
?還有另外一種方法,在使用這個方法之前,我們先來看看一道多繼承中指針偏移問題
下面說法正確的是( )?A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
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;
}
? ? ? ? 所以打印Base2的虛表也可以利用一個Base2的指針幫助我們自動定位在d中的位置!!
總結:
(1)父類的指針或引用指向子類對象的時候會自動定位到其繼承了自己的那部分區域!!
(2)多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中!!
?5.2 底層剖析
?我們會發現虛繼承后,虛表中重寫的func1的地址不同!!!下面我們來分析底層的原因——
? ? ? ?通過匯編我們可以知道,多態并不是直接去調用對應的函數,而是要先通過對象的this指針去拿到對象的虛函數指針,然后通過虛函數指針再去找對應的函數。而ptr1恰好是this指針的位置,所以就可以直接拿到base1對象的虛表指針然后找到虛表,而ptr2必須要將this指針修正sizeof(base1)的大小才能拿到對應的虛函數!! 所以地址不相同的原因是編譯器需要去通過其封裝的方法來修正this指針,這樣才能正確地取到對象的虛函數指針,實現多繼承的多態。
思考題:下面代碼中的ptr->func1()是多態調用嗎??
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}void func2(){cout << "func2" << endl;}
};int main()
{// 多態調用 -- 去虛表中找虛函數地址A* ptr = &aa;ptr->func1();return 0;
}
? 思考:?我們會發現這里并沒有出現繼承和虛函數的重寫,但是還是出現了多態調用!!!為什么呢???
? ? ??答:因為編譯器做的是一種傻瓜式的判斷(因為如果編譯器還要判斷是否出現繼承和重寫的話代價太高了),并且其實只要發現滿足多態的兩個條件,即(1)必須是父類的指針或者引用去調用。(2)是一個虛函數。 就會把他推斷成多態(多態調用的本質是能夠在虛表中找得到,即使沒有重寫的虛函數也會進虛表,那么調用的話就是多態調用。)
再看看下面的代碼:
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}void func2(){cout << "func2" << endl;}
};
class B : public A
{
public:virtual void func1(){cout << "B::func1" << endl;}void func2(){cout << "func2" << endl;}
};
int main()
{A aa;// 多態調用 -- 去虛表中找虛函數地址A* ptr = &aa;ptr->func1();
}
? ? ? ? 此時我們可以證明是否出現繼承和重寫并不是多態最本質的條件,只要是父類的指針或者引用去調用,并且該虛函數可以在虛表中找得到,那么就構成了多態調用!!!(所以不管是否重寫編譯器都會按照多態調用去走)。之所以要強調要完成虛函數的重寫,是因為只有虛函數重寫了才有實際意義,可以看得出來。
? ? ?總結:多態存在的意義就是得重寫虛函數,但是底層多態的調用只要是父類的指針和引用對應的虛表找得到,就會出現多態的調用,只不過不重寫的話表面上很難觀察并且也就失去意義了!!? ?所以我們要將多態的現象(需要兩個調用去對比)和多態的調用(原理層)區分開來!!
?5.3 菱形虛擬繼承
class A
{
public:virtual void func1(){}
public:int _a;
};class B : virtual public A
{
public:virtual void func1(){}virtual void func2(){}
public:int _b;
};class C : virtual public A
{
public:virtual void func1(){}virtual void func3(){}
public:int _c;
};class D : public B, public C
{
public:virtual void func1(){}
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
?總結:
(1)菱形虛擬繼承的D對象會有三張虛表,分別是A/B/C的,其中由于虛繼承的存在,所以A虛表指針以及其成員變量會被存在公共部分。
(2)虛基表的第一行存的偏移量可以找到其虛函數表指針,而第二行存的偏移量可以找到公共部分
? ? ? 實際中我們不建議設計出菱形繼承及菱形虛擬繼承,一方面太復雜容易出問題,另一方面這樣的模型,訪問基類成員有一定得性能損耗。
附上兩篇文章:
C++ 虛函數表解析 | 酷 殼 - CoolShell
C++ 對象的內存布局 | 酷 殼 - CoolShell
六、繼承和多態的相關面試題
1. 什么是多態?
2. 什么是重載、重寫(覆蓋)、重定義(隱藏)?
3. 多態的實現原理?
4. inline函數可以是虛函數嗎?
答:可以,不過為了能夠將該虛函數放進虛表中,編譯器會忽略掉inline的特性,因為本身inline就是一個建議。在某些函數必須出現在代碼段(常量區)的時候,inline會妥協并失去作用
5. 靜態成員可以是虛函數嗎?
答:不能,因為靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。
6. 構造函數、拷貝構造可以是虛函數嗎?
答:不能,因為對象中的虛函數表指針是在構造函數初始化列表階段才初始化的。
7、賦值重載可以是虛函數嗎???
答:可以,但是不建議,因為這樣的話如果沒有觸發多態,父類的賦值重載就會被隱藏,子類就調用不了父類的賦值重載了。
8. 析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?
答:可以,并且最好把基類的析構函數定義成虛函數。因為析構函數都會被編譯器處理為destruct,如果沒有多態會使得父類的析構函數被隱藏。而有多態的話子類可以重寫父類的析構函數,這樣子類就調子類的析構,父類調父類的析構,互相不會沖突更不會造成內存泄露。
9. 對象訪問普通函數快還是虛函數更快?
答:不一定,因為虛函數只不過是構成多態的條件之一,如果其他條件沒滿足的話其實也相當于普通對象。如果是普通對象,是一樣快的。如果是父類指針對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找。
10. 虛函數表是在什么階段生成的,存在哪的?
答:虛函數表是在編譯階段就生成的,一般情況下存在代碼段(常量區)的。
11. C++菱形繼承的問題?虛繼承的原理?
12. 什么是抽象類?抽象類的作用?
答:抽象類強制子類重寫了虛函數,另外抽象類體現出了接口繼承關系。
13.虛基表和虛函數表的區別
?答:虛函數表存的是虛函數的地址,是為了多態的實現。而虛基表存的是偏移量,是為了解決數據冗余和二義性。