C++語言學(xué)習(xí)(十六)——多繼承-創(chuàng)新互聯(lián)
C++語言學(xué)習(xí)(十六)——多繼承
一、多繼承簡介
1、多繼承簡介
C++語言支持多繼承,一個(gè)子類可以有多個(gè)父類,子類擁有所有父類的成員變量,子類繼承所有父類的成員函數(shù),子類對(duì)象可以當(dāng)作任意父類對(duì)象使用。
2、多繼承語法規(guī)則
class Derived : public BaseA,
public BaseB,
public BaseC
{
};
3、多繼承派生類的內(nèi)存布局
通過多重繼承得到的派生類對(duì)象可能具有不同的地址。
#include
using namespace std;
class BaseA
{
public:
BaseA(int a)
{
ma = a;
}
private:
int ma;
};
class BaseB
{
public:
BaseB(int b)
{
mb = b;
}
private:
int mb;
};
class Derived : public BaseA,public BaseB
{
public:
Derived(int a, int b, int c):BaseA(a),BaseB(b)
{
mc = c;
}
private:
int mc;
};
struct Test
{
int a;
int b;
int c;
};
int main(int argc, char *argv[])
{
Derived d(1,2,3);
cout << sizeof(d) << endl;//12
Test* p = (Test*)&d;
cout << p->a << endl;//1
cout << p->b << endl;//2
cout << p->c << endl;//3
cout << &p->a << endl;//1
cout << &p->b << endl;//2
cout << &p->c << endl;//3
BaseA* pa = &d;
BaseB* pb = &d;
//子類對(duì)象的地址、首位繼承類的成員地址
cout << &d << endl;
cout << pa << endl;
cout << &p->a <b << endl;
return 0;
}
上述代碼中,Derived類對(duì)象的內(nèi)存布局如下:
Derived類對(duì)象從基類繼承而來的處成員變量將根據(jù)繼承的聲明順序進(jìn)行依次排布?;谫x值兼容原則,如果BaseA類型指針pa、BaseB類型指針pb都指向子類對(duì)象d,pa將得到BaseA基類成員變量ma的地址,即子類對(duì)象的地址;pb將得到BaseB類成員變量mb的地址;因此,pa與pb的地址不相同。
4、菱形多繼承導(dǎo)致的成員冗余
上述類圖中,Teacher類和Student類都會(huì)繼承People的成員,Doctor會(huì)繼承Teacher類和Student類的成員,因此Doctor將會(huì)有兩份繼承自頂層父類People的成員。
#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
void print()
{
cout << "name: " << m_name
<< " age: " << m_age <
二、虛繼承
1、虛繼承簡介
在多繼承中,保存共同基類的多份同名成員,可以在不同的數(shù)據(jù)成員中分別存放不同的數(shù)據(jù),但保留多份數(shù)據(jù)成員的拷貝,不僅占有較多的存儲(chǔ)空間,增加了成員的冗余,還增加了訪問的困難。C++提供了虛基類和虛繼承機(jī)制,實(shí)現(xiàn)了在多繼承中只保留一份共同成員。
C++對(duì)于菱形多繼承導(dǎo)致的成員冗余問題的解決方案是使用虛繼承。
虛繼承中,中間層父類不再關(guān)注頂層父類的初始化,最終子類必須直接調(diào)用頂層父類的構(gòu)造函數(shù)。
虛繼承的語法如下:
class 派生類名:virtual 繼承方式 基類名
2、虛繼承示例
#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
void print()
{
cout << "name: " << m_name
<< " age: " << m_age <
上述代碼中,使用虛繼承解決了成員冗余的問題。
虛繼承解決了多繼承產(chǎn)生的數(shù)據(jù)冗余問題,但是中間層父類不再關(guān)心頂層父類的初始化,最終子類必須直接調(diào)用頂層父類的構(gòu)造函數(shù)。
三、多繼承派生類的對(duì)象模型
1、多繼承派生類對(duì)象的內(nèi)存布局

上述類圖中,Derived類繼承自BaseA和BaseB類,funcA和funcB為虛函數(shù),Derived對(duì)象模型如下:

#include
#include
using namespace std;
class BaseA
{
public:
BaseA(int a)
{
m_a = a;
}
virtual void funcA()
{
cout << "BaseA::funcA()" <a <b <c <vptrA <vptrB <
2、菱形繼承派生類對(duì)象的內(nèi)存布局
菱形繼承示例代碼如下:
#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
void print()
{
cout << "name: " << m_name
<< " age: " << m_age <name1 << endl;
cout << pTest->age1 << endl;
cout << pTest->research << endl;
cout << pTest->name2 << endl;
cout << pTest->age2 << endl;
cout << pTest->major << endl;
cout << pTest->subject << endl;
return 0;
}
// output:
// Doctor size: 28
// Bauer_1
// 31
// Computer
// Bauer_2
// 32
// Computer Engneering
// HPC
上述代碼中,底層子類對(duì)象的內(nèi)存局部如下:

底層子類對(duì)象中,分別繼承了中間層父類從頂層父類繼承而來的成員變量,因此內(nèi)存模型中含有兩份底層父類的成員變量。
如果頂層父類含有虛函數(shù),中間層父類會(huì)分別繼承頂層父類的虛函數(shù)表指針,因此,底層子類對(duì)象內(nèi)存布局如下:

#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
virtual void print()
{
cout << "name: " << m_name
<< " age: " << m_age <vptr1 << endl;
cout << pTest->name1 << endl;
cout << pTest->age1 << endl;
cout << pTest->research << endl;
cout << pTest->vptr2 << endl;
cout << pTest->name2 << endl;
cout << pTest->age2 << endl;
cout << pTest->major << endl;
cout << pTest->subject << endl;
return 0;
}
// output:
// Doctor size: 28
// 0x405370
// Bauer_1
// 31
// Computer
// 0x40537c
// Bauer_2
// 32
// Computer Engneering
// HPC
3、虛繼承派生類對(duì)象的內(nèi)存布局
虛繼承是解決C++多重繼承問題的一種手段,虛繼承的底層實(shí)現(xiàn)原理與C++編譯器相關(guān),一般通過虛基類指針和虛基類表實(shí)現(xiàn),每個(gè)虛繼承的子類都有一個(gè)虛基類指針(占用一個(gè)指針的存儲(chǔ)空間,4(8)字節(jié))和虛基類表(不占用類對(duì)象的存儲(chǔ)空間)(虛基類依舊會(huì)在子類里面存在拷貝,只是僅僅最多存在一份);當(dāng)虛繼承的子類被當(dāng)做父類繼承時(shí),虛基類指針也會(huì)被繼承。
在虛繼承情況下,底層子類對(duì)象的布局不同于普通繼承,需要多出一個(gè)指向中間層父類對(duì)象的虛基類表指針vbptr。
vbptr是虛基類表指針(virtual base table pointer),vbptr指針指向一個(gè)虛基類表(virtual table),虛基類表存儲(chǔ)了虛基類相對(duì)直接繼承類的偏移地址;通過偏移地址可以找到虛基類成員,虛繼承不用像普通多繼承維持著公共基類(虛基類)的兩份同樣的拷貝,節(jié)省了存儲(chǔ)空間。
#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
void print()
{
cout << "this: " << this <vbptr_left << endl;
cout << *(int*)pTest->vbptr_left << endl;
cout << pTest->research << endl;
cout << pTest->vbptr_right << endl;
cout << *(int*)pTest->vbptr_right << endl;
cout << pTest->major << endl;
cout << pTest->subject << endl;
cout << pTest->name << endl;
cout << pTest->age << endl;
return 0;
}
// output:
// Doctor size: 28
// 0x40539c
// 12
// Computer
// 0x4053a8
// 0
// Computer Engneering
// HPC
// Bauer
// 30
上述代碼沒有虛函數(shù),在G++編譯器打印結(jié)果如上,底層子類對(duì)象的內(nèi)存布局如下:

#include
#include
using namespace std;
class People
{
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
virtual void print()
{
cout << "this: " << this <vbptr_left << endl;
cout << std::hex << *(int*)pTest->vbptr_left << endl;
cout << std::dec << *((int*)pTest->vbptr_left+8) << endl;
cout << std::dec << *((int*)pTest->vbptr_left+16) << endl;
cout << std::dec << *((int*)pTest->vbptr_left+24) << endl;
cout << pTest->research << endl;
cout << pTest->vbptr_right << endl;
cout << pTest->major << endl;
cout << pTest->subject << endl;
cout << pTest->vptr_base << endl;
cout << pTest->name << endl;
cout << pTest->age << endl;
return 0;
}
上述代碼中,使用了虛繼承,因此不同的C++編譯器實(shí)現(xiàn)原理不同。
對(duì)于GCC編譯器,People對(duì)象大小為char + int + 虛函數(shù)表指針,Teacher對(duì)象大小為char+虛基類表指針+A類型的大小,Student對(duì)象大小為char+虛基類表指針+A類型的大小,Doctor對(duì)象大小為char + int +虛函數(shù)表指針+char+虛基類表指針+char+虛基類表指針+char*。中間層父類共享頂層父類的虛函數(shù)表指針,沒有自己的虛函數(shù)表指針,虛基類指針不共享,因此都有自己獨(dú)立的虛基類表指針。
VC++、GCC和Clang編譯器的實(shí)現(xiàn)中,不管是否是虛繼承還是有虛函數(shù),其虛基類指針都不共享,都是單獨(dú)的。對(duì)于虛函數(shù)表指針,VC++編譯器根據(jù)是否為虛繼承來判斷是否在繼承關(guān)系中共享虛表指針。如果子類是虛繼承擁有虛函數(shù)父類,且子類有新加的虛函數(shù)時(shí),子類中則會(huì)新加一個(gè)虛函數(shù)表指針;GCC編譯器和Clang編譯器的虛函數(shù)表指針在整個(gè)繼承關(guān)系中共享的。
G++編譯器對(duì)于類的內(nèi)存分布和虛函數(shù)表信息命令如下:
g++ -fdump-class-hierarchy main.cpp
cat main.cpp.002t.class
VC++編譯器對(duì)于類的內(nèi)存分布和虛函數(shù)表信息命令如下:
cl main.cpp /d1reportSingleClassLayoutX
Clang編譯器對(duì)于類的內(nèi)存分布和虛函數(shù)表信息命令如下:
clang -Xclang -fdump-record-layouts
4、多繼承派生類的虛函數(shù)表
所有的虛函數(shù)都保存在虛函數(shù)表中,多重繼承可能產(chǎn)生多個(gè)虛函數(shù)表。多繼承中,當(dāng)子類對(duì)父類的虛函數(shù)重寫時(shí),子類的函數(shù)覆蓋父類的函數(shù)在對(duì)應(yīng)虛函數(shù)表中的虛函數(shù)位置;當(dāng)子類有新的虛函數(shù)時(shí),新的虛函數(shù)被加到第一個(gè)基類的虛函數(shù)表的末尾。當(dāng)dynamic_cast對(duì)子類對(duì)象進(jìn)行轉(zhuǎn)換時(shí),子類和第一個(gè)基類的地址相同,不需要移動(dòng)指針,但當(dāng)dynamic_cast轉(zhuǎn)換子類到其他父類時(shí),需要做相應(yīng)的指針調(diào)整。
四、多繼承的指針類型轉(zhuǎn)換
1、多繼承中指針類型轉(zhuǎn)換的陷阱
C++語言中,通常對(duì)指針進(jìn)行類型轉(zhuǎn)換,不會(huì)改變指針的值,只會(huì)改變指針的類型(即改變編譯器對(duì)該指針指向內(nèi)存的解釋方式),但在C++多重繼承中并不成立。
#include
using namespace std;
class BaseA
{
public:
BaseA(int value = 0)
{
data = value;
}
virtual void printA()
{
cout << "BaseA::print data = " << data << endl;
}
protected:
int data;
};
class BaseB
{
public:
BaseB(int value = 0)
{
data = value;
}
virtual void printB()
{
cout << "BaseB::print data = " << data << endl;
}
protected:
int data;
};
class Derived : public BaseA, public BaseB
{
public:
Derived(int value = 0)
{
data = value;
}
virtual void printA()
{
cout << "Derived printA data = " << data << endl;
}
virtual void printB()
{
cout << "Derived printB data = " << data << endl;
}
protected:
int data;
};
int main(int argc, char *argv[])
{
Derived* dpd = new Derived(102);
cout << dpd << endl;//0x8d1190
BaseA* bpa = (BaseA*)dpd;
cout << bpa << endl;//0x8d1190
BaseB* bpb = (BaseB*)dpd;
cout << bpb << endl;//0x8d1198
cout << (dpd == bpb) << endl;//1
return 0;
}
上述代碼中,指向Derived對(duì)象的指針轉(zhuǎn)換為基類BaseA和BaseB后,指針值并不相同。dpd指針、bpa指針與bpb指針相差8個(gè)字節(jié)的地址空間,即BaseA類虛函數(shù)表指針與data成員占用的空間。
將一個(gè)派生類的指針轉(zhuǎn)換成某一個(gè)基類指針,C++編譯器會(huì)將指針的值偏移到該基類在對(duì)象內(nèi)存中的起始位置。
cout << (dpd == bpb) << endl;//1
上述代碼打印出1,C++編譯器屏蔽了指針的差異,當(dāng)C++編譯器遇到一個(gè)指向派生類的指針和指向其某個(gè)基類的指針進(jìn)行==運(yùn)算時(shí),會(huì)自動(dòng)將指針做隱式類型提升以屏蔽多重繼承帶來的指針差異。
2、多繼承中派生類、基類指針類型轉(zhuǎn)換
派生類對(duì)象指針轉(zhuǎn)換為不同基類對(duì)象指針時(shí),C++編譯器會(huì)按照派生類聲明的繼承順序,轉(zhuǎn)換為第一基類時(shí)指針不變,以后依次向后偏移前一基類所占字節(jié)數(shù)。
多繼承下,指針類型轉(zhuǎn)換需要考慮this指針調(diào)整的問題。
五、多繼承應(yīng)用示例
多繼承中,如果中間層父類有兩個(gè)以上父類實(shí)現(xiàn)了虛函數(shù),會(huì)造成子類產(chǎn)生多個(gè)虛函數(shù)表指針,可以使用dynamic_cast關(guān)鍵字作類型轉(zhuǎn)換。
工程實(shí)踐中通常使用單繼承某個(gè)類和實(shí)現(xiàn)多個(gè)接口解決多繼承的問題。
代碼實(shí)例:
#include
#include
using namespace std;
class Base
{
protected:
int mi;
public:
Base(int i)
{
mi = i;
}
int getI()
{
return mi;
}
bool equal(Base* obj)
{
return (this == obj);
}
};
class Interface1
{
public:
virtual void add(int i) = 0;
virtual void minus(int i) = 0;
};
class Interface2
{
public:
virtual void multiply(int i) = 0;
virtual void divide(int i) = 0;
};
class Derived : public Base, public Interface1, public Interface2
{
public:
Derived(int i) : Base(i)
{
}
void add(int i)
{
mi += i;
}
void minus(int i)
{
mi -= i;
}
void multiply(int i)
{
mi *= i;
}
void divide(int i)
{
if( i != 0 )
{
mi /= i;
}
}
};
int main()
{
Derived d(100);
Derived* p = &d;
Interface1* pInt1 = &d;
Interface2* pInt2 = &d;
cout << "p->getI() = " << p->getI() << endl; // 100
pInt1->add(10);
pInt2->divide(11);
pInt1->minus(5);
pInt2->multiply(8);
cout << "p->getI() = " << p->getI() << endl; // 40
cout << endl;
cout << "pInt1 == p : " << p->equal(dynamic_cast (pInt1)) << endl;
cout << "pInt2 == p : " << p->equal(dynamic_cast (pInt2)) << endl;
return 0;
}
在程序設(shè)計(jì)中最好不要出現(xiàn)多繼承,要有也是繼承多個(gè)作為接口使用抽象類(只聲明需要的功能,沒有具體的實(shí)現(xiàn))。因?yàn)槌霈F(xiàn)一般的多繼承本身就是一種不好的面向?qū)ο蟪绦蛟O(shè)計(jì)。
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務(wù)器15元起步,三天無理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢(shì),專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場(chǎng)景需求。
網(wǎng)頁題目:C++語言學(xué)習(xí)(十六)——多繼承-創(chuàng)新互聯(lián)
網(wǎng)站網(wǎng)址:http://www.xueling.net.cn/article/pgjsi.html