国产亚洲精品福利在线无卡一,国产精久久一区二区三区,亚洲精品无码国模,精品久久久久久无码专区不卡

當(dāng)前位置: 首頁(yè) > news >正文

高唐網(wǎng)站建設(shè)電子商務(wù)網(wǎng)站建設(shè)的步驟

高唐網(wǎng)站建設(shè),電子商務(wù)網(wǎng)站建設(shè)的步驟,網(wǎng)站建設(shè)行業(yè)發(fā)展,網(wǎng)站建設(shè)費(fèi)應(yīng)該怎樣入賬文章目錄1. 多態(tài)的概念2. 多態(tài)的定義及實(shí)現(xiàn)🍑 多態(tài)的構(gòu)成條件🍑 虛函數(shù)🍑 虛函數(shù)的重寫🍑 虛函數(shù)重寫的兩個(gè)例外🍑 C11的override 和 final🍑 重載、覆蓋(重寫)、隱藏(重定義)的對(duì)比3. 抽象類🍑…

文章目錄

  • 1. 多態(tài)的概念
  • 2. 多態(tài)的定義及實(shí)現(xiàn)
    • 🍑 多態(tài)的構(gòu)成條件
    • 🍑 虛函數(shù)
    • 🍑 虛函數(shù)的重寫
    • 🍑 虛函數(shù)重寫的兩個(gè)例外
    • 🍑 C++11的override 和 final
    • 🍑 重載、覆蓋(重寫)、隱藏(重定義)的對(duì)比
  • 3. 抽象類
    • 🍑 接口繼承和實(shí)現(xiàn)繼承
  • 4. 多態(tài)的原理
    • 🍑 虛函數(shù)表
    • 🍑 多態(tài)的原理
    • 🍑 動(dòng)態(tài)綁定與靜態(tài)綁定
  • 5. 單繼承和多繼承關(guān)系的虛函數(shù)表
    • 🍑 單繼承中的虛函數(shù)表
    • 🍑 多繼承中的虛函數(shù)表
    • 🍑 菱形繼承和菱形虛擬繼承
  • 6. 繼承和多態(tài)常見的面試問(wèn)題
    • 🍑 概念查考
    • 🍑 問(wèn)答題


1. 多態(tài)的概念

多態(tài)的概念:通俗來(lái)說(shuō),就是多種形態(tài),具體點(diǎn)就是去完成某個(gè)行為,當(dāng)不同的對(duì)象去完成時(shí)會(huì)產(chǎn)生出不同的狀態(tài)。

(1)示例一

比如春節(jié)回家買票這個(gè)行為,當(dāng)普通人買票時(shí),是全價(jià)買票;學(xué)生買票時(shí),是半價(jià)買票;軍人買票時(shí)是優(yōu)先買票。

不同身份的人去買票,所產(chǎn)生的行為是不同的,這就是所謂的多態(tài)。

(2)示例二

為了爭(zhēng)奪在線支付市場(chǎng),支付寶年底經(jīng)常會(huì)做誘人的掃紅包 - 支付 - 給獎(jiǎng)勵(lì)金的活動(dòng)。

那么大家想想為什么有人掃的紅包又大又新鮮 8 塊、10 塊…,而有人掃的紅包都是 1 毛,5 毛…。其實(shí)這背后也是一個(gè)多態(tài)行為。

支付寶首先會(huì)分析你的賬戶數(shù)據(jù),比如你是新用戶、比如你沒(méi)有經(jīng)常支付寶支付等等,那么你需要被鼓勵(lì)使用支付寶,那么就你 掃碼金額 = random()%99

比如你經(jīng)常使用支付寶支付或者支付寶賬戶中常年沒(méi)錢,那么就不需要太鼓勵(lì)你去使用支付寶,那么就你 掃碼金額 = random()%1

總結(jié)一下:同樣是掃碼動(dòng)作,不同的用戶掃得到的不一樣的紅包,這也是一種多態(tài)行為。

2. 多態(tài)的定義及實(shí)現(xiàn)

🍑 多態(tài)的構(gòu)成條件

多態(tài)是在不同繼承關(guān)系的類對(duì)象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。

比如 Student 繼承了 PersonPerson 對(duì)象買票全價(jià),Student 對(duì)象買票半價(jià)。

那么在繼承中要構(gòu)成多態(tài)還有兩個(gè)條件:

(1)必須通過(guò)基類的指針或者引用調(diào)用虛函數(shù)

(2)被調(diào)用的函數(shù)必須是虛函數(shù),且派生類必須對(duì)基類的虛函數(shù)進(jìn)行重寫(重寫有三同:函數(shù)名、參數(shù)、返回值)

在這里插入圖片描述

🍑 虛函數(shù)

虛函數(shù):即被 virtual 修飾的類成員函數(shù)稱為虛函數(shù)。

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

🍑 虛函數(shù)的重寫

虛函數(shù)的重寫(覆蓋):派生類中有一個(gè)跟基類完全相同的虛函數(shù)(即派生類虛函數(shù)與基類虛函數(shù)的返回值類型、函數(shù)名字、參數(shù)列表完全相同),稱子類的虛函數(shù)重寫了基類的虛函數(shù)。

下面代碼中,Person 是基類,StudentSoldier 是派生類,它們分別繼承了 Person 類,并且重寫了基類的虛函數(shù)

// 基類
class Person {
public:Person(const char* name):_name(name){}// 虛函數(shù)virtual void BuyTicket() { cout << _name << "Person:買票-全價(jià) 100¥" << endl; }protected:string _name;
};// 派生類 - 學(xué)生
class Student : public Person {
public:Student(const char* name):Person(name){}// 虛函數(shù) + 函數(shù)名/參數(shù)/返回值 ==> 重寫/覆蓋virtual void BuyTicket() { cout << _name << " Student:買票-半價(jià) 50 ¥" << endl; }
};// 派生類 - 軍人
class Soldier : public Person {
public:Soldier(const char* name):Person(name){}// 虛函數(shù) + 函數(shù)名/參數(shù)/返回值 ==> 重寫/覆蓋virtual void BuyTicket() { cout << _name << " Soldier:優(yōu)先買預(yù)留票-88折 88 ¥" << endl; }
};

思考一下:三個(gè)類里面都有 BuyTicket,那么會(huì)不會(huì)構(gòu)成隱藏呢?當(dāng)然不會(huì)!

我們這里是:虛函數(shù)+相同的函數(shù)名、相同的參數(shù)、相同的返回值,那么就構(gòu)成覆蓋或者重寫。意思就是子類里面的覆蓋了父類里面的相同函數(shù)!

如果我要去調(diào)用基類的虛函數(shù)怎么辦呢?有兩種方法!

(1)父類指針去調(diào)用虛函數(shù)

// 父類指針去調(diào)用虛函數(shù)
void Pay(Person* ptr)
{ptr->BuyTicket();
}int main()
{int option = 0;cout << "=======================================" << endl;do {cout << "請(qǐng)選擇身份:";cout << "1、普通人 2、學(xué)生 3、軍人" << endl;cin >> option;cout << "請(qǐng)輸入名字:";string name;cin >> name;switch (option){case 1:{Person p(name.c_str());Pay(&p); // 傳地址break;}case 2:{Student s(name.c_str());Pay(&s);break;}case 3:{Soldier s(name.c_str());Pay(&s);break;}default:cout << "輸入錯(cuò)誤,請(qǐng)重新輸入" << endl;break;}cout << "=======================================" << endl;} while (option != -1);return 0;
}

我們運(yùn)行以后可以看到,當(dāng)你選擇不同的身份時(shí),會(huì)去調(diào)用不同的買票函數(shù),產(chǎn)生的金額也是不一樣的,所以實(shí)現(xiàn)了函數(shù)調(diào)用的多種形態(tài)。

在這里插入圖片描述

(1)父類引用去調(diào)用虛函數(shù)

// 父類引用去調(diào)用虛函數(shù)
void Pay(Person& ptr)
{ptr.BuyTicket();
}int main()
{int option = 0;cout << "=======================================" << endl;do {cout << "請(qǐng)選擇身份:";cout << "1、普通人 2、學(xué)生 3、軍人" << endl;cin >> option;cout << "請(qǐng)輸入名字:";string name;cin >> name;switch (option){case 1:{Person p(name.c_str());Pay(p); // 這里就不能傳地址了break;}case 2:{Student s(name.c_str());Pay(s);break;}case 3:{Soldier s(name.c_str());Pay(s);break;}default:cout << "輸入錯(cuò)誤,請(qǐng)重新輸入" << endl;break;}cout << "=======================================" << endl;} while (option != -1);return 0;
}

當(dāng)然,運(yùn)行結(jié)果和上面也是一樣的:

在這里插入圖片描述

注意:在重寫基類虛函數(shù)時(shí),派生類的虛函數(shù)在不加 virtual 關(guān)鍵字時(shí),雖然也可以構(gòu)成重寫,因?yàn)槔^承后基類的虛函數(shù)被繼承下來(lái)了在派生類依舊保持虛函數(shù)屬性。但是該種寫法不是很規(guī)范,不建議這樣使用!

🍑 虛函數(shù)重寫的兩個(gè)例外

(1)協(xié)變(基類與派生類虛函數(shù)返回值類型不同)

派生類重寫基類虛函數(shù)時(shí),與基類虛函數(shù)返回值類型不同。即基類虛函數(shù)返回基類對(duì)象的指針或者引用,派生類虛函數(shù)返回派生類對(duì)象的指針或者引用時(shí),稱為協(xié)變。

下面代碼中,首先 A 是基類,B 繼承了 A是派生類;同樣 Person 是基類,Student 繼承了 Person 是派生類。

Person 中的虛函數(shù) fun 的返回值類型是基類 A 對(duì)象的指針,在派生類 Student 當(dāng)中的虛函數(shù) fun 的返回值類型是派生類 B 對(duì)象的指針。

那么此時(shí)是可以認(rèn)為派生類 Student 的虛函數(shù)重寫了基類 Person 的虛函數(shù)。

// 基類
class A{};// 派生類
class B : public A {};// 基類
class Person {
public:virtual A* f() { cout << "virtual A* Person::f()" << endl;return new A; }
};// 派生類
class Student : public Person {
public:virtual B* f() {cout << "virtual B* Student::f()" << endl;return new B; }
};int main()
{Person p;Student s;Person* ptr = &p;ptr->f();ptr = &s;ptr->f();return 0;
}

我們運(yùn)行可以看到,當(dāng) Person 指針指向的是基類對(duì)象時(shí),調(diào)用的是基類的虛函數(shù);當(dāng) Person 指針指向的是派生類對(duì)象時(shí),調(diào)用的是派生類的虛函數(shù)。

在這里插入圖片描述

注意,虛函數(shù)重寫對(duì)返回值要求有一個(gè)例外:協(xié)變時(shí),必須是父子關(guān)系指針或者引用。

也就是說(shuō)返回值不管是指針還是引用,AB 必須是父子關(guān)系!

還記得我們上面說(shuō)的派生類的虛函數(shù)在不加 virtual 關(guān)鍵字時(shí),也可以構(gòu)成重寫嗎?

// 基類
class A{};// 派生類
class B : public A {};// 基類
class Person {
public:virtual A* f() { cout << "virtual A* Person::f()" << endl;return new A; }
};// 派生類
class Student : public Person {
public:B* f() {cout << "virtual B* Student::f()" << endl;return new B; }
};int main()
{Person p;Student s;Person* ptr1 = &p;ptr1->f();Person* ptr2 = &s;ptr2->f();return 0;
}

此時(shí),子類虛函數(shù)沒(méi)有寫 virtualf() 依舊時(shí)虛函數(shù),因?yàn)樗壤^承了父類函數(shù)接口聲明,運(yùn)行以后結(jié)果也是正確的:

在這里插入圖片描述

注意:不推薦這種寫法,我們自己寫的時(shí)候子類虛函數(shù)也寫上 virtual。

(2)析構(gòu)函數(shù)的重寫(基類與派生類析構(gòu)函數(shù)的名字不同)

如果基類的析構(gòu)函數(shù)為虛函數(shù),此時(shí)派生類析構(gòu)函數(shù)只要定義,無(wú)論是否加 virtual 關(guān)鍵字,都與基類的析構(gòu)函數(shù)構(gòu)成重寫,即使基類與派生類析構(gòu)函數(shù)名字不同。

下面代碼中,基類 Person 和 派生類 Student 都沒(méi)有加 virtual,那么此時(shí)構(gòu)成的關(guān)系就是 隱藏關(guān)系(也叫重定義)

// 基類
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};// 派生類
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};int main()
{Person p;Student s;return 0;
}

運(yùn)行以后可以看到,先調(diào)用派生類 Student 對(duì)象自己的析構(gòu)函數(shù),然后 Student 會(huì)自動(dòng)調(diào)用基類 Person 的析構(gòu)函數(shù)清理基類成員,最后基類 Person 對(duì)象再調(diào)用自己的析構(gòu)函數(shù)。

在這里插入圖片描述

如果基類 Person 析構(gòu)函數(shù)加了 virtual,那么此時(shí)關(guān)系就變了,從 重定義(隱藏)關(guān)系 變成了 重寫(覆蓋)關(guān)系

// 基類
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};// 派生類
class Student : public Person {
public:~Student(){ cout << "~Student()" << endl; }
};

雖然,它們打印的結(jié)果還是一樣滴。

雖然函數(shù)名不相同,看起來(lái)違背了重寫的規(guī)則,其實(shí)不然,這里可以理解為編譯器對(duì)析構(gòu)函數(shù)的名稱做了特殊處理,編譯后析構(gòu)函數(shù)的名稱統(tǒng)一處理成 destructor。

那么場(chǎng)景下才需要子類的析構(gòu)函數(shù)也寫上 virtual 呢?

假設(shè)有這么一個(gè)場(chǎng)景:分別 new 一個(gè)父類對(duì)象和子類對(duì)象,并均用父類指針指向它們,然后分別用 delete 調(diào)用析構(gòu)函數(shù)并釋放對(duì)象空間。

// 基類
class Person {
public:~Person() { cout << "~Person()" << endl; }
};// 派生類
class Student : public Person {
public:~Student(){ cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

如果不加 virtual,就可能會(huì)導(dǎo)致內(nèi)存泄漏,因?yàn)榇藭r(shí) delete p1delete p2 都是調(diào)用的父類的析構(gòu)函數(shù):

在這里插入圖片描述

只有派生類 Student 的析構(gòu)函數(shù)重寫了 Person 的析構(gòu)函數(shù),下面的 delete 對(duì)象調(diào)用析構(gòu)函數(shù),才能構(gòu)成多態(tài),才能保證 p1p2 指向的對(duì)象正確的調(diào)用析構(gòu)函數(shù)。

// 基類
class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};// 派生類
class Student : public Person {
public:virtual ~Student(){ cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

可以看到,p1 調(diào)用父類的析構(gòu)函數(shù),p2 調(diào)用子類的析構(gòu)函數(shù),是一種多態(tài)行為。

在這里插入圖片描述

🍑 C++11的override 和 final

從上面示例中可以看出,C++ 對(duì)函數(shù)重寫的要求比較嚴(yán)格,但是有些情況下由于疏忽,可能會(huì)導(dǎo)致函數(shù)名字母次序?qū)懛炊鵁o(wú)法構(gòu)成重載,而這種錯(cuò)誤在編譯期間是不會(huì)報(bào)出的,只有在程序運(yùn)行時(shí)沒(méi)有得到預(yù)期結(jié)果再來(lái)進(jìn)行 debug 會(huì)得不償失,因此,C++11 提供了 overridefinal 兩個(gè)關(guān)鍵字,可以幫助用戶檢測(cè)是否重寫。

(1) final:修飾虛函數(shù),表示該虛函數(shù)不能再被重寫

代碼示例

// 基類
class Car
{
public:// 被final修飾,該虛函數(shù)不能再被重寫virtual void Drive() final {}
};// 子類
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒適" << endl; }
};int main()
{return 0;
}

基類 Car 的虛函數(shù) Drive()final 修飾后就不能再被重寫了,派生類若是重寫了基類的 Drive() 函數(shù)則編譯報(bào)錯(cuò)。

在這里插入圖片描述

(2)override: 檢查派生類虛函數(shù)是否重寫了基類某個(gè)虛函數(shù),如果沒(méi)有重寫編譯報(bào)錯(cuò)。

代碼示例

// 基類
class Car {
public:virtual void Drive() {}
};// 派生類
class Benz :public Car {
public:// 子類完成了父類虛函數(shù)的重寫,編譯通過(guò)virtual void Drive() override { cout << "Benz-舒適" << endl; }
};// 派生類
class BMW :public Car {
public:// 子類沒(méi)有完成了父類虛函數(shù)的重寫,編譯報(bào)錯(cuò)void Drive(int i) override{cout << "Benz-好開" << endl;}
};int main()
{return 0;
}

派生類 BenzBMW 的虛函數(shù) Driveoverride 修飾,編譯時(shí)就會(huì)檢查子類的這兩個(gè) Drive 函數(shù)是否重寫了父類的虛函數(shù),如果沒(méi)有重寫就會(huì)編譯報(bào)錯(cuò)。

在這里插入圖片描述

🍑 重載、覆蓋(重寫)、隱藏(重定義)的對(duì)比

總結(jié)一下這三者的含義:

在這里插入圖片描述

3. 抽象類

在虛函數(shù)的后面寫上 = 0,則這個(gè)函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實(shí)例化出對(duì)象。

代碼示例

//抽象類(接口類)
class Car
{
public://純虛函數(shù)virtual void Drive() = 0;
};int main()
{Car c; //抽象類不能實(shí)例化出對(duì)象,errorreturn 0;
}

派生類繼承后也不能實(shí)例化出對(duì)象,只有重寫純虛函數(shù),派生類才能實(shí)例化出對(duì)象。

//抽象類(接口類)
class Car
{
public://純虛函數(shù)virtual void Drive() = 0;
};//派生類
class Benz : public Car
{
public://重寫純虛函數(shù)virtual void Drive(){cout << "Benz-舒適" << endl;}
};//派生類
class BMV : public Car
{
public://重寫純虛函數(shù)virtual void Drive(){cout << "BMV-操控" << endl;}
};int main()
{//派生類重寫了純虛函數(shù),可以實(shí)例化出對(duì)象Benz b1;BMV b2;//不同對(duì)象用基類指針調(diào)用Drive函數(shù),完成不同的行為Car* p1 = &b1;Car* p2 = &b2;p1->Drive();  //Benz-舒適p2->Drive();  //BMV-操控return 0;
}

運(yùn)行結(jié)果

在這里插入圖片描述

純虛函數(shù)規(guī)范了派生類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。

抽象類既然不能實(shí)例化出對(duì)象,那抽象類存在的意義是什么?

  • 抽象類可以更好的去表示現(xiàn)實(shí)世界中,沒(méi)有實(shí)例對(duì)象對(duì)應(yīng)的抽象類型,比如:植物、人、動(dòng)物等。
  • 抽象類很好的體現(xiàn)了虛函數(shù)的繼承是一種接口繼承,強(qiáng)制子類去重寫純虛函數(shù),因?yàn)樽宇惾羰遣恢貙憦母割惱^承下來(lái)的純虛函數(shù),那么子類也是抽象類也不能實(shí)例化出對(duì)象。

🍑 接口繼承和實(shí)現(xiàn)繼承

  • 實(shí)現(xiàn)繼承: 普通函數(shù)的繼承是一種實(shí)現(xiàn)繼承,派生類繼承了基類函數(shù)的實(shí)現(xiàn),可以使用該函數(shù),繼承的是函數(shù)的實(shí)現(xiàn)。
  • 接口繼承: 虛函數(shù)的繼承是一種接口繼承,派生類繼承的是基類虛函數(shù)的接口,目的是為了重寫,達(dá)成多態(tài),繼承的是接口。

建議: 所以如果不實(shí)現(xiàn)多態(tài),就不要把函數(shù)定義成虛函數(shù)。

4. 多態(tài)的原理

🍑 虛函數(shù)表

下面是常考一道筆試題:sizeof(Base) 是多少?

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

通過(guò)觀察測(cè)試,我們發(fā)現(xiàn) Base 類實(shí)例化的對(duì)象 b 的大小是 8 個(gè)字節(jié)。

在這里插入圖片描述

b 對(duì)象當(dāng)中除了 _b 成員外,實(shí)際上還有一個(gè) _vfptr 放在對(duì)象的前面(有些平臺(tái)可能會(huì)放到對(duì)象的最后面,這個(gè)跟平臺(tái)有關(guān))。

在這里插入圖片描述

對(duì)象中的這個(gè)指針叫做虛函數(shù)表指針,簡(jiǎn)稱虛表指針,虛表指針指向一個(gè)虛函數(shù)表,簡(jiǎn)稱虛表,每一個(gè)含有虛函數(shù)的類中都至少都有一個(gè)虛表指針。

因?yàn)樘摵瘮?shù)的地址要被放到虛函數(shù)表中,虛函數(shù)表也簡(jiǎn)稱虛表。

那么虛函數(shù)表中到底放的是什么?我們接著往下分析

下面代碼中 Base 類有三個(gè)成員函數(shù),其中 Func1 和 Func2 是虛函數(shù),Func3 是普通成員函數(shù),子類 Derive 當(dāng)中僅對(duì)父類的 Func1 函數(shù)進(jìn)行了重寫。

//父類
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;
};//子類
class Derive : public Base
{
public://重寫虛函數(shù)Func1virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{Base b;Derive d;return 0;
}

通過(guò)調(diào)試可以發(fā)現(xiàn),父類對(duì)象 b 和基類對(duì)象 d 當(dāng)中除了自己的成員變量之外,父類和子類對(duì)象都有一個(gè)虛表指針,分別指向?qū)儆谧约旱奶摫怼?/p>

在這里插入圖片描述

實(shí)際上虛表當(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。

總結(jié)一下,派生類的虛表生成步驟如下:

  • 先將基類中的虛表內(nèi)容拷貝一份到派生類的虛表。
  • 如果派生類重寫了基類中的某個(gè)虛函數(shù),則用派生類自己的虛函數(shù)地址覆蓋虛表中基類的虛函數(shù)地址。
  • 派生類自己新增加的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的最后。

這里還有很容易混淆的問(wèn)題:虛函數(shù)存在哪的?虛表存在哪的?

虛表實(shí)際上是在構(gòu)造函數(shù)初始化列表階段進(jìn)行初始化的,注意虛表當(dāng)中存的是虛函數(shù)的地址不是虛函數(shù),虛函數(shù)和普通函數(shù)一樣,都是存在 代碼段 的,只是它的地址又存到了虛表當(dāng)中。另外,對(duì)象中存的不是虛表而是指向虛表的指針。

我們可以通過(guò)下面這段代碼判斷虛表是存在哪里的。

int main()
{Base b;Base* p = &b;printf("vfptr:%p\n", *((int*)p)); int i = 0;printf("棧上地址:%p\n", &i);       printf("數(shù)據(jù)段地址:%p\n", &j);     int* k = new int;printf("堆上地址:%p\n", k);   const char* cp = "hello world";printf("代碼段地址:%p\n", cp);    return 0;
}

可以看到,代碼當(dāng)中打印了對(duì)象 b 當(dāng)中的虛表指針,也就是虛表的地址,可以發(fā)現(xiàn)虛表地址與代碼段的地址非常接近,由此我們可以得出虛表實(shí)際上是存在代碼段的。

在這里插入圖片描述

🍑 多態(tài)的原理

上面分析了這個(gè)半天了那么多態(tài)的原理到底是什么?

我們還是拿買票這個(gè)代碼來(lái)說(shuō)明:

// 父類
class Person {
public:virtual void BuyTicket() { cout << "買票-全價(jià)" << endl; }int _p = 1;
};// 子類
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價(jià)" << endl; }int _s = 2;
};// 調(diào)用函數(shù)
void Func(Person& p)
{p.BuyTicket();
}int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}

為什么當(dāng)父類 Person 指針指向的是父類對(duì)象 Mike 時(shí),調(diào)用的就是父類的 BuyTicket,當(dāng)父類 Person 指針指向的是子類對(duì)象 Johnson 時(shí),調(diào)用的就是子類的 BuyTicket?

在這里插入圖片描述

通過(guò)調(diào)試可以發(fā)現(xiàn),對(duì)象 Mike 中包含一個(gè)成員變量 _p 和一個(gè)虛表指針,對(duì)象 Johnson 中包含兩個(gè)成員變量 _p 和 _s 以及一個(gè)虛表指針,這兩個(gè)對(duì)象當(dāng)中的虛表指針?lè)謩e指向自己的虛表。

在這里插入圖片描述

圍繞此圖分析便可得到多態(tài)的原理:

  • p 是指向 Mike 對(duì)象時(shí),p->BuyTicket 在 Mike 的虛表中找到虛函數(shù)是 Person::BuyTicket。
  • p 是指向 Johnson 對(duì)象時(shí),p->BuyTicket 在 Johson 的虛表中找到虛函數(shù)是 Student::BuyTicket。

這樣就實(shí)現(xiàn)出了不同對(duì)象去完成同一行為時(shí),展現(xiàn)出不同的形態(tài)。

現(xiàn)在想想多態(tài)構(gòu)成的兩個(gè)條件,一是完成虛函數(shù)的重寫,二是必須使用父類的指針或者引用去調(diào)用虛函數(shù)。

必須完成虛函數(shù)的重寫是因?yàn)槲覀冃枰瓿勺宇愄摫懋?dāng)中虛函數(shù)地址的覆蓋,那 為什么必須使用父類的指針或者引用去調(diào)用虛函數(shù)呢?為什么使用父類對(duì)象去調(diào)用虛函數(shù)達(dá)不到多態(tài)的效果呢?

使用父類指針或者引用時(shí),實(shí)際上是一種切片行為,切片時(shí)只會(huì)讓父類指針或者引用得到父類對(duì)象或子類對(duì)象中切出來(lái)的那一部分。

在這里插入圖片描述

因此我們現(xiàn)在對(duì)代碼進(jìn)行一下修改,當(dāng)我們把父類和子類對(duì)象直接賦值給 p1 和 p2 時(shí),再去調(diào)用,會(huì)發(fā)生什么呢?

int main()
{Person Mike;Student Johnson;Johnson._p = 3; //以便觀察是否完成切片Person p1 = Mike;Person p2 = Johnson;p1.BuyTicket();p2.BuyTicket();return 0;
}

可以看到并沒(méi)有實(shí)現(xiàn)多態(tài),因?yàn)?p1 和 p2 調(diào)用虛函數(shù)時(shí),p1 和 p2 通過(guò)虛表指針找到的虛表是不一樣的,最終調(diào)用的函數(shù)也是不一樣的。

在這里插入圖片描述

使用父類對(duì)象時(shí),切片得到部分成員變量后,會(huì)調(diào)用父類的拷貝構(gòu)造函數(shù)對(duì)那部分成員變量進(jìn)行拷貝構(gòu)造,而拷貝構(gòu)造出來(lái)的父類對(duì)象 p1 和 p2 當(dāng)中的虛表指針指向的都是父類對(duì)象的虛表。因?yàn)橥愋偷膶?duì)象共享一張?zhí)摫?#xff0c;他們的虛表指針指向的虛表是一樣的。

在這里插入圖片描述

對(duì)象切片的時(shí)候,子類只會(huì)拷貝成員給父類對(duì)象,不會(huì)拷貝虛表指針,否則拷貝就混亂了,所以父類對(duì)象中到底是父類的虛表指針還是子類的虛表指針,是都有可能的,那么是去調(diào)用父類的虛函數(shù)還是子類的虛函數(shù)就不確定!

因此,我們用 p1 和 p2 調(diào)用虛函數(shù)時(shí),p1 和 p2 通過(guò)虛表指針找到的虛表是一樣的,最終調(diào)用的函數(shù)也是一樣的,也就無(wú)法構(gòu)成多態(tài)。

總結(jié)一下:

  • 構(gòu)成多態(tài),指向誰(shuí)就調(diào)用誰(shuí)的虛函數(shù),跟對(duì)象有關(guān)。
  • 不構(gòu)成多態(tài),對(duì)象類型是什么就調(diào)用誰(shuí)的虛函數(shù),跟類型有關(guān)。

🍑 動(dòng)態(tài)綁定與靜態(tài)綁定

靜態(tài)綁定: 靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也成為靜態(tài)多態(tài),比如:函數(shù)重載。

動(dòng)態(tài)綁定: 動(dòng)態(tài)綁定又稱為后期綁定(晚綁定),在程序運(yùn)行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動(dòng)態(tài)多態(tài)。

對(duì)于下面這段代碼,我們可以通過(guò)查看匯編的方式進(jìn)一步理解靜態(tài)綁定和動(dòng)態(tài)綁定。

//父類
class Person
{
public:virtual void BuyTicket(){cout << "買票-全價(jià)" << endl;}
};//子類
class Student : public Person
{
public:virtual void BuyTicket(){cout << "買票-半價(jià)" << endl;}
};

我們?nèi)羰前凑障旅娣绞秸{(diào)用 BuyTicket 函數(shù),則不構(gòu)成多態(tài),函數(shù)的調(diào)用是在編譯時(shí)確定的。


int main()
{Student Johnson;Person p = Johnson; //不構(gòu)成多態(tài)p.BuyTicket();return 0;
}

將調(diào)用函數(shù)的那句代碼翻譯成匯編就只有以下兩條匯編指令,也就是直接調(diào)用的函數(shù)。

在這里插入圖片描述

而我們?nèi)羰前凑杖缦路绞秸{(diào)用 BuyTicket 函數(shù),則構(gòu)成多態(tài),函數(shù)的調(diào)用是在運(yùn)行時(shí)確定的。

int main()
{Student Johnson;Person& p = Johnson; //構(gòu)成多態(tài)p.BuyTicket();return 0;
}

相比不構(gòu)成多態(tài)時(shí)的代碼,構(gòu)成多態(tài)時(shí)調(diào)用函數(shù)的那句代碼翻譯成匯編后就變成了八條匯編指令,主要原因就是我們需要在運(yùn)行時(shí),先到指定對(duì)象的虛表中找到要調(diào)用的虛函數(shù),然后才能進(jìn)行函數(shù)的調(diào)用。

在這里插入圖片描述

這樣就很好的體現(xiàn)了靜態(tài)綁定是在編譯時(shí)確定的,而動(dòng)態(tài)綁定是在運(yùn)行時(shí)確定的。

5. 單繼承和多繼承關(guān)系的虛函數(shù)表

需要注意的是在單繼承和多繼承關(guān)系中,下面我們?nèi)リP(guān)注的是派生類對(duì)象的虛表模型,因?yàn)榛?br /> 的虛表模型前面我們已經(jīng)看過(guò)了,沒(méi)什么需要特別研究的。

🍑 單繼承中的虛函數(shù)表

以下列單繼承關(guān)系為例,我們來(lái)看看基類和派生類的虛表模型。

// 父類
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};// 子類
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 b;
};

其中,基類和派生類對(duì)象的虛表模型如下:

在這里插入圖片描述

在單繼承關(guān)系當(dāng)中,派生類的虛表生成過(guò)程如下:

  • 繼承基類的虛表內(nèi)容到派生類的虛表。
  • 對(duì)派生類重寫了的虛函數(shù)地址進(jìn)行覆蓋,比如 func1。
  • 虛表當(dāng)中新增派生類當(dāng)中新的虛函數(shù)地址,比如 func3 和 func4。

但是,通過(guò)監(jiān)視窗口我們發(fā)現(xiàn)看不見 func3 和 func4。這里是編譯器的監(jiān)視窗口故意隱藏了這兩個(gè)函數(shù),也可以認(rèn)為是他的一個(gè)小 bug。那么我們?nèi)绾尾榭?d 的虛表呢?

在這里插入圖片描述

(1)使用內(nèi)存監(jiān)視窗口

使用內(nèi)存監(jiān)視窗口看到的內(nèi)容是最真實(shí)的,我們調(diào)出內(nèi)存監(jiān)視窗口,然后輸入派生類對(duì)象當(dāng)中的虛表指針,即可看到虛表當(dāng)中存儲(chǔ)的四個(gè)虛函數(shù)地址。

在這里插入圖片描述

(2)使用代碼打印虛表內(nèi)容

我們可以使用以下代碼,打印上述基類和派生類對(duì)象的虛表內(nèi)容,在打印過(guò)程中可以順便用虛函數(shù)地址調(diào)用對(duì)應(yīng)的虛函數(shù),從而打印出虛函數(shù)的函數(shù)名,這樣可以進(jìn)一步確定虛表當(dāng)中存儲(chǔ)的是哪一個(gè)函數(shù)的地址。

代碼示例

// 取內(nèi)存值,打印并調(diào)用,確認(rèn)是否是func4
typedef void(*VFPTR) ();// 打印虛表
void PrintVTable(VFPTR vTable[])
{// 依次取虛表中的虛函數(shù)指針打印并調(diào)用。調(diào)用就可以看出存的是哪個(gè)函數(shù)cout << " 虛表地址:" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個(gè)虛函數(shù)地址:0X%x --> ", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{Base b;Derive d;VFPTR * vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;
}

我這里稍微解釋一下打印虛表的代碼:

  • 思路:取出 b、d 對(duì)象的頭 4bytes,就是虛表的指針,前面我們說(shuō)了虛函數(shù)表本質(zhì)是一個(gè)存虛函數(shù)指針的指針數(shù)組,這個(gè)數(shù)組最后面放了一個(gè) nullptr
  • 先取 b 的地址,強(qiáng)轉(zhuǎn)成一個(gè) int* 的指針
  • 再解引用取值,就取到了 b 對(duì)象頭 4bytes 的值,這個(gè)值就是指向虛表的指針
  • 再?gòu)?qiáng)轉(zhuǎn)成 VFPTR*,因?yàn)樘摫砭褪且粋€(gè)存 VFPTR 類型(虛函數(shù)指針類型)的數(shù)組
  • 虛表指針傳遞給 PrintVTable 進(jìn)行打印虛表
  • 需要說(shuō)明的是這個(gè)打印虛表的代碼經(jīng)常會(huì)崩潰,因?yàn)榫幾g器有時(shí)對(duì)虛表的處理不干凈,虛表最后面沒(méi)有放 nullptr,導(dǎo)致越界,這是編譯器的問(wèn)題。

運(yùn)行結(jié)果如下:

在這里插入圖片描述

模型圖如下:

在這里插入圖片描述

🍑 多繼承中的虛函數(shù)表

以下列多繼承關(guān)系為例,我們來(lái)看看基類和派生類的虛表模型。

// 父類1
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int _b1;
};// 父類2
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;
};

其中,兩個(gè)基類的虛表模型如下:

在這里插入圖片描述

而派生類的虛表模型就不那么簡(jiǎn)單了,派生類的虛表模型如下:

在這里插入圖片描述

在多繼承關(guān)系當(dāng)中,派生類的虛表生成過(guò)程如下:

  • 分別繼承各個(gè)基類的虛表內(nèi)容到派生類的各個(gè)虛表當(dāng)中。
  • 對(duì)派生類重寫了的虛函數(shù)地址進(jìn)行覆蓋(派生類中的各個(gè)虛表中存有該被重寫虛函數(shù)地址的都需要進(jìn)行覆蓋),比如 func1。
  • 在派生類第一個(gè)繼承基類部分的虛表當(dāng)中新增派生類當(dāng)中新的虛函數(shù)地址,比如 func3。

這里在調(diào)試時(shí),在某些編譯器下也會(huì)出現(xiàn)顯示不全的問(wèn)題,此時(shí)如果我們想要看到派生類對(duì)象完整的虛表也是用那兩種方法。

(1)使用內(nèi)存監(jiān)視窗口

直接調(diào)用內(nèi)存窗口查看:

在這里插入圖片描述

(2)使用代碼打印虛表內(nèi)容

需要注意的是,我們?cè)谂缮惖谝粋€(gè)虛表地址的基礎(chǔ)上,向后移 sizeof(Base1) 個(gè)字節(jié)即可得到第二個(gè)虛表的地址。

// 取內(nèi)存值,打印并調(diào)用,確認(rèn)是否是func4
typedef void(*VFPTR) ();// 打印虛表
void PrintVTable(VFPTR vTable[])
{// 依次取虛表中的虛函數(shù)指針打印并調(diào)用。調(diào)用就可以看出存的是哪個(gè)函數(shù)cout << " 虛表地址:" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個(gè)虛函數(shù)地址:0X%x --> ", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{Base1 b1;Base2 b2;VFPTR* vTableb1 = (VFPTR*)(*(int*)&b1); PrintVTable(vTableb1); // 打印基類對(duì)象b1的虛表地址及其內(nèi)容VFPTR* vTableb2 = (VFPTR*)(*(int*)&b2);PrintVTable(vTableb2); // 打印基類對(duì)象b2的虛表地址及其內(nèi)容Derive d;VFPTR* vTableb3 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb3); // 打印派生類對(duì)象d的第一個(gè)虛表地址及其內(nèi)容VFPTR* vTableb4 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb4); // 打印派生類對(duì)象d的第二個(gè)虛表地址及其內(nèi)容return 0;
}

運(yùn)行結(jié)果如下:

在這里插入圖片描述

觀察下圖可以看出:多繼承派生類的未重寫的虛函數(shù)放在第一個(gè)繼承基類部分的虛函數(shù)表中:

在這里插入圖片描述

🍑 菱形繼承和菱形虛擬繼承

實(shí)際中我們不建議設(shè)計(jì)出菱形繼承及菱形虛擬繼承,一方面太復(fù)雜容易出問(wèn)題,另一方面這樣的模型,訪問(wèn)基類成員有一定得性能損耗。所以菱形繼承、菱形虛擬繼承我們的虛表我們就不看了,一般我們也不需要研究清楚,因?yàn)閷?shí)際中很少用。

但是這里可以給大家推薦兩篇文章:

  • C++ 虛函數(shù)表解析
  • C++ 對(duì)象的內(nèi)存布局

6. 繼承和多態(tài)常見的面試問(wèn)題

🍑 概念查考

  1. 下面哪種面向?qū)ο蟮姆椒梢宰屇阕兊酶挥?#xff1f;
    A. 繼承 B. 封裝 C. 多態(tài) D. 抽象

  2. 什么是面向?qū)ο蟪绦蛟O(shè)計(jì)語(yǔ)言中的一種機(jī)制。這種機(jī)制實(shí)現(xiàn)了方法的定義與具體的對(duì)象無(wú)關(guān),而對(duì)方法的調(diào)用則可以關(guān)聯(lián)于具體的對(duì)象。
    A. 繼承 B. 模板 C. 對(duì)象的自身引用 D. 動(dòng)態(tài)綁定

  3. 面向?qū)ο笤O(shè)計(jì)中的繼承和組合,下面說(shuō)法錯(cuò)誤的是?
    A. 繼承允許我們覆蓋重寫父類的實(shí)現(xiàn)細(xì)節(jié),父類的實(shí)現(xiàn)對(duì)于子類是可見的,是一種靜態(tài)復(fù)用,也稱為白盒復(fù)用
    B. 組合的對(duì)象不需要關(guān)心各自的實(shí)現(xiàn)細(xì)節(jié),之間的關(guān)系是在運(yùn)行時(shí)候才確定的,是一種動(dòng)態(tài)復(fù)用,也稱為黑盒復(fù)用
    C. 優(yōu)先使用繼承,而不是組合,是面向?qū)ο笤O(shè)計(jì)的第二原則
    D. 繼承可以使子類能自動(dòng)繼承父類的接口,但在設(shè)計(jì)模式中認(rèn)為這是一種破壞了父類的封裝性的表現(xiàn)

  4. 以下關(guān)于純虛函數(shù)的說(shuō)法,正確的是?
    A. 聲明純虛函數(shù)的類不能實(shí)例化對(duì)象 B. 聲明純虛函數(shù)的類是虛基類
    C. 子類必須實(shí)現(xiàn)基類的純虛函數(shù) D. 純虛函數(shù)必須是空函數(shù)

  5. 關(guān)于虛函數(shù)的描述正確的是?
    A. 派生類的虛函數(shù)與基類的虛函數(shù)具有不同的參數(shù)個(gè)數(shù)和類型 B. 內(nèi)聯(lián)函數(shù)不能是虛函數(shù)
    C. 派生類必須重新定義基類的虛函數(shù) D. 虛函數(shù)可以是一個(gè)static型的函數(shù)

  6. 關(guān)于虛表說(shuō)法正確的是?
    A. 一個(gè)類只能有一張?zhí)摫?br /> B. 基類中有虛函數(shù),如果子類中沒(méi)有重寫基類的虛函數(shù),此時(shí)子類與基類共用同一張?zhí)摫?br /> C. 虛表是在運(yùn)行期間動(dòng)態(tài)生成的
    D. 一個(gè)類的不同對(duì)象共享該類的虛表

  7. 假設(shè) A 類中有虛函數(shù),B 繼承自 A,B 重寫 A 中的虛函數(shù),也沒(méi)有定義任何虛函數(shù),則
    A. A類對(duì)象的前4個(gè)字節(jié)存儲(chǔ)虛表地址,B類對(duì)象前4個(gè)字節(jié)不是虛表地址
    B. A類對(duì)象和B類對(duì)象前4個(gè)字節(jié)存儲(chǔ)的都是虛基表的地址
    C. A類對(duì)象和B類對(duì)象前4個(gè)字節(jié)存儲(chǔ)的虛表地址相同
    D. A類和B類虛表中虛函數(shù)個(gè)數(shù)相同,但A類和B類使用的不是同一張?zhí)摫?/p>

  8. 下面程序輸出結(jié)果是什么?

#include <iostream>
using namespace std;class A
{
public:A(char* s) { cout << s << endl; }~A() {};
};
class B : virtual public A
{
public:B(char* s1, char* s2):A(s1){cout << s2 << endl;}
};
class C : virtual public A
{
public:C(char* s1, char* s2):A(s1){cout << s2 << endl;}
};
class D : public B, public C
{
public:D(char* s1, char* s2, char* s3, char* s4):B(s1, s2), C(s1, s3), A(s1){cout << s4 << endl;}
};
int main()
{D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}

A. class A class B class C class D B. class D class B class C class A
C. class D class C class B class A D. class A class C class B class D

  1. 多繼承中指針偏移問(wèn)題?下面說(shuō)法正確的是?
#include <iostream>
using namespace std;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;
}

A. p1 == p2 == p3 B. p1 < p2 < p3 C. p1 == p3 != p2 D. p1 != p2 != p3

  1. 以下程序輸出結(jié)果是什么?
#include <iostream>
using namespace std;class A
{
public:virtual void func(int val = 1){cout << "A->" << val << endl;}virtual void test(){func();}
};class B : public A
{
public:void func(int val = 0){cout << "B->" << val << endl;}
};int main()
{B* p = new B;p->test();return 0;
}

A. A->0 B. B->1 C. A->1 D. B->0 E. 編譯出錯(cuò) F. 以上都不正確

答案如下:

在這里插入圖片描述

🍑 問(wèn)答題

(1)什么是多態(tài)?

多態(tài)是指不同繼承關(guān)系的類對(duì)象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。多態(tài)又分為靜態(tài)的多態(tài)和動(dòng)態(tài)的多態(tài)。

(2)什么是重載、重寫(覆蓋)、重定義(隱藏)?

重載是指兩個(gè)函數(shù)在同一作用域,這兩個(gè)函數(shù)的函數(shù)名相同,參數(shù)不同。

重寫(覆蓋)是指兩個(gè)函數(shù)分別在基類和派生類的作用域,這兩個(gè)函數(shù)的函數(shù)名、參數(shù)、返回值都必須相同(協(xié)變例外),且這兩個(gè)函數(shù)都是虛函數(shù)。

重定義(隱藏)是指兩個(gè)函數(shù)分別在基類和派生類的作用域,這兩個(gè)函數(shù)的函數(shù)名相同。若兩個(gè)基類和派生類的同名函數(shù)不構(gòu)成重寫就是重定義。

(3)多態(tài)的實(shí)現(xiàn)原理?

構(gòu)成多態(tài)的父類對(duì)象和子類對(duì)象的成員當(dāng)中都包含一個(gè)虛表指針,這個(gè)虛表指針指向一個(gè)虛表,虛表當(dāng)中存儲(chǔ)的是該類對(duì)應(yīng)的虛函數(shù)地址。

因此,當(dāng)父類指針指向父類對(duì)象時(shí),通過(guò)父類指針找到虛表指針,然后在虛表當(dāng)中找到的就是父類當(dāng)中對(duì)應(yīng)的虛函數(shù);

當(dāng)父類指針指向子類對(duì)象時(shí),通過(guò)父類指針找到虛表指針,然后在虛表當(dāng)中找到的就是子類當(dāng)中對(duì)應(yīng)的虛函數(shù)。

(4)inline 函數(shù)可以是虛函數(shù)嗎?

我們知道內(nèi)聯(lián)函數(shù)是會(huì)在調(diào)用的地方展開的,也就是說(shuō)內(nèi)聯(lián)函數(shù)是沒(méi)有地址的,但是內(nèi)聯(lián)函數(shù)是可以定義成虛函數(shù)的,當(dāng)我們把內(nèi)聯(lián)函數(shù)定義虛函數(shù)后,編譯器就忽略了該函數(shù)的內(nèi)聯(lián)屬性,這個(gè)函數(shù)就不再是內(nèi)聯(lián)函數(shù)了,因?yàn)樾枰獙⑻摵瘮?shù)的地址放到虛表中去。

(5)靜態(tài)成員可以是虛函數(shù)嗎?

靜態(tài)成員函數(shù)不能是虛函數(shù),因?yàn)殪o態(tài)成員函數(shù)沒(méi)有this指針,使用類型 :: 成員函數(shù)的調(diào)用方式無(wú)法訪問(wèn)虛表,所以靜態(tài)成員函數(shù)無(wú)法放進(jìn)虛表。

(6)構(gòu)造函數(shù)可以是虛函數(shù)嗎?

構(gòu)造函數(shù)不能是虛函數(shù),因?yàn)閷?duì)象中的虛表指針是在構(gòu)造函數(shù)初始化列表階段才初始化的。

(7)析構(gòu)函數(shù)可以是虛函數(shù)嗎?

析構(gòu)函數(shù)可以是虛函數(shù),并且最后把基類的析構(gòu)函數(shù)定義成虛函數(shù)。若是我們分別 new 一個(gè)父類對(duì)象和一個(gè)子類對(duì)象,并均用父類指針指向它們,當(dāng)我們使用 delete 調(diào)用析構(gòu)函數(shù)并釋放對(duì)象空間時(shí),只有當(dāng)父類的析構(gòu)函數(shù)是虛函數(shù)的情況下,才能正確調(diào)用父類和子類的析構(gòu)函數(shù)分別對(duì)父類和子類對(duì)象進(jìn)行析構(gòu),否則當(dāng)我們使用父類指針 delete 對(duì)象時(shí),只能調(diào)用到父類的析構(gòu)函數(shù)。

(8)對(duì)象訪問(wèn)普通函數(shù)快還是虛函數(shù)更快?

對(duì)象訪問(wèn)普通函數(shù)比訪問(wèn)虛函數(shù)更快,若我們?cè)L問(wèn)的是一個(gè)普通函數(shù),那直接訪問(wèn)就行了,但當(dāng)我們?cè)L問(wèn)的是虛函數(shù)時(shí),我們需要先找到虛表指針,然后在虛表當(dāng)中找到對(duì)應(yīng)的虛函數(shù),最后才能調(diào)用到虛函數(shù)。

(9)虛函數(shù)表是在什么階段生成的?存在哪的?

虛表是在構(gòu)造函數(shù)初始化列表階段進(jìn)行初始化的,虛表一般情況下是存在代碼段(常量區(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)什么是抽象類?抽線類的作用?

抽象類很好的體現(xiàn)了虛函數(shù)的繼承是一種接口繼承,強(qiáng)制子類去抽象純虛函數(shù),因?yàn)樽宇惾羰遣怀橄髲母割惱^承下來(lái)的純虛函數(shù),那么子類也是抽象類也不能實(shí)例化出對(duì)象。

其次,抽象類可以很好的去表示現(xiàn)實(shí)世界中沒(méi)有示例對(duì)象對(duì)應(yīng)的抽象類型,比如:植物、人、動(dòng)物等。

http://m.aloenet.com.cn/news/37644.html

相關(guān)文章:

  • 深圳微商城網(wǎng)站制作費(fèi)用網(wǎng)站seo排名優(yōu)化工具
  • 網(wǎng)站關(guān)鍵詞seo優(yōu)化怎么做怎樣進(jìn)行seo優(yōu)化
  • 個(gè)人軟件制作網(wǎng)站網(wǎng)站的優(yōu)化與推廣分析
  • 網(wǎng)站如果不在公安局備案怎樣百度seo關(guān)鍵詞排名查詢
  • 網(wǎng)頁(yè)抓取 wordpress西安自動(dòng)seo
  • php網(wǎng)站模塊如何編寫一個(gè)網(wǎng)站
  • 尋找手機(jī)網(wǎng)站建設(shè)北京優(yōu)化seo排名
  • 官網(wǎng)做的好看的網(wǎng)站有哪些設(shè)計(jì)網(wǎng)站排行
  • 宜春做網(wǎng)站公司網(wǎng)站seo優(yōu)化工具
  • 網(wǎng)站開發(fā)測(cè)試過(guò)程中文域名查詢官網(wǎng)
  • 阜寧做網(wǎng)站的公司新手怎么做電商
  • 自適應(yīng)網(wǎng)站做mip改造在哪里可以免費(fèi)自學(xué)seo課程
  • 哪家做公司網(wǎng)站互聯(lián)網(wǎng)廣告推廣好做嗎
  • 吧網(wǎng)站做軟件的軟件網(wǎng)絡(luò)銷售平臺(tái)怎么做
  • 做國(guó)際網(wǎng)站的流程廣州seo報(bào)價(jià)
  • java做網(wǎng)站百度客服怎么轉(zhuǎn)人工電話
  • 仿做唯品會(huì)網(wǎng)站黃岡便宜的網(wǎng)站推廣怎么做
  • pmp培訓(xùn)seo網(wǎng)站
  • 沈陽(yáng)網(wǎng)站搜索引擎優(yōu)化google推廣教程
  • 網(wǎng)頁(yè)版視頻網(wǎng)站建設(shè)需要多少錢百度sem推廣具體做什么
  • kol合作推廣seo外鏈?zhǔn)鞘裁?/a>
  • 自己創(chuàng)業(yè)做原公司一樣的網(wǎng)站網(wǎng)站seo設(shè)計(jì)
  • 公司做網(wǎng)站的步驟廣州seo關(guān)鍵字推廣
  • 做韋恩圖的網(wǎng)站怎么樣推廣自己的公司
  • wordpress 添加導(dǎo)航菜單成都seo招聘
  • 網(wǎng)站域名有什么用計(jì)算機(jī)培訓(xùn)
  • 大學(xué)新校區(qū)建設(shè)網(wǎng)站網(wǎng)站seo重慶
  • 網(wǎng)站推廣資訊上海百度競(jìng)價(jià)托管
  • 中國(guó)大型建筑公司有哪些seo西安
  • 全國(guó)公安網(wǎng)站備案應(yīng)用寶aso優(yōu)化