溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

發(fā)布時(shí)間:2022-03-04 15:11:55 來(lái)源:億速云 閱讀:158 作者:小新 欄目:開(kāi)發(fā)技術(shù)

這篇文章主要介紹了C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

覆蓋:如果派生類(lèi)中的方法,和基類(lèi)繼承來(lái)的某個(gè)方法,返回值、函數(shù)名、參數(shù)列表都相同,而且基類(lèi)的方法是virtual虛函數(shù),那么派生類(lèi)的這個(gè)方法,自動(dòng)處理成虛函數(shù),它們之間成為覆蓋關(guān)系;也就是說(shuō)派生類(lèi)會(huì)在自己虛函數(shù)表中將從基類(lèi)繼承來(lái)的虛函數(shù)進(jìn)行替換,替換成派生類(lèi)自己的。

靜態(tài)綁定:編譯時(shí)期的多態(tài),通過(guò)函數(shù)的重載以及模板來(lái)實(shí)現(xiàn),也就是說(shuō)調(diào)用函數(shù)的地址在編譯時(shí)期我們就可以確定,在匯編代碼層次,呈現(xiàn)的就是 call 函數(shù)名;

動(dòng)態(tài)綁定:運(yùn)行時(shí)期的多態(tài),通過(guò)派生類(lèi)重寫(xiě)基類(lèi)的虛函數(shù)來(lái)實(shí)現(xiàn)。在匯編代碼層次,呈現(xiàn)的就是 call 寄存器,寄存器的值只有運(yùn)行起來(lái)我們才可以確定。

不存在虛函數(shù)

#include <iostream>
#include <typeinfo>
class Base
{
public:
  Base(int data = 10): ma(data) {}
  ~Base() {};
 
  void show() {
    std::cout << "Base::show()" << std::endl;
  }
 
  void show(int data) {
    std::cout << "Base::show()" << data << std::endl;
  }
 
protected:
  int ma;
 
};
 
class Derive :public Base 
{
public:
  Derive(int data) :Base(data), mb(data) {}
  ~Derive() {}
  void show() {
    std::cout << "Derive::show()" << std::endl;
  }
 
private:
  int mb;
};
 
int main() {
 
  Derive d(50);
  Base *pb = &d;
  pb->show();//靜態(tài)(編譯時(shí)期)綁定(函數(shù)調(diào)用) Base::show (06F12E4h)   
  pb->show(10);//Base::show (06F12BCh)
 
  std::cout << "Base size:" << sizeof(Base) << std::endl;//4
  std::cout << "Derive size:" << sizeof(Derive) << std::endl;//8
 
  std::cout << typeid(pb).name() << std::endl;//class Base *
  std::cout << typeid(*pb).name() << std::endl;//class Base 
 
  return 0;
 
 }

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

打斷點(diǎn),F(xiàn)5進(jìn)入調(diào)試,點(diǎn)擊反匯編

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

可以看到調(diào)用的都是基類(lèi)的show(),在編譯階段已經(jīng)生成指令調(diào)用Base下的show;

可以看到結(jié)果:
因?yàn)閜b是Base類(lèi)型的指針,所以調(diào)用的都是Base類(lèi)的成員方法;
基類(lèi)Base只有一個(gè)數(shù)據(jù)成員ma,所以大小只有4字節(jié);
派生類(lèi)Derive繼承了ma,其次還有自己的mb,所以有8字節(jié);
pb的類(lèi)型是一個(gè)class Base *;
*pb的類(lèi)型是一個(gè)class Base。
為了更好地理解上述過(guò)程,我們簡(jiǎn)單畫(huà)圖如下:

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

為什么Base *類(lèi)型的指針,Derive類(lèi)型的對(duì)象,調(diào)用方法的時(shí)候是Base而不是Derive呢?
原因如上圖:
Derive類(lèi)繼承了Base類(lèi),導(dǎo)致了派生類(lèi)的大小要比基類(lèi)大,而pb的類(lèi)型是基類(lèi)的指針,所以通過(guò)pb調(diào)用方法時(shí)只能訪(fǎng)問(wèn)到Derive中從Base繼承而來(lái)的方法,訪(fǎng)問(wèn)不到自己重寫(xiě)的方法(指針的類(lèi)型限制了指針解引用的能力)

基類(lèi)定義虛函數(shù)

#include <iostream>
#include <typeinfo>
class Base
{
public:
  Base(int data = 10): ma(data) {}
  ~Base() {};
 
  //虛函數(shù)
  virtual void show() {
    std::cout << "Base::show()" << std::endl;
  }
 
  void show(int data) {
    std::cout << "Base::show()" << data << std::endl;
  }
 
protected:
  int ma;
 
};
 
class Derive :public Base 
{
public:
  Derive(int data) :Base(data), mb(data) {}
  ~Derive() {}
  void show() {
    std::cout << "Derive::show()" << std::endl;
  }
 
private:
  int mb;
};
 
int main() {
 
  Derive d(50);
  Base *pb = &d;
 
  /*
  pb->show();
  pb 指針是base類(lèi)型,如果發(fā)現(xiàn)Base中的show是虛函數(shù),就進(jìn)行動(dòng)態(tài)綁定
mov         ecx,dword ptr [pb]  
00292B01 8B 45 D4             mov         eax,dword ptr [pb]   //將pb指向的內(nèi)存前4個(gè)字節(jié)放入ecx寄存器,pb指向derive對(duì)象,前四個(gè)字節(jié)即vfptr,將虛函數(shù)表地址加載到eax
00292B04 8B 10                mov         edx,dword ptr [eax]  //將eax 的前四個(gè)字節(jié) 即Derive::show 加載到edx中
00292B06 8B F4                mov         esi,esp
00292B08 8B 4D D4             mov         ecx,dword ptr [pb]
00292B0B 8B 02                mov         eax,dword ptr [edx]
00292B0D FF D0                call        eax   //虛函數(shù)的地址
00292B0F 3B F4                cmp         esi,esp
00292B11 E8 9C E7 FF FF       call        __RTC_CheckEsp (02912B2h)
我們可以看到這一次,匯編碼call的就不是確切的函數(shù)地址了,而是寄存器eax;
那么就很好理解了:
eax寄存器里存放的是什么內(nèi)容,編譯階段根本無(wú)從知曉,只能在運(yùn)行的時(shí)候確定;
故,動(dòng)態(tài)綁定。
  pb->show(10);  如果發(fā)現(xiàn)show是普通函數(shù),就進(jìn)行靜態(tài)綁定 call Base::show
  
  */
  pb->show();//
  pb->show(10);//
 
  std::cout << "Base size:" << sizeof(Base) << std::endl;//8
  std::cout << "Derive size:" << sizeof(Derive) << std::endl;//12
 
  std::cout << typeid(pb).name() << std::endl;//class Base *
  /*
  pb的類(lèi)型:Base類(lèi)型,查看Base中有沒(méi)有虛函數(shù)
  (1)Base中沒(méi)有虛函數(shù)*pb識(shí)別的就是編譯時(shí)期的類(lèi)型 *pb 就是Base類(lèi)型
  (2) Base中有虛函數(shù),*pb識(shí)別的就是運(yùn)行時(shí)期的類(lèi)型 RTTI類(lèi)型:Derive
  */
  std::cout << typeid(*pb).name() << std::endl;//class Derive 
 
  return 0;
 
 }

在我們添加了virtual關(guān)鍵字后,對(duì)應(yīng)的函數(shù)就變成了虛函數(shù);
那么,一個(gè)類(lèi)添加了虛函數(shù),對(duì)這個(gè)類(lèi)有什么影響呢?

  • 首先,如果類(lèi)里面定義了虛函數(shù),那么編譯階段,編譯器給這個(gè)類(lèi)類(lèi)型產(chǎn)生一個(gè)唯一的vftable虛函數(shù)表,虛函數(shù)表中主要存儲(chǔ)的內(nèi)容是:RTTI(Run-time Type Information)指針和虛函數(shù)的地址,當(dāng)程序運(yùn)行時(shí),每一張?zhí)摵瘮?shù)表都會(huì)加載到內(nèi)存的.rodata區(qū);

  • 一個(gè)類(lèi)里面定義了虛函數(shù),那么這個(gè)類(lèi)定義的對(duì)象,在運(yùn)行時(shí),內(nèi)存中會(huì)多存儲(chǔ)一個(gè)vfptr虛函數(shù)指針,指向了對(duì)應(yīng)類(lèi)型的虛函數(shù)表vftable;

  • 一個(gè)類(lèi)型定義的n個(gè)對(duì)象,他們的vfptr指向的都是同一張?zhí)摵瘮?shù)表;

  • 一個(gè)類(lèi)里面虛函數(shù)的個(gè)數(shù),不影響對(duì)象內(nèi)存的大小(vfptr),影響的是虛函數(shù)表的大小。

  • 如果派生類(lèi)中的方法和從基類(lèi)繼承來(lái)的某個(gè)方法中返回值、函數(shù)名以及參數(shù)列表都相同,且基類(lèi)的方法是virtual,那么派生類(lèi)的這個(gè)方法,自動(dòng)處理成虛函數(shù)

圖示如下:(以Base為例)

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

虛函數(shù)表
1、RTTI,存放的是類(lèi)型信息,也就是(Base或者Derive)
2、偏移地址:虛函數(shù)指針相對(duì)于對(duì)象內(nèi)存空間的偏移,一般vfptr都在0偏移位置
3、下面的函數(shù)時(shí)虛函數(shù)入口地址

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

在Derive類(lèi)中,由于重寫(xiě)了show(),因此在Derive的虛函數(shù)表中,是使用子類(lèi)的show()方法代替了Base類(lèi)的show()

VS的工具來(lái)查看虛函數(shù)表的有關(guān)信息

1 找到

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

2 在打開(kāi)的窗口中切換到當(dāng)前工程所在目錄:

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community>cd C:\Users\Admin\source\repos\C++test\

3 輸入命令:cl XXX.cpp /d1reportSingleClassLayoutXX(第一個(gè)XXX表示源文件的名字,第二個(gè)代表你想查看的類(lèi)類(lèi)型,我這里就是Derive)

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

以看到class Derived的對(duì)象的內(nèi)存布局,在派生類(lèi)對(duì)象的開(kāi)始包含了基類(lèi)Base的對(duì)象,其中有一個(gè)虛表指針,指向的就是下面的Derived::$vftable@ (virtual function table),表中包含了Derived類(lèi)中所有的虛函數(shù)

多重繼承、多繼承 的虛函數(shù)表 1 內(nèi)存分布

假設(shè)有一個(gè)基類(lèi)ClassA,一個(gè)繼承了該基類(lèi)的派生類(lèi)ClassB,并且基類(lèi)中有虛函數(shù),派生類(lèi)實(shí)現(xiàn)了基類(lèi)的虛函數(shù)。
我們?cè)诖a中運(yùn)用多態(tài)這個(gè)特性時(shí),通常以?xún)煞N方式起手:
(1) ClassA *a = new ClassB();
(2) ClassB b; ClassA *a = &b;
以上兩種方式都是用基類(lèi)指針去指向一個(gè)派生類(lèi)實(shí)例,區(qū)別在于第1個(gè)用了new關(guān)鍵字而分配在堆上,第2個(gè)分配在棧上

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

請(qǐng)看上圖,不同兩種方式起手僅僅影響了派生類(lèi)對(duì)象實(shí)例存在的位置。
以左圖為例,ClassA *a是一個(gè)棧上的指針。
該指針指向一個(gè)在堆上實(shí)例化的子類(lèi)對(duì)象?;?lèi)如果存在虛函數(shù),那么在子類(lèi)對(duì)象中,除了成員函數(shù)與成員變量外,編譯器會(huì)自動(dòng)生成一個(gè)指向**該類(lèi)的虛函數(shù)表(這里是類(lèi)ClassB)**的指針,叫作虛函數(shù)表指針。通過(guò)虛函數(shù)表指針,父類(lèi)指針即可調(diào)用該虛函數(shù)表中所有的虛函數(shù)。

2 類(lèi)的虛函數(shù)表與類(lèi)實(shí)例的虛函數(shù)指針

首先不考慮繼承的情況。如果一個(gè)類(lèi)中有虛函數(shù),那么該類(lèi)就有一個(gè)虛函數(shù)表。
這個(gè)虛函數(shù)表是屬于類(lèi)的,所有該類(lèi)的實(shí)例化對(duì)象中都會(huì)有一個(gè)虛函數(shù)表指針去指向該類(lèi)的虛函數(shù)表。
從第一部分的圖中我們也能看到,一個(gè)類(lèi)的實(shí)例要么在堆上,要么在棧上。也就是說(shuō)一個(gè)類(lèi)可以有很多很多個(gè)實(shí)例。但是!一個(gè)類(lèi)只能有一個(gè)虛函數(shù)表。在編譯時(shí),一個(gè)類(lèi)的虛函數(shù)表就確定了,這也是為什么它放在了只讀數(shù)據(jù)段中。

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

3 多態(tài)代碼及多重繼承情況

在第二部分中,我們討論了在沒(méi)有繼承的情況下,虛函數(shù)表的邏輯結(jié)構(gòu)。
那么在有繼承情況下,只要基類(lèi)有虛函數(shù),子類(lèi)不論實(shí)現(xiàn)或沒(méi)實(shí)現(xiàn),都有虛函數(shù)表。

#include <iostream>
 
using namespace std;
 
class ClassA
{
public:
  ClassA() { cout << "ClassA::ClassA()" << endl; }
  virtual ~ClassA() { cout << "ClassA::~ClassA()" << endl; }
 
  void func1() { cout << "ClassA::func1()" << endl; }
  void func2() { cout << "ClassA::func2()" << endl; }
 
  virtual void vfunc1() { cout << "ClassA::vfunc1()" << endl; }
  virtual void vfunc2() { cout << "ClassA::vfunc2()" << endl; }
private:
  int aData;
};
 
class ClassB : public ClassA
{
public:
  ClassB() { cout << "ClassB::ClassB()" << endl; }
  virtual ~ClassB() { cout << "ClassB::~ClassB()" << endl; }
 
  void func1() { cout << "ClassB::func1()" << endl; }
  virtual void vfunc1() { cout << "ClassB::vfunc1()" << endl; }
private:
  int bData;
};
 
class ClassC : public ClassB
{
public:
  ClassC() { cout << "ClassC::ClassC()" << endl; }
  virtual ~ClassC() { cout << "ClassC::~ClassC()" << endl; }
 
  void func2() { cout << "ClassC::func2()" << endl; }
  virtual void vfunc2() { cout << "ClassC::vfunc2()" << endl; }
private:
  int cData;
};
 
 
int main()
{
  ClassC c;
 
  return 0;
}

請(qǐng)看上面代碼
(1) ClassA是基類(lèi), 有普通函數(shù): func1() func2() 。虛函數(shù): vfunc1() vfunc2() ~ClassA()
(2) ClassB繼承ClassA, 有普通函數(shù): func1()。虛函數(shù): vfunc1() ~ClassB()
(3) ClassC繼承ClassB, 有普通函數(shù): func2()。虛函數(shù): vfunc2() ~ClassB()
基類(lèi)的虛函數(shù)表和子類(lèi)的虛函數(shù)表不是同一個(gè)表。下圖是基類(lèi)實(shí)例與多態(tài)情形下,數(shù)據(jù)邏輯結(jié)構(gòu)。注意,虛函數(shù)表是在編譯時(shí)確定的,屬于類(lèi)而不屬于某個(gè)具體的實(shí)例。虛函數(shù)在代碼段,僅有一份
ClassB繼承與ClassA,其虛函數(shù)表是在ClassA虛函數(shù)表的基礎(chǔ)上有所改動(dòng)的,變化的僅僅是在子類(lèi)中重寫(xiě)的虛函數(shù)。如果子類(lèi)沒(méi)有重寫(xiě)任何父類(lèi)虛函數(shù),那么子類(lèi)的虛函數(shù)表和父類(lèi)的虛函數(shù)表在內(nèi)容上是一致的

ClassA *a = new ClassB();
a->func1();                    // "ClassA::func1()"   隱藏了ClassB的func1()
a->func2();                    // "ClassA::func2()"
a->vfunc1();                   // "ClassB::vfunc1()"  重寫(xiě)了ClassA的vfunc1()
a->vfunc2();                   // "ClassA::vfunc2()"

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

這個(gè)結(jié)果不難想象,看上圖,ClassA類(lèi)型的指針a能操作的范圍只能是黑框中的范圍,之所以實(shí)現(xiàn)了多態(tài)完全是因?yàn)樽宇?lèi)的虛函數(shù)表指針與虛函數(shù)表的內(nèi)容與基類(lèi)不同
這個(gè)結(jié)果已經(jīng)說(shuō)明了C++的隱藏、重寫(xiě)(覆蓋)特性。

同理,也就不難推導(dǎo)出ClassC的邏輯結(jié)構(gòu)圖了
類(lèi)的繼承情況是: ClassC繼承ClassB,ClassB繼承ClassA
這是一個(gè)多次單繼承的情況。(多重繼承)

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

4、多繼承下的虛函數(shù)表 (同時(shí)繼承多個(gè)基類(lèi))

多繼承是指一個(gè)類(lèi)同時(shí)繼承了多個(gè)基類(lèi),假設(shè)這些基類(lèi)都有虛函數(shù),也就是說(shuō)每個(gè)基類(lèi)都有虛函數(shù)表,那么該子類(lèi)的邏輯結(jié)果和虛函數(shù)表是什么樣子呢?

#include <iostream>
 
using namespace std;
 
class ClassA1
{
public:
  ClassA1() { cout << "ClassA1::ClassA1()" << endl; }
  virtual ~ClassA1() { cout << "ClassA1::~ClassA1()" << endl; }
 
  void func1() { cout << "ClassA1::func1()" << endl; }
 
  virtual void vfunc1() { cout << "ClassA1::vfunc1()" << endl; }
  virtual void vfunc2() { cout << "ClassA1::vfunc2()" << endl; }
private:
  int a1Data;
};
 
class ClassA2
{
public:
  ClassA2() { cout << "ClassA2::ClassA2()" << endl; }
  virtual ~ClassA2() { cout << "ClassA2::~ClassA2()" << endl; }
 
  void func1() { cout << "ClassA2::func1()" << endl; }
 
  virtual void vfunc1() { cout << "ClassA2::vfunc1()" << endl; }
  virtual void vfunc2() { cout << "ClassA2::vfunc2()" << endl; }
  virtual void vfunc4() { cout << "ClassA2::vfunc4()" << endl; }
private:
  int a2Data;
};
 
class ClassC : public ClassA1, public ClassA2
{
public:
  ClassC() { cout << "ClassC::ClassC()" << endl; }
  virtual ~ClassC() { cout << "ClassC::~ClassC()" << endl; }
 
  void func1() { cout << "ClassC::func1()" << endl; }
 
  virtual void vfunc1() { cout << "ClassC::vfunc1()" << endl; }
  virtual void vfunc2() { cout << "ClassC::vfunc2()" << endl; }
  virtual void vfunc3() { cout << "ClassC::vfunc3()" << endl; }
};
 
 
int main()
{
  ClassC c;
 
  return 0;
}

ClassA1是第一個(gè)基類(lèi),擁有普通函數(shù)func1(),虛函數(shù)vfunc1() vfunc2()。
ClassA2是第二個(gè)基類(lèi),擁有普通函數(shù)func1(),虛函數(shù)vfunc1() vfunc2(),vfunc4()。
ClassC依次繼承ClassA1、ClassA2。普通函數(shù)func1(),虛函數(shù)vfunc1() vfunc2() vfunc3()。

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

在多繼承情況下,有多少個(gè)基類(lèi)就有多少個(gè)虛函數(shù)表指針,前提是基類(lèi)要有虛函數(shù)才算上這個(gè)基類(lèi)。
如圖,虛函數(shù)表指針01指向的虛函數(shù)表是以ClassA1的虛函數(shù)表為基礎(chǔ)的,子類(lèi)的ClassC::vfunc1(),和vfunc2()的函數(shù)指針覆蓋了虛函數(shù)表01中的虛函數(shù)指針01的位置、02位置。當(dāng)子類(lèi)有多出來(lái)的虛函數(shù)時(shí),添加在第一個(gè)虛函數(shù)表中。注意:
1.子類(lèi)虛函數(shù)會(huì)覆蓋每一個(gè)父類(lèi)的每一個(gè)同名虛函數(shù)。
2.父類(lèi)中沒(méi)有的虛函數(shù)而子類(lèi)有,填入第一個(gè)虛函數(shù)表中,且用父類(lèi)指針是不能調(diào)用。
3.父類(lèi)中有的虛函數(shù)而子類(lèi)沒(méi)有,則不覆蓋。僅子類(lèi)和該父類(lèi)指針能調(diào)用

虛基類(lèi)和多重繼承

什么是多重繼承

多重繼承,很好理解,一個(gè)派生類(lèi)如果只繼承一個(gè)基類(lèi),稱(chēng)作單繼承;
一個(gè)派生類(lèi)如果繼承了多個(gè)基類(lèi),稱(chēng)作多繼承。
如圖所示:

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

多重繼承的優(yōu)點(diǎn)
這個(gè)很好理解:
多重繼承可以做更多的代碼復(fù)用!
派生類(lèi)通過(guò)多重繼承,可以得到多個(gè)基類(lèi)的數(shù)據(jù)和方法,更大程度的實(shí)現(xiàn)了代碼復(fù)用。

關(guān)于菱形繼承的問(wèn)題
凡事有利也有弊,對(duì)于多繼承而言,也有自己的缺點(diǎn)。
我們先通過(guò)了解菱形繼承來(lái)探究多重繼承的缺點(diǎn):
菱形繼承是多繼承的一種情況,繼承方式如圖所示:

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

從圖中我們可以看到:
類(lèi)B類(lèi)C類(lèi)A單繼承而來(lái);
類(lèi)D類(lèi)B類(lèi)C多繼承而來(lái)。
那么這樣繼承會(huì)產(chǎn)生什么問(wèn)題呢?
我們來(lái)看代碼:

#include <iostream>
 
using namespace std;
class A
{
public:
  A(int data) :ma(data) { cout << "A()" << endl; }
  ~A() { cout << "~A()" << endl; }
protected:
  int ma;
};
class B :public A
{
public:
  B(int data) :A(data), mb(data) { cout << "B()" << endl; }
  ~B() { cout << "~B()" << endl; }
protected:
  int mb;
};
class C :public A
{
public:
  C(int data) :A(data), mc(data) { cout << "C()" << endl; }
  ~C() { cout << "~C()" << endl; }
protected:
  int mc;
};
class D :public B, public C
{
public:
  D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
  ~D() { cout << "~D()" << endl; }
protected:
  int md;
};
int main()
{
  D d(10);
 
  return 0;
}

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

通過(guò)運(yùn)行結(jié)果,我們發(fā)現(xiàn)了問(wèn)題:
對(duì)于基類(lèi)A而言,構(gòu)造了兩次,析構(gòu)了兩次!
并且,通過(guò)分析各個(gè)派生類(lèi)的內(nèi)存布局我們可以看到:

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

對(duì)于派生類(lèi)D來(lái)說(shuō),間接繼承的基類(lèi)A中的數(shù)據(jù)成員ma重復(fù)了!
這對(duì)資源來(lái)說(shuō)是一種浪費(fèi)與消耗。
(如果多繼承的數(shù)量增加,那么派生類(lèi)中重復(fù)的數(shù)據(jù)也會(huì)增加?。?/p>

查看D類(lèi)的內(nèi)存布局:

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

其他多重繼承的情況

除了菱形繼承外,還有其他多重繼承的情況,也會(huì)出現(xiàn)相同的問(wèn)題

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

比如說(shuō)圖中呈現(xiàn)的:半圓形繼承。

如何解決多重繼承的問(wèn)題

通過(guò)分析我們知道了,多重繼承的主要問(wèn)題是,通過(guò)多重繼承,有可能得到重復(fù)的基類(lèi)數(shù)據(jù),并且可能重復(fù)的構(gòu)造和析構(gòu)同一個(gè)基類(lèi)對(duì)象。
那么如何能夠避免重復(fù)現(xiàn)象的產(chǎn)生呢?
答案就是:=》虛基類(lèi)。

什么是虛基類(lèi)
要理解虛基類(lèi),我們首先需要認(rèn)識(shí)virtual關(guān)鍵字的使用場(chǎng)景:

修飾成員方法時(shí):產(chǎn)生虛函數(shù);
修飾繼承方式時(shí):產(chǎn)生虛基類(lèi)。
對(duì)于被虛繼承的類(lèi),稱(chēng)作虛基類(lèi)。
比如說(shuō):

class A
{
	XXXXXX;
};
class B : virtual public A
{
	XXXXXX;
};

對(duì)于這個(gè)示例而言,B虛繼承了A,所以把A稱(chēng)作虛基類(lèi)。

虛基類(lèi)如何解決問(wèn)題

那么虛基類(lèi)如何解決上述多重繼承產(chǎn)生的重復(fù)問(wèn)題呢?
我們來(lái)看代碼:

#include <iostream>
 
using namespace std;
class A
{
public:
  A(int data) :ma(data) { cout << "A()" << endl; }
  ~A() { cout << "~A()" << endl; }
protected:
  int ma;
};
class B :virtual public A
{
public:
  B(int data) :A(data), mb(data) { cout << "B()" << endl; }
  ~B() { cout << "~B()" << endl; }
protected:
  int mb;
};
class C :virtual public A
{
public:
  C(int data) :A(data), mc(data) { cout << "C()" << endl; }
  ~C() { cout << "~C()" << endl; }
protected:
  int mc;
};
class D :public B, public C
{
public:
  D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
  ~D() { cout << "~D()" << endl; }
protected:
  int md;
};

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

提示說(shuō):"A::A" : 沒(méi)有合適的默認(rèn)構(gòu)造函數(shù)可用;
為什么會(huì)這樣呢?
我們可以這么理解:

剛開(kāi)始BC單繼承A的時(shí)候,實(shí)例化對(duì)象時(shí),會(huì)首先調(diào)用基類(lèi)的構(gòu)造函數(shù),也就是A的構(gòu)造函數(shù),到了D,由于多繼承了BC,所以在實(shí)例化D的對(duì)象時(shí),會(huì)首先調(diào)用BC的構(gòu)造函數(shù),然后調(diào)用自己(D)的。

但是這樣會(huì)出現(xiàn)A重復(fù)構(gòu)造的問(wèn)題,所以,采用虛繼承,把有關(guān)重復(fù)的基類(lèi)A改為虛基類(lèi),這樣的話(huà),對(duì)于A構(gòu)造的任務(wù)就落到了最終派生類(lèi)D的頭上,但是我們的代碼中,對(duì)于D的構(gòu)造函數(shù):D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }并沒(méi)有對(duì)A進(jìn)行構(gòu)造。
所以會(huì)報(bào)錯(cuò)。
那么我們就給D的構(gòu)造函數(shù),調(diào)用A的構(gòu)造函數(shù):
D(int data) :A(data), B(data), C(data), md(data) { cout << "D()" << endl; }
這一次再運(yùn)行

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

我們會(huì)發(fā)現(xiàn),問(wèn)題解決了。

查看虛基類(lèi)的內(nèi)存布局

我們可以看到當(dāng)前B的內(nèi)存空間:

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

當(dāng)前B的內(nèi)存空間里,前四個(gè)字節(jié)是vbptr(這個(gè)就代表里虛基類(lèi)指針:virtual base ptr);
vfptr(虛函數(shù)指針)指向了vftable(虛函數(shù)表)一樣,
vbptr(虛基類(lèi)指針)指向了vbtable(虛基類(lèi)表)。

vbtable(虛基類(lèi)表)的布局也如圖所示,
首先是偏移量0:表示了虛基類(lèi)指針再內(nèi)存布局中的偏移量;
接著是偏移量8:表示從虛基類(lèi)中繼承而來(lái)的數(shù)據(jù)成員在內(nèi)存中的偏移量。

對(duì)比普通繼承下的內(nèi)存布局

我們可以對(duì)比沒(méi)有虛繼承下的B的內(nèi)存布局來(lái)理解:

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

我們把他們放在一起對(duì)比可以看到:

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

繼承虛基類(lèi)的類(lèi)(BC)會(huì)把自己從虛基類(lèi)繼承而來(lái)的數(shù)據(jù)ma放在自己內(nèi)存的最末尾(偏移量最大),并在原來(lái)ma的位置填充一個(gè)vbptr(虛基類(lèi)指針),這個(gè)指針指向了vbtable(虛基類(lèi)表)。
理解了B,我們可以看看更為復(fù)雜的D

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析

可以看到,將ma移動(dòng)到了末尾處,并在含有ma的地方,都用vbptr進(jìn)行填充。
這樣一來(lái),就只有一個(gè)ma了!解決了多重繼承的重復(fù)問(wèn)題。

感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“C++虛函數(shù)與靜態(tài)、動(dòng)態(tài)綁定的示例分析”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來(lái)學(xué)習(xí)!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

c++
AI