C++的多態(tài)和虛函數(shù)你真的了解嗎
一、C++的面試??键c(diǎn)
阿里雖然是國內(nèi)Java的第一大廠但是并非所有的業(yè)務(wù)都是由Java支撐,很多服務(wù)和中下層的存儲(chǔ),計(jì)算,網(wǎng)絡(luò)服務(wù),大規(guī)模的分布式任務(wù)都是由C++編寫。在阿里所有部門當(dāng)中對(duì)C++考察最深的可能就是阿里云。
阿里對(duì)C++的常考點(diǎn):
1.STL 容器相關(guān)實(shí)現(xiàn)
2.C++新特性的了解
3.多態(tài)和虛函數(shù)的實(shí)現(xiàn)
4.指針的使用
二、阿里真題
2.1 真題一
現(xiàn)在假設(shè)有一個(gè)編譯好的C++程序,編譯沒有錯(cuò)誤,但是運(yùn)行時(shí)報(bào)錯(cuò),報(bào)錯(cuò)如下:你正在調(diào)用一個(gè)純虛函數(shù)(Pure virtual function call error),請(qǐng)問導(dǎo)致這個(gè)錯(cuò)誤的原因可能是什么?
純虛函數(shù)調(diào)用錯(cuò)誤一般由以下幾種原因?qū)е拢?/strong>
- 從基類構(gòu)造函數(shù)直接調(diào)用虛函數(shù)。(直接調(diào)用是指函數(shù)內(nèi)部直接調(diào)用虛函數(shù))
- 從基類析構(gòu)函數(shù)直接調(diào)用虛函數(shù)。
- 從基類構(gòu)造函數(shù)間接調(diào)用虛函數(shù)。(間接調(diào)用是指函數(shù)內(nèi)部調(diào)用其他的非虛函數(shù),其內(nèi)部直接或間接地調(diào)用了虛函數(shù))
- 從基類析構(gòu)函數(shù)間接調(diào)用虛函數(shù)。
- 通過懸空指針調(diào)用虛函數(shù)。
注意:其中1,2編譯器會(huì)檢測到此類錯(cuò)誤。3,4,5編譯器無法檢測出此類情況,會(huì)在運(yùn)行時(shí)報(bào)錯(cuò)。
(1)虛函數(shù)表vtbl
編譯器在編譯時(shí)期為每個(gè)帶虛函數(shù)的類創(chuàng)建一份虛函數(shù)表
實(shí)例化對(duì)象時(shí), 編譯器自動(dòng)將類對(duì)象的虛表指針指向這個(gè)虛函數(shù)表
(2)構(gòu)造一個(gè)派生類對(duì)象的過程
1.構(gòu)造基類部分:
- 構(gòu)造虛表指針,將實(shí)例的虛表指針指向基類的vtbl
- 構(gòu)造基類的成員變量
- 執(zhí)行基類的構(gòu)造函數(shù)函數(shù)體
2.遞歸構(gòu)造派生類部分:
- 將實(shí)例的虛表指針指向派生類vtbl
- 構(gòu)造派生類的成員變量
- 執(zhí)行派生類的構(gòu)造函數(shù)體
(3)析構(gòu)一個(gè)派生類對(duì)象的過程
1.遞歸析構(gòu)派生類部分:
- 將實(shí)例的虛表指針指向派生類vtbl
- 執(zhí)行派生類的析構(gòu)函數(shù)體
- 析構(gòu)派生類的成員變量(這里的執(zhí)行函數(shù)體,析構(gòu)派生類成員變量,兩者的順序和構(gòu)造的步驟是相反的)
2.析構(gòu)基類部分:
- 將實(shí)例的虛表指針指向基類的vtbl
- 執(zhí)行基類的析構(gòu)函數(shù)函數(shù)體
- 析構(gòu)基類的成員變量
構(gòu)造函數(shù)和析構(gòu)函數(shù)執(zhí)行函數(shù)體時(shí),實(shí)例的虛函數(shù)表指針,指向構(gòu)造函數(shù)和析構(gòu)函數(shù)本身所屬的類的虛函數(shù)表,此時(shí)執(zhí)行的虛函數(shù),即調(diào)用的本身的該類本身的虛函數(shù),下面是一個(gè)【間接調(diào)用】的栗子:基類中的析構(gòu)函數(shù)中,調(diào)用純虛函數(shù)(該虛函數(shù)就在基類中定義)。
#include <iostream>
using namespace std;
class Parent {
public:
//純虛函數(shù)
virtual void virtualFunc() = 0;
void helper() {
virtualFunc();
}
virtual ~Parent(){
helper();
}
};
class Child : public Parent{
public:
void virtualFunc() {
cout << "Child" << endl;
}
virtual ~Child(){}
};
int main() {
Child child;
//system("pause");
return 0;
}
運(yùn)行時(shí)報(bào)錯(cuò):libc++abi.dylib: Pure virtual function called:

2.2 真題二
在構(gòu)造實(shí)例過程當(dāng)中一部分是初始化列表一部分是在函數(shù)體內(nèi),你能說一下這些的順序是什么?差別是什么和this指針構(gòu)造的順序
順序:
(1)初始化列表中的先初始化。
(2)執(zhí)行函數(shù)體代碼。
- 執(zhí)行類中函數(shù)體,如執(zhí)行構(gòu)造函數(shù)時(shí),所有成員已經(jīng)初始化完畢了;
this指針屬于對(duì)象,而對(duì)象還沒構(gòu)造完成前,若使用this指針,編譯器會(huì)無法識(shí)別。在初始化列表中顯然不能使用this指針,注意:在構(gòu)造函數(shù)體內(nèi)部可以使用this指針。
構(gòu)造函數(shù)的執(zhí)行可以分成兩個(gè)階段:
- 初始化階段:所有類類型的成員都會(huì)在初始化階段初始化,即使該成員沒有出現(xiàn)在構(gòu)造函數(shù)的初始化列表中。
- 計(jì)算賦值階段:一般用于執(zhí)行構(gòu)造函數(shù)體內(nèi)的賦值操作。
#include <iostream>
using namespace std;
class Test1 {
public:
Test1(){
cout << "Construct Test1" << endl;
}
//拷貝構(gòu)造函數(shù)
Test1& operator = (const Test1& t1) {
cout << "Assignment for Test1" << endl;
this->a = t1.a;
return *this;
}
int a ;
};
class Test2 {
public:
Test1 test1;
//Test2的構(gòu)造函數(shù)
Test2(Test1 &t1) {
cout << "構(gòu)造函數(shù)體開始" << endl;
test1 = t1 ;
cout << "構(gòu)造函數(shù)體結(jié)束" << endl;
}
};
int main() {
Test1 t1;
Test2 test(t1);
system("pause");
return 0;
}

分析上面的結(jié)果:
(1)第一行結(jié)果即Test t1實(shí)例化對(duì)象時(shí),執(zhí)行Test1的構(gòu)造函數(shù);
(2)第二行代碼,實(shí)例化Test2對(duì)象時(shí),在執(zhí)行Test2構(gòu)造函數(shù)時(shí),正如上面所說的,構(gòu)造函數(shù)的第一步是初始化階段:所有類類型的成員都會(huì)在初始化階段初始化,即使該成員沒有出現(xiàn)在構(gòu)造函數(shù)的初始化列表中。所以Test2在構(gòu)造函數(shù)體執(zhí)行之前已經(jīng)使用了Test1的默認(rèn)構(gòu)造函數(shù)初始化好了t1。打印出Construct Test1。
這里的拷貝構(gòu)造函數(shù)中可以使用
this指針,指向當(dāng)前對(duì)象。
(3)第三四五行結(jié)果:執(zhí)行Test2的構(gòu)造函數(shù)。
2.3 真題三
初始化列表的寫法和順序有沒有什么關(guān)系?
構(gòu)造函數(shù)的初始化列表中的前后位置,不影響實(shí)際標(biāo)量的初始化順序。成員初始化的順序和它們在類中的定義順序一致。
必須使用初始化列表的情況:數(shù)據(jù)成員是const、引用,或者屬于某種未提供默認(rèn)構(gòu)造函數(shù)的類類型。
2.4 真題四
在普通的函數(shù)當(dāng)中調(diào)用虛函數(shù)和在構(gòu)造函數(shù)當(dāng)中調(diào)用虛函數(shù)有什么區(qū)別?
普調(diào)函數(shù)當(dāng)中調(diào)用虛函數(shù)是希望運(yùn)行時(shí)多態(tài)。而在構(gòu)造函數(shù)當(dāng)中不應(yīng)該去調(diào)用虛函數(shù)因?yàn)闃?gòu)造函數(shù)當(dāng)中調(diào)用的就是本類型當(dāng)中的虛函數(shù),無法達(dá)到運(yùn)行時(shí)多態(tài)的作用。
2.5 真題五
成員變量,虛函數(shù)表指針的位置是怎么排布?
如果一個(gè)類帶有虛函數(shù),那么該類實(shí)例對(duì)象的內(nèi)存布局如下:
- 首先是一個(gè)虛函數(shù)指針,
- 接下來是該類的成員變量,按照成員在類當(dāng)中聲明的順序排布,整體對(duì)象的大小由于內(nèi)存對(duì)齊會(huì)有空白補(bǔ)齊。
- 其次如果基類沒有虛函數(shù)但是子類含有虛函數(shù):
- 此時(shí)內(nèi)存子類對(duì)象的內(nèi)存排布也是先虛函數(shù)表指針再各個(gè)成員。
如果將子類指針轉(zhuǎn)換成基類指針此時(shí)編譯器會(huì)根據(jù)偏移做轉(zhuǎn)換。在visual studio,x64環(huán)境下測試,下面的Parent p = Child();是父類對(duì)象,由子類來實(shí)例化對(duì)象。
#include <iostream>
using namespace std;
class Parent{
public:
int a;
int b;
};
class Child:public Parent{
public:
virtual void test(){}
int c;
};
int main() {
Child c = Child();
Parent p = Child();
cout << sizeof(c) << endl;//24
cout << sizeof(p) << endl;//8
Child* cc = new Child();
Parent* pp = cc;
cout << cc << endl;//0x7fbe98402a50
cout << pp << endl;//0x7fbe98402a58
cout << endl << "子類對(duì)象abc成員地址:" << endl;
cout << &(cc->a) << endl;//0x7fbe98402a58
cout << &(cc->b) << endl;//0x7fbe98402a5c
cout << &(cc->c) << endl;//0x7fbe98402a60
system("pause");
return 0;
}
結(jié)果如下:
24
8
0000013AC9BA4A40
0000013AC9BA4A48子類對(duì)象abc成員地址:
0000013AC9BA4A48
0000013AC9BA4A4C
0000013AC9BA4A50
請(qǐng)按任意鍵繼續(xù). . .
分析上面的結(jié)果:
(1)第一行24為子類對(duì)象的大小,首先是虛函數(shù)表指針8B,然后是2個(gè)繼承父類的int型數(shù)值,還有1個(gè)是該子類本身的int型數(shù)值,最后的4是填充的。
(2)第二行的8為父類對(duì)象的大小,該父類對(duì)象由子類初始化,含有2個(gè)int型成員變量。
(3)子類指針cc指向又new出來的子類對(duì)象(第三個(gè)),然后父類指針pp指向這個(gè)子類對(duì)象,這兩個(gè)指針的值:
- 父類指針
pp值:0000013AC9BA4A48 - 子類指針
cc值:0000013AC9BA4A40
即發(fā)現(xiàn)如之前所說的:如果將子類指針轉(zhuǎn)換成基類指針此時(shí)編譯器會(huì)根據(jù)偏移做轉(zhuǎn)換。我測試環(huán)境是64位,所以指針為8個(gè)字節(jié)。轉(zhuǎn)換之后pp和cc相差一個(gè)虛表指針的偏移。
(4)&(cc->a)的值即 0000013AC9BA4A48,和pp值是一樣的,注意前面的 0000013AC9BA4A40到0000013AC9BA4A47其實(shí)就是子類對(duì)象的虛函數(shù)表指針了。
三、小結(jié)
阿里??嫉腃++的問題集中在以下幾點(diǎn):
- 虛函數(shù)的實(shí)現(xiàn)
- 虛函數(shù)使用出現(xiàn)的問題原因
- 帶有虛函數(shù)的類對(duì)象的構(gòu)造和析構(gòu)過程
- 對(duì)象的內(nèi)存布局
- 虛函數(shù)的缺點(diǎn):相比普通函數(shù),虛函數(shù)調(diào)用需要2次跳轉(zhuǎn)(即需要先找到對(duì)象的虛函數(shù)表,再查找該表項(xiàng),即虛函數(shù)指針,即真正的虛函數(shù)地址),會(huì)降低CPU緩存的命中率。運(yùn)行時(shí)綁定,編譯器不好優(yōu)化。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
C++常對(duì)象精講_const關(guān)鍵字的用法
用const修飾的聲明數(shù)據(jù)成員稱為常數(shù)據(jù)成員。變量或?qū)ο蟊?const修飾后其值不能被更新。因此被const修飾的變量或?qū)ο蟊仨氁M(jìn)行初始化2013-10-10
C++利用類實(shí)現(xiàn)矩陣的數(shù)乘,乘法以及點(diǎn)乘
這篇文章主要為大家詳細(xì)介紹了C++如何利用類實(shí)現(xiàn)矩陣的數(shù)乘,乘法以及點(diǎn)乘,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)C++有一定幫助,需要的可以參考一下2022-11-11

