C++筆記:OOP三大特性之多態

前言

本博客中的代碼和解釋都是在VS2019下的x86程序中進行的,涉及的指針都是 4 字節,如果要其他平臺下測試,部分代碼需要改動。比如:如果是x64程序,則需要考慮指針是8bytes問題等等。

文章目錄

  • 前言
  • 一、多態的概念
  • 二、多態的定義及實現
    • 2.1 構成多態的兩個必要條件
    • 2.2 什么虛函數?
    • 2.3 什么是虛函數重寫?
    • 2.4 多態調用的例子
    • 2.5 虛函數重寫的三個例外
      • 第一:派生類虛函數不加 virtual 關鍵字
      • 第二:協變(基類與派生類虛函數返回值類型不同)
      • 第三:析構函數的重寫(基類與派生類析構函數的名字不同)
    • 2.6 重載、覆蓋(重寫)、隱藏(重定義)的對比
    • 2.7 C++11 override 和 final
  • 三、抽像類
    • 3.1 接口繼承與實現繼承
  • 四、探究多態下的對象模型及認識虛表
    • 4.1 虛函數指針與虛函數表
    • 4.2 虛函數與虛函數表的存儲位置
    • 4.3 虛函數指針初始化和虛表生成時間
    • 4.4 動態多態的原理
  • 五、單繼承和多繼承關系的虛函數表
    • 5.1 單繼承中的虛函數表
    • 5.2多繼承中的虛函數表
  • 六、多態相關的一些問題

一、多態的概念

多態是面向對象編程中一個重要特性,它允許以一致的方式來使用不同的對象得到不同的結果,或者說,某一個動作被不同的對象完成會得到不同的結果,這兩種說法都是一樣的。

在C++中,多態性有兩種主要形式:編譯時多態性(靜態多態性)和運行時多態性(動態多態性)。

  • 靜態多態性:在程序編譯階段實現,表現為函數重載,通過傳遞不同的實參調用相應的同名函數獲取不同的結果。
  • 動態多態性基于繼承實現,指在程序運行階段,根據具體拿到的類型確定程序的具體行為,調用具體的函數。

后面的內容都是關于動態多態,為了方便,接下來的內容的“多態”都默認指動態多態

二、多態的定義及實現

2.1 構成多態的兩個必要條件

  1. 必須通過基類的指針或者引用調用虛函數。
  2. 被調用的函數必須是虛函數,且派生類必須對基類的虛函數完成重寫
    (注意:只有虛函數才有重寫這個概念)

2.2 什么虛函數?

虛函數:即被關鍵字 virtual 修飾的類成員函數稱為虛函數。

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

2.3 什么是虛函數重寫?

虛函數的重寫,又叫做虛函數的覆蓋,當派生類中實現一個跟基類完全相同的虛函數,這時候稱 “派生類的虛函數重寫了基類的虛函數”。

派生類虛函數與基類虛函數的完全相同要求滿足以下三同:① 返回值類型相同、② 函數名相同、③ 參數列表相同

2.4 多態調用的例子

以下是一個多態調用的例子:

首先,左邊 Func 函數中,people 是基類的引用,派生類 Student 完成了對基類 Person 的 BuyTicket() 的重寫,滿足多態調用。

其次,people 引用基類對象調用基類的 BuyTicket() ,引用派生類對象調用派生類重寫的 BuyTicket() 。
在這里插入圖片描述

2.5 虛函數重寫的三個例外

C++中有三個形式上不滿足函數重寫的語法規定,但依舊是虛函數重寫的特殊情況。

第一:派生類虛函數不加 virtual 關鍵字

上面那個例子中 ,Student 類中的虛函數像下面這樣寫也是可以編譯通過的,因為繼承后基類的虛函數被繼承下來了在派生類依舊保持虛函數屬性,但是該種寫法不是很規范,建議基類和派生類都加上 virtual,以提高可讀性

class Student : public Person {
public:void BuyTicket() {cout << "買票-半價" << endl; }
};

第二:協變(基類與派生類虛函數返回值類型不同)

派生類重寫基類虛函數時,派生類虛函數與基類虛函數的返回值類型可以不同,但要求基類虛函數返回基類對象的指針或者引用,派生類虛函數返回派生類對象的指針或者引用,即返回值構成繼承關系,這種做法稱之為 “ 協變 ”。

以下代碼為一個協變的例子:

// A、B構成繼承關系
class A {};
class B : public A {};class Person {
public:// Person 返回 基類A 的 指針virtual A* f() { cout << "A* f()" << endl;return new A; }
};
class Student : public Person {
public:// Student 返回 派生類B 的 指針virtual B* f() { cout << "B* f()" << endl;return new B; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;p1->f();p2->f();return 0;
}

在這里插入圖片描述

假設A、B不構成繼承關系,就會引發報錯

// 去掉繼承關系
class A {}
class B {}

在這里插入圖片描述

在VS2019中,編譯器對協變進行了強制檢查,如果沒有強制檢查,會發生什么?

首先,基類和派生類的f()函數由于返回值類型不同不構成重寫,不構成重寫就滿足多態調用,所以和普通的函數調用沒有區別,普通函數調用取決于對象或者指針或者引用的類型

其次,由于Person和Student是繼承關系,f()構成隱藏關系,由于編譯器的賦值兼容轉換機制且指針p1p2的類型都是Person*,兩個指針會去調用Person的f(),而不會去調用Student類的f()

而下面講的第三個例外不實現成重寫也會導致這個問題。

第三:析構函數的重寫(基類與派生類析構函數的名字不同)

一個繼承體系中,派生類和基類的析構函數都會被編譯器特殊處理成 destructor(),所以基類和派生類的析構函數會構成隱藏關系,在派生類調用基類析構函數需要指定類域顯式調用,現在可以解釋為什么要做這種特殊處理了,是為了重寫。

學了動態多態之后,函數調用可以分成兩種:

  • 普通調用,取決于指針或者引用或者對象的類型。
  • 多態調用,取決于指針或者引用指向的對象。

下面這份代碼中由于兩個析構函數沒有滿足虛函數重寫,無法進行多態調用,指針p2僅對一個Student對象中的Person部分進行析構,Student對象內部的資源沒有完全回收,這會導致內存泄漏問題

// 析構隱藏
class Person {
public:~Person() { cout << "~Person()" << endl; }
};class Student : public Person {
public:~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;
}

在這里插入圖片描述

編譯器對析構函數的名稱做了特殊處理,編譯后析構函數的名稱統一處理成destructor,只要基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加 virtual 關鍵字,都與基類的析構函數構成重寫。

// 析構重寫
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.6 重載、覆蓋(重寫)、隱藏(重定義)的對比

在這里插入圖片描述

2.7 C++11 override 和 final

從上面可以看出,C++對函數重寫的要求比較嚴格,但是有些情況下由于疏忽,可能會導致函數名字母次序寫反而無法構成重載,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有得到預期結果才來debug會得不償失,因此:C++11提供了override和final兩個關鍵字,可以幫助用戶檢測是否重寫。

  1. final,修飾虛函數時,表示該虛函數不能再被重寫;修飾一個類時,表示該類不能被繼承

在這里插入圖片描述
在這里插入圖片描述

  1. override: 檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯。

在這里插入圖片描述

三、抽像類

在虛函數的后面寫上 =0 ,則這個函數為純虛函數。

class Car
{
public:// Drive是純虛函數virtual void Drive() = 0;
};

包含純虛函數的類被稱之為抽象類(也叫接口類),抽象類不能被實例化對象。
在這里插入圖片描述

抽象類定義了一個類可能發出的動作的原型,但既沒有實現,也沒有任何狀態信息,引入抽象類的原因在于很多時候基類本身實例化不合情理的,例如車類作為一個基類可以派生出奔馳、寶馬等子類,但是車類本身實例化是沒有意義的。

這時候就可以將車類定義成抽象類,由于抽象類只能提供原型而無法被實例化,因此派生類必須提供接口的實現,派生類亦無法被實例化,純虛函數規范了派生類必須重寫。

class Car
{
public:virtual void Drive() = 0;
};// 奔馳類
class Benz :public Car
{
public:// 完成重寫virtual void Drive(){cout << "Benz-舒適" << endl;}
};// 寶馬類
class BMW :public Car
{
public:// 不完成重寫
};int main()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

在這里插入圖片描述

3.1 接口繼承與實現繼承

普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。

虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。

下面這道題就體現了接口繼承

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;
}

A:A->0 B:B->1 C:A->1 D:B->0 E:編譯出錯 F:以上都不正確

答案是B,解析如下:
A、B是繼承關系,A中有兩個虛函數(func 和 test),B中有一個虛函數(func),func接口構成重寫。

在 main 函數中:B* 的指針變量 p 指向一個B對象,p-> 告訴編譯器要到 B 的類域中找 test 的定義,同時把 p 傳給 this,換言之,this 指向的B對象。

編譯器在 B 類中找不到 test,然后由于繼承關系存在,到 A 類中去找,找到并且繼承到了使用權,所以,會調用到 A 類中的 test 接口。

A 類的 test 接口調用了 func 函數,函數是通過 this 指針來調用的(this->func();),此時在 A 的類域中,this 的類型顯然是 A*。

類型為A* 的 this 指針指向一個 B 對象,且 func 滿足虛函數重寫,會去調用 B 中的 func()。

虛函數的重寫是接口繼承,virtual void func(int val = 1),這時候 val 的是 1,所以答案是 B->1。

四、探究多態下的對象模型及認識虛表

4.1 虛函數指針與虛函數表

下面代碼中,sizeof(Base)是多少?

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

在這里插入圖片描述
按道理說,對象只存儲成員變量,預期大小應該是 4 字節,可通過運行結果可以發現,Base對象的大小是 8 字節(看前言),因此,當一個類包含虛函數時,類對象模型肯定發生了改變。

接下來實例化出 Base 類的兩個對象,然后通過監視窗口觀察 Base 類的對象結構發現:

  • Base類對象中除了_b成員,還多一個__vfptr指針放在對象的最前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關),__vfptr指向一個叫做 vftable 的數組,數組里有兩個元素,但監視窗口只顯示了第一個元素,它是 Base::Func 的函數指針。
  • Base 類實例化出的兩個對象的 __vfptr 的內容都是一樣的。
    在這里插入圖片描述

當一個類中包含虛函數成員,類對象模型如下:

  • 對象內部除了自己定義的成員變量外,編譯器自動添加了一個指針成員,對象中的這個指針我們叫做虛函數表指針(v代表virtual,f代表function),該指針指向的是一個數組,被稱為虛函數表,虛函數表也簡稱虛表,虛表里面存放的是虛函數的地址。
  • 一個類的實例化出多個對象時,它們共享該類的虛表。

在這里插入圖片描述


了解什么是虛表指針和虛表之后,Base的派生類對象模型又是怎樣的呢?接著往下分析。

為了更好地測試,針對上面的代碼改造成單繼承但無虛函數重寫的場景,查看派生類對象模型

  1. 我們增加一個派生類Derive去繼承Base
  2. Base再增加一個虛函數Func2和一個普通函數Func3
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:
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

通過對監視窗口的觀察可以看到:

  1. d 對象由兩部分構成,一部分是基類繼承下來的成員,另一部分是自己的成員。
  2. 派生類對象 d 中也有一個虛表指針,虛表指針存在基類部分的首個位置。
  3. 基類b對象和派生類d對象虛表指針是不一樣的,可是虛表的內容是一樣的,也就是說派生類對象會拷貝一份基類的虛表給自己。
  4. Func3 也繼承下來了,但是不是它虛函數,所以不會放進虛表。

在這里插入圖片描述


針對上面的代碼的Derive中重寫Func1改造成單繼承且有虛函數重寫的場景,再查看派生類對象模型

// Base 類不變class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};// main 函數不變

通過對監視窗口的觀察可以看到:

  • 派生類對 Func1 完成重寫之后,派生類對象 d 的虛表發生部分變換,原本 Base::Func1 地址被重寫后的 Derive::Func1 的地址覆蓋,這就是為什么虛函數的重寫也叫作覆蓋,重寫是語法的叫法,覆蓋是原理層的叫法。

在這里插入圖片描述


針對上面的代碼的Derive中增加虛函數 Func4再查看派生類對象模型

// Base 類不變class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}// 增加虛函數 Func4virtual void Func4(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};// main 函數不變

通過監視窗口 + 內存窗口的觀察驗證發現:

  • 派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后。

在這里插入圖片描述

總結一下派生類的虛表生成:

  1. 先將基類中的虛表內容拷貝一份到派生類虛表中。
  2. 如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數
  3. 派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后。

4.2 虛函數與虛函數表的存儲位置

這里還有一個很容易混淆的問題:

虛函數存在哪的?虛表存在哪的?
答:虛函數存在虛表,虛表存在對象中。注意上面的回答的錯的。

上面的回答的錯的。

首先,虛表存的是虛函數指針,不是虛函數本身,虛函數和普通函數雖然在語法上一樣的,但在編譯器看來它們都是函數,經過編譯之后都會生成地址和指令,指令存儲在代碼段的,地址存到了虛表中。

其次,對象中存的不是虛表,存的是虛表指針,虛表指針是對象的成員,如果對象在棧上的,虛表指針就在棧上,如果對象是new出來的,虛表指針就在堆上。

既然不確定虛表的存儲位置,那樣可以對比法來驗證一下。

int main()
{Base b;Derive d;int i = 0;static int j = 0;int* p1 = new int;const char* p2 = "xxxxxxxxxxxxxxxxx";Base* p3 = &b;Derive* p4 = &d;printf("棧:%p\n", &i);printf("堆:%p\n", p1);printf("靜態區:%p\n", &j);printf("常量區:%p\n", p2);// vfptr在對象的第一個位置,x86下指針是4字節,類型強轉(int*)p3獲得vfptr// 對vfptr解引用能夠找到虛表第一個虛函數的地址// 對比分析虛函數地址和哪個區的地址接近就在哪個區printf("Base虛表首元素:%p\n", *(int*)p3);printf("Derive虛表首元素:%p\n", *(int*)p4);return 0;
}

測試結果發現,虛表上的函數指針和常量區(代碼段)的地址是最接近,由此可以認為在VS下虛表是存儲在常量區(代碼段)
在這里插入圖片描述

Linux 發行版 CentOS 7.6 下的g++編譯器的測試結果如下:
測試結果同樣是發現虛表實在代碼端上的在這里插入圖片描述

4.3 虛函數指針初始化和虛表生成時間

先來一波猜測:

  1. 對象內部的虛函數指針成員是編譯器自己加上去的,虛函數指針的初始化應當交由編譯器在對象構造時進行的。
  2. 類與對象的語法部分規定:對象的成員變量的初始化必須經過初始化列表,如果虛函數指針是在調用構造函數期間初始化的,就能夠說明虛函數指針在初始化列表完成初始化的
  3. 在VS平臺下,虛函數指針在對象模型的首位,假如虛函數指針的初始化時間比一個對象中任意一個成員還早就說明它是第一個被初始化
  4. 在 C++ 中,虛函數轉換成地址和指令是程序在編譯期間完成的,對象的構造函數是在運行時期間被調用的,如果虛函數指針在初始化列表被初始化,說明虛表在虛函數指針被初始化之前就已經生成好了

為 Base 類添加構造函數后驗證結果如下:

  1. 虛表在編譯階段生成。
  2. 虛函數指針在運行階段由編譯器調用構造函數通過初始化列表初始化。
  3. 虛函數指針在VS的類對像模型中是第一個被初始化的。

在這里插入圖片描述

4.4 動態多態的原理

多態調用通過基類的指針或者引用,指向基類調用基類的虛函數,指向派生類調用派生類的虛函數,通過對上面虛表的了解之后,不用說肯定是通過虛表來完成的,但具體的過程是怎么樣的呢?

下面就用這份代碼例子來做一個深入的研究:

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

在這里插入圖片描述

  1. 觀察下圖的紅色箭頭我們看到,p 是指向 mike 對象時,p->BuyTicketmike 的虛表中找到虛函數是 Person::BuyTicket
  2. 觀察下圖的藍色箭頭我們看到,p 是指向 johnson 對象時,p->BuyTicketjohson 的虛表中找到虛函數是 Student::BuyTicket
  3. 反過來思考我們要達到多態,有兩個條件,一個是虛函數覆蓋,一個是基類的指針或引用調用虛函數,這是為什么?

第一:基類的指針或者引用指向派生類對象時,編譯器會發生賦值兼容轉換操作,將派生類對象中基類部分切割給基類的指針或者引用,然后基類的指針和引用可以把這些派生類對象當成基類對象來使用。
第二:由于繼承的緣故,派生類的虛表指針是在基類部分的成員中的,切割之后基類的指針或者引用依舊能夠使用派生類的虛表。
第三:如果不完成虛函數覆蓋,派生類的虛表和基類的虛表是一樣的,只有派生類完成了虛函數覆蓋,虛表上的函數指針才會發生改變,基類指針或者引用才能調用到派生類重寫的虛函數,否則只能調用到基類的虛函數。

  1. 為什么說動態多態是在運行時階段實現的?

編譯期間,編譯器主要檢測代碼是否違反語法規則,此時無法知道基類的指針或者引用到底引用那個類的對象,也就無法知道調用哪個類的虛函數。
只有在程序運行時,才知道具體指向那個類的對象,然后通過虛表調用對應的虛函數,從而實現多態。

五、單繼承和多繼承關系的虛函數表

5.1 單繼承中的虛函數表

在前面 4.1 探究派生類對象模型中,通過下面三種情況基本了解清楚了:

  1. 單繼承,派生類無虛函數覆蓋
  2. 單繼承,派生類有虛函數覆蓋,但無自己的虛函數
  3. 單繼承,派生類有虛函數覆蓋,有自己的虛函數

這里不進行過多的贅述,不過可以將基類和派生類的虛表打印出來進行一個驗證:
取出b、d對象的頭4bytes,就是虛表的指針,前面我們說了虛函數表本質是一個存虛函數
指針的指針數組,這個數組最后面放了一個nullptr

  1. 先取b的地址,強轉成一個int*的指針
  2. 再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針
  3. 再強轉成VFPTR*,因為虛表就是一個存VFPTR類型(虛函數指針類型)的數組。
  4. 虛表指針傳遞給PrintVTable進行打印虛表
  5. 需要說明的是這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后面沒有放nullptr,導致越界,這是編譯器的問題。我們只需要點目錄欄的-生成-清理解決方案,再編譯就好了。
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;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虛表中的虛函數指針打印并調用。調用就可以看出存的是哪個函數cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :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;
}

在這里插入圖片描述

5.2多繼承中的虛函數表

用下面這份代碼來探究一下,多繼承中派生類對象模型以及虛表結構:

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;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{Derive d;cout << " 對象空間的大小: " << sizeof(d) << endl << endl;Base1* ptr1 = &d;Base2* ptr2 = &d;VFPTR* vTableb1 = (VFPTR*)(*(int*)ptr1);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)ptr1 + sizeof(Base1)));PrintVTable(vTableb2);return 0;
}

第一:sizeof(d) 的大小是多少?

對象 d 由三部分成員構成,① Base1 部分的虛表指針及其成員,這里有 8 字節;② Base2 部分的虛表指針及其成員,這里有 8 字節;③ Derive 自己的成員變量,這里有 4 字節,結果應該是 20 字節。
在這里插入圖片描述

第二:賦值兼容轉換的過程是怎樣,或者說,ptr1ptr2 是否相等?

答案是不相等。

  1. 監視窗口中,&d 和 ptr1 的值是一樣的,但是意義不一樣,雖然 &d 和 ptr1 都是指向 對象 d 這塊空間的起始位置,但是指針的類型限制了指針解引用能夠訪問多大的空間,&d 的類型是 Derive* 解引用可以訪問 20 個字節,ptr1 的類型是 Base1* 解引用只能夠訪問 8 個字節。
  2. ptr2 在切片過程中會發生偏移,編譯器會找到 Base2 部分的開始,然后將地址交給 ptr2。
    在這里插入圖片描述

第三:對象 d 中有兩張虛表,Base1 的虛函數指針放在 Base1 部分的虛表,Base2 的虛函數指針放在 Base3 部分的虛表,但是 Derive 中有一個 Func3() 既不屬于 Base1 也不屬于 Base2,它該放到哪張虛表里?

有兩種可能性:①兩張虛表都有 Func3 的函數指針,② Base1部分的虛表里有 Func3 的函數指針
經過測試驗證:在VS平臺下,多繼承體系總派生類的虛函數放在第一個聲明的基類當中。
在這里插入圖片描述

六、多態相關的一些問題

  1. inline 函數能否是虛函數?

inline 函數會在編譯階段原地展開,直接轉換為指令,剩下的建立棧幀帶來的消耗,但是這樣的做法導致 inline 函數沒有函數指針,按道理來說,inline 函數無法稱為虛函數。
但是 inline 只是對編譯器的一個建議,加不加 inline 是否生效取決于編譯器。
如果 inline 虛函數 滿足多態調用,編譯器就會忽略 inline 屬性;
如果 inline 虛函數不滿足多態調用, inline 虛函數依舊可以在原地展開。

class Base
{
public:inline 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; }
private:int _d = 2;
};
int main()
{// inline 虛函數滿足多態調用Base* p = new Derive;p->Func1();// inline 虛函數不滿足多態調用Base b;b.Func1();return 0;
}

在這里插入圖片描述

  1. 靜態成員可以是虛函數嗎?

不能,因為靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。
在這里插入圖片描述

  1. 構造函數可以是虛函數嗎?

不能,因為對象中的虛函數表指針是在構造函數初始化列表階段才初始化的。

  1. 析構函數可以是虛函數嗎?

可以,并且建議虛構函數都定義成虛函數,具體看虛函數重寫的第三個例外。

  1. 對象訪問普通函數快還是虛函數更快?

首先如果是普通調用,結果是一樣快的。
如果是指針對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/696254.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/696254.shtml
英文地址,請注明出處:http://en.pswp.cn/news/696254.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【C++初階】系統實現日期類

目錄 一.運算符重載實現各個接口 1.小于 (d1)<> 2.等于 (d1d2) 3.小于等于&#xff08;d1<d2&#xff09; 4.大于&#xff08;d1>d2&#xff09; 5.大于等于&#xff08;d1>d2&#xff09; 6.不等于&#xff08;d1!d2&#xff09; 7.日期天數 (1) 算…

mac圖片怎么轉換格式jpg?四種高效方法助你輕松搞定JPG格式

mac圖片怎么轉換格式jpg&#xff1f;在數字時代&#xff0c;圖片格式的轉換成為了我們日常操作中的一項基本技能。特別是在使用Mac操作系統的用戶中&#xff0c;如何將圖片轉換為JPG格式成為了一個熱門話題。本文將為你詳細介紹四種簡單實用的方法&#xff0c;幫助你在Mac上輕松…

測試基礎1:偉大航路喲呼(Linux基礎、mysql基礎)

1 測試流程和方法 軟件測試定義&#xff1a; 從方式上看&#xff1a;包含人工測試、自動化測試 從方法上看&#xff1a;運行程序或系統和測定程序或系統的過程 從目的上看&#xff1a;包括找bug和找bug出現的原因 軟件測試的原則&#xff1a;功能性、可靠性、易用性、效率性…

一、網絡基礎知識

1、IP地址和端口號 1.1、IP地址 定義&#xff1a;用于在網絡中唯一標識設備的地址。格式&#xff1a;通常由四個數字組成&#xff0c;以點分十進制表示&#xff0c;例如&#xff1a;192.168.0.1。(IPv4)作用&#xff1a;允許網絡中的設備相互通信&#xff0c;通過IP地址可以定…

Python 數據可視化之密度散點圖 Density Scatter Plot

&#x1f349; CSDN 葉庭云&#xff1a;https://yetingyun.blog.csdn.net/ 密度散點圖&#xff08;Density Scatter Plot&#xff09;&#xff0c;也稱為密度點圖或核密度估計散點圖&#xff0c;是一種數據可視化技術&#xff0c;主要用于展示大量數據點在二維平面上的分布情況…

Swift基礎知識:24.Swift可選鏈

在 Swift 中&#xff0c;可選鏈&#xff08;Optional Chaining&#xff09;是一種用于調用可選類型屬性、方法或下標的安全方式。可選鏈允許我們在調用鏈中的任何一個屬性、方法或下標返回 nil 時&#xff0c;整個調用鏈仍然可以繼續執行&#xff0c;而不會因為其中的任何一個可…

一樣的代碼不同項目跳轉頁面報404的解決辦法

今天收到實施反饋的一個問題&#xff0c;點項目名稱跳轉項目詳情頁面時&#xff0c;有的頁面跳轉顯示正常&#xff0c;有的頁面跳轉報404錯誤。錯誤如下&#xff1a; 發現報錯的項目都有一個共性就是有特殊字符“[ ]” , 解決的辦法就是把帶有特殊字符的字段 用 encodeURI()…

Java SE 入門到精通—4.抽象類與接口【Java】

抽象類 同接口一樣&#xff0c;用來約束子類&#xff0c;限制子類必須擁有某些方法&#xff0c;比普通類多了個抽象方法&#xff0c;用抽象方法該類必為抽象類 概念 沒有具體的對象&#xff0c;具體的方法的一個類 abstract關鍵字聲明為抽象類/方法 一個類中有抽象方法則該…

統計前端傳過來的Req的非空屬性個數的工具類

背景 日常開發中&#xff0c;我們通常會根據前端傳過來的實體類的屬性個數去做邏輯判斷&#xff0c;下面的是判斷屬性個數的工具類。 工具類 public static Integer nonNullFieldCount(Req req) {if (req null) {return 0;}int nonNullFieldCount 0;Field[] fields req.ge…

【Django】Django自定義后臺表單——對一個關聯外鍵對象同時添加多個內容

以官方文檔為例&#xff1a; 一個投票問題包含多個選項&#xff0c;基本的表單設計只能一個選項一個選項添加&#xff0c;效率較低&#xff0c;如何在表單設計中一次性添加多個關聯選項&#xff1f; 示例代碼&#xff1a; from django.contrib import adminfrom .models impo…

Java中的關鍵字有哪些?它們各自的作用是什么?請詳細說明?Java中的訪問修飾符有哪些?它們的訪問權限是怎樣的?

1、Java中的關鍵字有哪些&#xff1f;它們各自的作用是什么&#xff1f;請詳細說明&#xff1f; Java中的關鍵字是預先定義好的&#xff0c;具有特殊含義的標識符&#xff0c;用于表示數據類型、程序結構或控制流程等。以下是Java中的一些常用關鍵字及其作用&#xff1a; abs…

【軟件架構】02-復雜度來源

1、性能 1&#xff09;單機 受限于主機的CPU、網絡、磁盤讀寫速度等影響 在多線程的互斥性、并發中的同步數據狀態等&#xff1b; 擴展&#xff1a;硬件資源、增大線程池 2&#xff09;集群 微服務化拆分&#xff0c;導致調用鏈過長&#xff0c;網絡傳輸的消耗過多。 集…

嵌入式Qt 計算器核心算法_3

一.后綴表達式實現算數運算思路 二.算法實現 #include "QCalculatorDec.h"QCalculatorDec::QCalculatorDec() {m_exp "";m_result ""; }QCalculatorDec::~QCalculatorDec() {}bool QCalculatorDec::isDigitOrDot(QChar c) {return ((0 < c)…

基于SpringBoot的景區旅游管理系統

項目介紹 本期給大家介紹一個 景區旅游管理 系統.。主要模塊有首頁&#xff0c;旅游路線&#xff0c;旅行攻略&#xff0c;在線預定。管理員可以登錄管理后臺對用戶進行管理&#xff0c;可以添加酒店&#xff0c;景區&#xff0c;攻略&#xff0c;路線等信息。整體完成度比較高…

一文搞懂match、match_phrase與match_phrase_prefix的檢索過程

一、在開始之前&#xff0c;完成數據準備&#xff1a; # 創建映射 PUT /tehero_index {"settings": {"index": {"number_of_shards": 1,"number_of_replicas": 1}},"mappings": {"_doc": {"dynamic": …

探索氣膜球幕影院:未來的電影體驗

氣膜球幕影院作為一種新興的電影放映方式&#xff0c;正逐漸成為人們關注的焦點。它采用了充氣式膜結構&#xff0c;可以為觀眾帶來 360 度全景的觀影體驗&#xff0c;讓人仿佛置身于電影之中。本文將介紹氣膜球幕影院的特點、技術原理以及未來的發展前景。 傳說在古代&#x…

Linux系統運維命令:使用 tail,grep組合命令(包括wc,sort,awk,sed等),可以方便的查閱和操作正在改變的日志文件的具體內容

一、命令介紹 1、tail命令 tail命令是Linux系統中常用的命令之一&#xff0c;用于查看文件的末尾內容。它具有許多有用的選項&#xff0c;可以幫助用戶輕松地查找并顯示文件中的信息。 它默認顯示文件的最后10行&#xff0c;但可以通過各種選項來定制輸出的行數、字節數等。ta…

十四、圖像幾何形狀繪制

項目功能實現&#xff1a;矩形、圓形、橢圓等幾何形狀繪制&#xff0c;并與原圖進行相應比例融合 按照之前的博文結構來&#xff0c;這里就不在贅述了 一、頭文件 drawing.h #pragma once#include<opencv2/opencv.hpp>using namespace cv;class DRAWING { public:void…

Python筆記-super().init(root)的作用

假設我們有一個名為Animal的父類&#xff0c;它有一個屬性color&#xff0c;在其構造函數__init__中被初始化&#xff1a; class Animal:def __init__(self, color):self.color color現在&#xff0c;我們想創建一個Animal的子類&#xff0c;名為Dog。Dog類有自己的屬性name&…

QPaint繪制自定義儀表盤組件01

網上抄別人的&#xff0c;只是放這里自己看一下&#xff0c;看完就刪掉 ui Dashboard.pro QT core guigreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c11# You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomm…