標籤:sizeof 函數 記憶體模型 out 文法規則 因此 eof 虛基類表指標 覆蓋
C++語言學習(十六)——多繼承一、多繼承簡介1、多繼承簡介
C++語言支援多繼承,一個子類可以有多個父類,子類擁有所有父類的成員變數,子類繼承所有父類的成員函數,子類對象可以當作任意父類對象使用。
2、多繼承文法規則
class Derived : public BaseA, public BaseB, public BaseC {};
3、多繼承衍生類別的記憶體布局
通過多重繼承得到的衍生類別對象可能具有不同的地址。
#include <iostream>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; //子類對象的地址、首位繼承類的成員地址 cout << &d << endl; cout << pa << endl; cout << &p->a <<endl; //子類對象的地址、次位繼承類的成員地址 cout << pb << endl; cout << &p->b << endl; return 0;}
上述代碼中,Derived類對象的記憶體布局如下:
Derived類對象從基類繼承而來的處成員變數將根據繼承的聲明順序進行依次排布。基於賦值相容原則,如果BaseA類型指標pa、BaseB類型指標pb都指向子類對象d,pa將得到BaseA基類成員變數ma的地址,即子類對象的地址;pb將得到BaseB類成員變數mb的地址;因此,pa與pb的地址不相同。
4、菱形多繼承導致的成員冗餘
上述類圖中,Teacher類和Student類都會繼承People的成員,Doctor會繼承Teacher類和Student類的成員,因此Doctor將會有兩份繼承自頂層父類People的成員。
#include <iostream>#include <string>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 <<endl; }private: string m_name; int m_age;};class Teacher : public People{public: Teacher(string name, int age):People(name, age) { }};class Student : public People{public: Student(string name, int age):People(name, age) { }};class Doctor : public Teacher, public Student{public: Doctor(string name, int age): Teacher(name + "_1", age), Student(name + "_2", age) { }};int main(int argc, char *argv[]){ Doctor doc("Bauer", 30); //doc.print();//error //error: request for member ‘print‘ is ambiguous //Doctor繼承了從Teacher,Student繼承來的print函數。 //doc.People::print();//error //error: ‘People‘ is an ambiguous base of ‘Doctor‘ //People被繼承了兩次 doc.Teacher::print();//name:bauer_1 age:30 doc.Student::print();//name:bauer_2 age:30 return 0;}
二、虛繼承1、虛繼承簡介
在多繼承中,儲存共同基類的多份同名成員,可以在不同的資料成員中分別存放不同的資料,但保留多份資料成員的拷貝,不僅佔有較多的儲存空間,增加了成員的冗餘,還增加了訪問的困難。C++提供了虛基類和虛繼承機制,實現了在多繼承中只保留一份共同成員。
C++對於菱形多繼承導致的成員冗餘問題的解決方案是使用虛繼承。
虛繼承中,中介層父類不再關注頂層父類的初始化,最終子類必須直接調用頂層父類的建構函式。
虛繼承的文法如下:
class 衍生類別名:virtual 繼承方式 基類名
2、虛繼承樣本
#include <iostream>#include <string>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 <<endl; }private: string m_name; int m_age;};class Teacher : virtual public People{public: Teacher(string name, int age):People(name, age) { }};class Student : virtual public People{public: Student(string name, int age):People(name, age) { }};class Doctor : public Teacher, public Student{public: //最終子類必須調用頂層父類的建構函式 Doctor(string name, int age): People(name, age), Teacher(name + "_1", age), Student(name + "_2", age) { }};int main(int argc, char *argv[]){ Doctor doc("Bauer", 30); doc.print();//name:bauer age:30 doc.People::print();//name:bauer age:30 doc.Teacher::print();//name:bauer age:30 doc.Student::print();//name:bauer age:30 return 0;}
上述代碼中,使用虛繼承解決了成員冗餘的問題。
虛繼承解決了多繼承產生的資料冗餘問題,但是中介層父類不再關心頂層父類的初始化,最終子類必須直接調用頂層父類的建構函式。
三、多繼承衍生類別的物件模型1、多繼承衍生類別對象的記憶體布局
上述類圖中,Derived類繼承自BaseA和BaseB類,funcA和funcB為虛函數,Derived物件模型如下:
#include <iostream>#include <string>using namespace std;class BaseA{public: BaseA(int a) { m_a = a; } virtual void funcA() { cout << "BaseA::funcA()" <<endl; }private: int m_a;};class BaseB{public: BaseB(int b) { m_b = b; } virtual void funcB() { cout << "BaseB::funcB()" <<endl; }private: int m_b;};class Derived : public BaseA, public BaseB{public: Derived(int a, int b, int c):BaseA(a),BaseB(b) { m_c = c; }private: int m_c;};struct Test{ void* vptrA; int a; void* vptrB; int b; int c;};int main(int argc, char *argv[]){ cout << sizeof(Derived) << endl; Derived d(1,2,3); Test* pTest = (Test*)&d; cout << pTest->a <<endl;//1 cout << pTest->b <<endl;//2 cout << pTest->c <<endl;//3 cout << pTest->vptrA <<endl;// cout << pTest->vptrB <<endl;// return 0;}
2、菱形繼承衍生類別對象的記憶體布局
菱形繼承範例程式碼如下:
#include <iostream>#include <string>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 <<endl; }private: string m_name; int m_age;};class Teacher : public People{ string m_research;public: Teacher(string name, int age, string research):People(name + "_1", age + 1) { m_research = research; }};class Student : public People{ string m_major;public: Student(string name, int age,string major):People(name + "_2", age + 2) { m_major = major; }};class Doctor : public Teacher, public Student{ string m_subject;public: Doctor(string name, int age,string research, string major, string subject): Teacher(name, age,research),Student(name, age, major) { m_subject = subject; }};struct Test{ string name1; int age1; string research; string name2; int age2; string major; string subject;};int main(int argc, char *argv[]){ Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC"); cout << "Doctor size: " << sizeof(doc) << endl; Test* pTest = (Test*)&doc; cout << pTest->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
上述代碼中,底層子類對象的記憶體局部如下:
底層子類對象中,分別繼承了中介層父類從頂層父類繼承而來的成員變數,因此記憶體模型中含有兩份底層父類的成員變數。
如果頂層父類含有虛函數,中介層父類會分別繼承頂層父類的虛函數表指標,因此,底層子類對象記憶體布局如下:
#include <iostream>#include <string>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 <<endl; }private: string m_name; int m_age;};class Teacher : public People{ string m_research;public: Teacher(string name, int age, string research):People(name + "_1", age + 1) { m_research = research; }};class Student : public People{ string m_major;public: Student(string name, int age,string major):People(name + "_2", age + 2) { m_major = major; }};class Doctor : public Teacher, public Student{ string m_subject;public: Doctor(string name, int age,string research, string major, string subject): Teacher(name, age,research),Student(name, age, major) { m_subject = subject; } virtual void print() { }};struct Test{ void* vptr1; string name1; int age1; string research; void* vptr2; string name2; int age2; string major; string subject;};int main(int argc, char *argv[]){ Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC"); cout << "Doctor size: " << sizeof(doc) << endl; Test* pTest = (Test*)&doc; cout << pTest->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、虛繼承衍生類別對象的記憶體布局
虛繼承是解決C++多重繼承問題的一種手段,虛繼承的底層實現原理與C++編譯器相關,一般通過虛基類指標和虛基類表實現,每個虛繼承的子類都有一個虛基類指標(佔用一個指標的儲存空間,4(8)位元組)和虛基類表(不佔用類對象的儲存空間)(虛基類依舊會在子類裡面存在拷貝,只是僅僅最多存在一份);當虛繼承的子類被當做父類繼承時,虛基類指標也會被繼承。
在虛繼承情況下,底層子類對象的布局不同於普通繼承,需要多出一個指向中介層父類對象的虛基類表指標vbptr。
vbptr是虛基類表指標(virtual base table pointer),vbptr指標指向一個虛基類表(virtual table),虛基類表格儲存體了虛基類相對直接繼承類的位移地址;通過位移地址可以找到虛基類成員,虛繼承不用像普通多繼承維持著公用基類(虛基類)的兩份同樣的拷貝,節省了儲存空間。
#include <iostream>#include <string>using namespace std;class People{public: People(string name, int age) { m_name = name; m_age = age; } void print() { cout << "this: " << this <<endl; }private: string m_name; int m_age;};class Teacher : virtual public People{ string m_research;public: Teacher(string name, int age, string research):People(name + "_1", age + 1) { m_research = research; } void print() { cout << "this: " << this <<endl; }};class Student : virtual public People{ string m_major;public: Student(string name, int age,string major):People(name + "_2", age + 2) { m_major = major; } void print() { cout << "this: " << this <<endl; }};class Doctor : public Teacher, public Student{ string m_subject;public: Doctor(string name, int age,string research, string major, string subject): People(name, age),Teacher(name, age,research),Student(name, age, major) { m_subject = subject; }};struct Test{ void* vbptr_left; string research; void* vbptr_right; string major; string subject; string name; int age;};int main(int argc, char *argv[]){ Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC"); cout << "Doctor size: " << sizeof(doc) << endl; Test* pTest = (Test*)&doc; cout << pTest->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
上述代碼沒有虛函數,在G++編譯器列印結果如上,底層子類對象的記憶體布局如下:
#include <iostream>#include <string>using namespace std;class People{public: People(string name, int age) { m_name = name; m_age = age; } virtual void print() { cout << "this: " << this <<endl; }private: string m_name; int m_age;};class Teacher : virtual public People{ string m_research;public: Teacher(string name, int age, string research):People(name + "_1", age + 1) { m_research = research; } void print() { cout << "this: " << this <<endl; } virtual void func1() {}};class Student : virtual public People{ string m_major;public: Student(string name, int age,string major):People(name + "_2", age + 2) { m_major = major; } void print() { cout << "this: " << this <<endl; } virtual void func2() {}};class Doctor : public Teacher, public Student{ string m_subject;public: Doctor(string name, int age,string research, string major, string subject): People(name, age),Teacher(name, age,research),Student(name, age, major) { m_subject = subject; } void print() { cout << "this: " << this <<endl; } virtual void func3() {}};struct Test{ void* vbptr_left; char* research; void* vbptr_right; char* major; char* subject; void* vptr_base; char* name; long age;};int main(int argc, char *argv[]){ Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC"); cout << "Doctor size: " << sizeof(doc) << endl; Test* pTest = (Test*)&doc; cout << pTest->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++編譯器實現原理不同。
對於GCC編譯器,People對象大小為char + int + 虛函數表指標,Teacher對象大小為char+虛基類表指標+A類型的大小,Student對象大小為char+虛基類表指標+A類型的大小,Doctor對象大小為char + int +虛函數表指標+char+虛基類表指標+char+虛基類表指標+char*。中介層父類共用頂層父類的虛函數表指標,沒有自己的虛函數表指標,虛基類指標不共用,因此都有自己獨立的虛基類表指標。
VC++、GCC和Clang編譯器的實現中,不管是否是虛繼承還是有虛函數,其虛基類指標都不共用,都是單獨的。對於虛函數表指標,VC++編譯器根據是否為虛繼承來判斷是否在繼承關係中共用虛表指標。如果子類是虛繼承擁有虛函數父類,且子類有新加的虛函數時,子類中則會新加一個虛函數表指標;GCC編譯器和Clang編譯器的虛函數表指標在整個繼承關係中共用的。
G++編譯器對於類的記憶體分布和虛函數表資訊命令如下:
g++ -fdump-class-hierarchy main.cppcat main.cpp.002t.class
VC++編譯器對於類的記憶體分布和虛函數表資訊命令如下:
cl main.cpp /d1reportSingleClassLayoutX
Clang編譯器對於類的記憶體分布和虛函數表資訊命令如下:
clang -Xclang -fdump-record-layouts
4、多繼承衍生類別的虛函數表
所有的虛函數都儲存在虛函數表中,多重繼承可能產生多個虛函數表。多繼承中,當子類對父類的虛函數重寫時,子類的函數覆蓋父類的函數在對應虛函數表中的虛函數位置;當子類有新的虛函數時,新的虛函數被加到第一個基類的虛函數表的末尾。當dynamic_cast對子類對象進行轉換時,子類和第一個基類的地址相同,不需要移動指標,但當dynamic_cast轉換子類到其他父類時,需要做相應的指標調整。
四、多繼承的指標類型轉換1、多繼承中指標類型轉換的陷阱
C++語言中,通常對指標進行類型轉換,不會改變指標的值,只會改變指標的類型(即改變編譯器對該指標指向記憶體的解釋方式),但在C++多重繼承中並不成立。
#include <iostream>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對象的指標轉換為基類BaseA和BaseB後,指標值並不相同。dpd指標、bpa指標與bpb指標相差8個位元組的地址空間,即BaseA類虛函數表指標與data成員佔用的空間。
將一個衍生類別的指標轉換成某一個基類指標,C++編譯器會將指標的值位移到該基類在對象記憶體中的起始位置。
cout << (dpd == bpb) << endl;//1
上述代碼列印出1,C++編譯器屏蔽了指標的差異,當C++編譯器遇到一個指向衍生類別的指標和指向其某個基類的指標進行==運算時,會自動將指標做隱式型別提升以屏蔽多重繼承帶來的指標差異。
2、多繼承中衍生類別、基類指標類型轉換
衍生類別對象指標轉換為不同基類對象指標時,C++編譯器會按照衍生類別聲明的繼承順序,轉換為第一基類時指標不變,以後依次向後位移前一基類所佔位元組數。
多繼承下,指標類型轉換需要考慮this指標調整的問題。
五、多繼承應用樣本
多繼承中,如果中介層父類有兩個以上父類實現了虛函數,會造成子類產生多個虛函數表指標,可以使用dynamic_cast關鍵字作類型轉換。
工程實踐中通常使用單繼承某個類和實現多個介面解決多繼承的問題。
代碼執行個體:
#include <iostream>#include <string>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<Base*>(pInt1)) << endl; cout << "pInt2 == p : " << p->equal(dynamic_cast<Base*>(pInt2)) << endl; return 0;}
在程式設計中最好不要出現多繼承,要有也是繼承多個作為介面使用抽象類別(只聲明需要的功能,沒有具體的實現)。因為出現一般的多繼承本身就是一種不好的物件導向程式設計。
C++語言學習(十六)——多繼承