C++的虛繼承實現(xiàn)示例
一、虛繼承的核心定位:解決菱形繼承的痛點
在講解虛繼承前,先明確其誕生的背景——菱形繼承(鉆石繼承) 是多重繼承的典型問題,而虛繼承是C++專門設(shè)計的解決方案:
- 菱形繼承:多個基類繼承自同一個“共同基類”,最終派生類又同時繼承這些基類,導(dǎo)致共同基類的成員在最終派生類中存在多份副本(數(shù)據(jù)冗余),訪問時引發(fā)二義性。
- 虛繼承:通過
virtual關(guān)鍵字聲明繼承,讓“共同基類”成為虛基類,使其在最終派生類中僅保留一份實例,徹底解決數(shù)據(jù)冗余和二義性。
二、虛繼承的基本語法與核心概念
1. 語法格式
虛繼承的關(guān)鍵字virtual需加在“繼承方式”前,修飾的是“繼承行為”,而非基類本身:
// 格式:class 派生類 : virtual 繼承方式 虛基類 { ... };
class 中間基類1 : virtual public 共同基類 { ... };
class 中間基類2 : virtual public 共同基類 { ... };
class 最終派生類 : public 中間基類1, public 中間基類2 { ... };
- 虛基類:被
virtual繼承的“共同基類”(比如下面示例中的Animal); - 最終派生類:菱形結(jié)構(gòu)最底層的類(比如下面示例中的
Duck),是唯一負(fù)責(zé)初始化虛基類的類。
2. 先看問題:普通菱形繼承的坑
先通過代碼復(fù)現(xiàn)菱形繼承的核心問題(數(shù)據(jù)冗余+二義性),讓你直觀感受為什么需要虛繼承:
#include <iostream>
using namespace std;
// 共同基類:Animal
class Animal {
public:
int age;
Animal(int a) : age(a) {
cout << "Animal構(gòu)造,age=" << a << endl;
}
};
// 中間基類1:Flyable(普通繼承Animal)
class Flyable : public Animal {
public:
Flyable(int a) : Animal(a) {} // 初始化Animal
void fly() { cout << "能飛,age=" << age << endl; }
};
// 中間基類2:Swimmable(普通繼承Animal)
class Swimmable : public Animal {
public:
Swimmable(int a) : Animal(a) {} // 初始化Animal
void swim() { cout << "能游泳,age=" << a << age << endl; }
};
// 最終派生類:Duck(多重繼承Flyable、Swimmable)
class Duck : public Flyable, public Swimmable {
public:
// 必須初始化兩個中間基類,間接初始化兩次Animal
Duck(int a) : Flyable(a), Swimmable(a) {}
};
int main() {
Duck duck(2);
// 問題1:二義性——編譯器不知道訪問哪一份age
// cout << duck.age << endl; // 編譯報錯:ambiguous reference to 'age'
// 問題2:數(shù)據(jù)冗余——存在兩份age,地址不同
cout << &duck.Flyable::age << endl; // 0x7ffeefbff5e0
cout << &duck.Swimmable::age << endl;// 0x7ffeefbff5e4
return 0;
}
輸出(構(gòu)造階段):
Animal構(gòu)造,age=2 // Flyable初始化的Animal
Animal構(gòu)造,age=2 // Swimmable初始化的Animal
核心問題總結(jié):
Duck對象中有兩份age,浪費內(nèi)存;- 直接訪問
age編譯報錯,必須通過Flyable::或Swimmable::限定作用域; Animal被構(gòu)造了兩次,不符合邏輯(一只鴨子只有一個年齡)。
3. 用虛繼承解決問題
僅需修改Flyable和Swimmable的繼承方式,添加virtual關(guān)鍵字:
#include <iostream>
using namespace std;
class Animal {
public:
int age;
Animal(int a) : age(a) {
cout << "Animal構(gòu)造,age=" << a << endl; // 僅構(gòu)造一次
}
};
// 關(guān)鍵修改:virtual public Animal
class Flyable : virtual public Animal {
public:
Flyable(int a) : Animal(a) {} // 此初始化會被忽略!
};
// 關(guān)鍵修改:virtual public Animal
class Swimmable : virtual public Animal {
public:
Swimmable(int a) : Animal(a) {} // 此初始化會被忽略!
};
class Duck : public Flyable, public Swimmable {
public:
// 核心規(guī)則:最終派生類必須直接初始化虛基類Animal
Duck(int a) : Animal(a), Flyable(a), Swimmable(a) {}
};
int main() {
Duck duck(2);
// 問題解決1:無二義性,直接訪問age
cout << duck.age << endl; // 輸出2
// 問題解決2:數(shù)據(jù)冗余消除,只有一份age
cout << &duck.Flyable::age << endl; // 0x7ffeefbff5e0
cout << &duck.Swimmable::age << endl;// 0x7ffeefbff5e0(和上面地址相同)
return 0;
}
輸出(構(gòu)造階段):
Animal構(gòu)造,age=2 // 僅構(gòu)造一次!
核心變化總結(jié):
Animal僅被構(gòu)造一次,Duck中只有一份age;- 直接訪問
duck.age無編譯錯誤,二義性徹底解決; - 兩份
age的地址完全相同,證明數(shù)據(jù)冗余消除。
三、虛繼承的核心規(guī)則(必須牢記)
1. 虛基類的初始化規(guī)則(最關(guān)鍵)
- 最終派生類負(fù)責(zé)初始化虛基類:無論中間基類是否寫了虛基類的初始化代碼,都會被編譯器忽略,只有最終派生類的初始化才生效;
- 若最終派生類未顯式初始化虛基類,且虛基類無默認(rèn)構(gòu)造函數(shù),編譯報錯。
2. 虛基類的實例唯一性
- 虛基類的實例在最終派生類中全局唯一,所有中間基類都共享這一份實例;
- 即使最終派生類有多層繼承,虛基類也僅構(gòu)造一次。
3. 作用域解析的優(yōu)先級
- 若最終派生類有和虛基類同名的成員,直接訪問時優(yōu)先訪問最終派生類的成員;
- 若需訪問虛基類的成員,需通過
虛基類名::限定。
四、虛繼承的底層原理(新手簡化版)
虛繼承的實現(xiàn)依賴編譯器的兩個核心機(jī)制(無需深入底層,理解概念即可):
- 虛基類指針(vbptr):每個虛繼承的中間基類(如
Flyable、Swimmable)會在對象中添加一個隱藏的指針vbptr; - 虛基類表(vbtable):編譯器為每個虛繼承的類生成一張表,記錄
vbptr到虛基類實例(如Animal)的內(nèi)存偏移量。
工作流程:當(dāng)訪問duck.Flyable::age時,編譯器通過Flyable的vbptr找到vbtable,再通過偏移量定位到唯一的Animal::age,從而避免二義性和冗余。
提示:虛繼承有輕微的性能開銷(指針訪問+表查詢),但在現(xiàn)代編譯器下幾乎可以忽略,僅需在“必須解決菱形繼承”時使用,不要濫用。
五、虛繼承與虛函數(shù)的區(qū)別(易混淆點)
很多新手會混淆“虛繼承”和“虛函數(shù)”,核心區(qū)別如下:
| 特性 | 虛繼承(virtual inheritance) | 虛函數(shù)(virtual function) |
|---|---|---|
| 關(guān)鍵字 | virtual修飾繼承行為 | virtual修飾成員函數(shù) |
| 核心目的 | 解決菱形繼承的二義性和數(shù)據(jù)冗余 | 實現(xiàn)運行時多態(tài)(動態(tài)綁定) |
| 底層實現(xiàn) | 依賴vbptr + vbtable | 依賴vptr + vtable |
| 作用范圍 | 類的繼承體系 | 類的成員函數(shù) |
六、總結(jié)
- 核心目的:虛繼承是C++解決多重繼承中菱形繼承問題的專屬機(jī)制,通過讓共同基類成為虛基類,保證其在最終派生類中僅存在一份實例,消除數(shù)據(jù)冗余和訪問二義性;
- 核心規(guī)則:虛基類的構(gòu)造函數(shù)由最終派生類統(tǒng)一初始化,中間基類對虛基類的初始化會被忽略;
- 使用場景:僅在遇到菱形繼承時使用,避免濫用(可優(yōu)先考慮組合/接口替代多重繼承);
- 易混點:虛繼承≠虛函數(shù),前者解決繼承冗余,后者實現(xiàn)多態(tài),底層機(jī)制不同但都依賴編譯器的隱藏指針和表。
到此這篇關(guān)于C++的虛繼承實現(xiàn)示例的文章就介紹到這了,更多相關(guān)C++ 虛繼承內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
clion最新激活碼+漢化的步驟詳解(親測可用激活到2089)
這篇文章主要介紹了clion最新版下載安裝+破解+漢化的步驟詳解,本文分步驟給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11
C++實現(xiàn)LeetCode(134.加油站問題)
這篇文章主要介紹了C++實現(xiàn)LeetCode(134.加油站問題),本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
C/C++細(xì)數(shù)宏與函數(shù)有那些區(qū)別
在C程序中,可以用宏代碼提高執(zhí)行效率。宏代碼本身不是函數(shù),但使用起來象函數(shù)。預(yù)處理器用復(fù)制宏代碼的方式代替函數(shù)調(diào)用,省去了參數(shù)壓棧、生成匯編語言的CALL調(diào)用、返回參數(shù)、執(zhí)行return等過程,從而提高了速度2022-10-10
C++實現(xiàn)學(xué)校運動會管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++實現(xiàn)學(xué)校運動會管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-10-10
VS2017+Qt5+Opencv3.4調(diào)用攝像頭拍照并存儲
本文主要介紹了VS2017+Qt5+Opencv3.4調(diào)用攝像頭拍照并存儲,實現(xiàn)了視頻,拍照,保存這三個功能。具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-05-05

