1. 程式人生 > >C++語言學習(十六)——多繼承

C++語言學習(十六)——多繼承

虛函數表 -o nag http layout 調用 img error names

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.cpp
cat 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++語言學習(十六)——多繼承