[C++] 继承和多态

        Be water my friend.         

 一.关于继承(inheritance)

基础知识:

继承的定义格式:

 继承方式的比较:

继承中的作用域:

基类和派生类对象赋值转换 :

        派生类的默认成员函数 

关于继承的补充

如何防止继承的发生(final关键字)

友元关系不能继承

继承与静态成员

改变个别成员的可访问性(使用 using声明)

二.多态(polymorphism)

概念:

那么在继承中要构成多态还有两个条件:

虚函数(virtual function)

关于虚函数的重写(override)

虚函数重写的两个例外:

override 和 final 

虚函数和默认实参 

回避虚函数的机制

          纯虚函数(pure virtual) 和 抽象(abstract)类

接口继承和实现继承 

+++++++++++++++++++++进阶知识+++++++++++++++++++++

多态的原理(虚函数表 vfptr 的解析)

多态

使用指针访问虚表

小结:

        关于虚函数表在其他情况的下总结:

一般继承(无虚函数覆盖)

一般继承(有虚函数覆盖)

多重继承(无虚函数覆盖)

        多重继承(有虚函数覆盖)

        关于虚函数表的创建和销毁时间

典型的构造函数执行以下操作:

典型的析构函数几乎以相反的顺序工作:

        关于虚函数安全性

关于菱形继承和虚继承(virtual inheritance)

菱形继承

虚继承

虚基类表解析

简单虚继承

菱形虚拟继承

参考资料


 一.关于继承(inheritance)

基础知识:

继承的定义格式:

 继承方式的比较:

总结:  

1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),

public  > protected > private。
4.
使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

//测试代码
class Person
{
public :
    void Print ()
    {
        cout<<_name <<endl;
    }
protected :
    string _name ; // 姓名
private :
    int _age ;   // 年龄
};

//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :
    int _stunum ; // 学号
};

继承中的作用域:

  • 在继承体系中基类和派生类都有独立的作用域。子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  • 注意在实际中在继承体系里面最好不要定义同名的成员。
//关于同名隐藏的代码测试  取自<<c++ primer>>[15.6]

struct Base{
    Base(): mem(0){}
    int memfcn();
protected:
    int mem;

};

struct Derived :Base{
    Derived(int i):men(i){  }         //用i初始化Derived::mem
                                      //Base::mem 进行默认初始化
    
    int memfcn(int);                 //隐藏基类的memfcn
    int get_mem(){  return mem;}
protectd:
    int mem;                         //隐藏基类中的

};


//demo

Derived d(42);
cout<< d.get_mem()<<endl // 输出42    

Base b;
b.memfcn();                    //调用 Base::memfcn
d.memfcn(10);                  //调用 Derived::memfcn
d.memfcn();                    //错误: 参数列表为空的memfcn被隐藏了
d.Base::memfcn();              //正确: 调用memfcn



//note

/*
    如前所述,声明在内层作用域的函数并不会重载声明在外层作用域的函数(参见6.4.1节,第210页)。
因此,定义派生类中的函数也不会重载其基类中的成员。和其他作用域一样,如果派生类(即内层作用
域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派
生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉

    Derived中的memfcn声明隐藏了Base 中的memfcn声明。在上面的代码中前两条调用语句容易理解,
第一个通过Base对象b进行的调用执行基类的版本;类似的,第二个通过d进行的调用执行Derived的版本;
第三条调用语句有点特殊,d.memfcn ()是非法的。
    
    为了解析这条调用语句,编译器首先在Derived中查找名字memfcn;因为 Derived确实定义了一个名为
memfcn的成员,所以查找过程终止。一旦名字找到,编译器就不再继续查找了。Derived中的memfcn版本
需要一个int实参,而当前的调用语句无法提供任何实参,所以该调用语句是错误的。

*/

基类和派生类对象赋值转换 :

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。

class Person
{
protected :
    string _name; // 姓名
    string _sex;  // 性别
    int _age;    // 年龄
};
class Student : public Person
{
public :
    int _No ; // 学号
};
void Test ()
{
    Student sobj ;
    // 1.子类对象可以赋值给父类对象/指针/引用
    Person pobj = sobj ;
    Person* pp = &sobj;
    Person& rp = sobj;
    
    //2.基类对象不能赋值给派生类对象
    sobj = pobj;
    
    // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
    pp = &sobj
    Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
    ps1->_No = 10;
    
    pp = &pobj;
    Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
                               
    ps2->_No = 10;        //编译通过,但是越界访问 可能出现运行时错误  
                        //故子类向父类转化推荐使用dynamic
}

派生类的默认成员函数 

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

class Person
{
public :
    Person(const char* name = "peter")
        : _name(name )
    {
        cout<<"Person()" <<endl;
    }
    
    Person(const Person& p)
        : _name(p._name)
    {
 cout<<"Person(const Person& p)" <<endl;
    }
    
    Person& operator=(const Person& p )
    {
        cout<<"Person operator=(const Person& p)"<< endl;
        if (this != &p)
            _name = p ._name;
        
        return *this ;
    }
    
    ~Person()
    {
        cout<<"~Person()" <<endl;
    }
protected :
    string _name ; // 姓名
};

class Student : public Person
{
public :
    Student(const char* name, int num)
        : Person(name )
        , _num(num )
    {
        cout<<"Student()" <<endl;
    }
    
    Student(const Student& s)
        : Person(s)
        , _num(s ._num)
    {
        cout<<"Student(const Student& s)" <<endl ;
    }
    
    Student& operator = (const Student& s )
    {
        cout<<"Student& operator= (const Student& s)"<< endl;
        if (this != &s)
        {
            Person::operator =(s);
            _num = s ._num;
        }
        return *this ;
    } 
    
    ~Student()
    {
        cout<<"~Student()" <<endl;
    }
protected :
    int _num ; //学号
};
void Test ()
{
    Student s1 ("jack", 18);
Student s2 (s1);
    Student s3 ("rose", 17);
    s1 = s3 ;
}

关于继承的补充

如何防止继承的发生(final关键字)

        有时我们会定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类。为了实现这一目的,C++11新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字final:

友元关系不能继承

ps:一句话幽默

                C++是一个很好的编译语言,因为你的parent(父母)不能访问你的private(隐私),但是你的friend(朋友)可以。        

继承与静态成员

        基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例  。 

class Person
{
public :
    Person () {++ _count ;}
protected :
    string _name ; // 姓名
public :
    static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :
    int _stuNum ; // 学号
};


class Graduate : public Student
{
protected :
    string _seminarCourse ; // 研究科目
};


void TestPerson()
{
    Student s1 ;
    Student s2 ;
    Student s3 ;
    Graduate s4 ;
    cout <<" 人数 :"<< Person ::_count << endl;
    Student ::_count = 0;
    cout <<" 人数 :"<< Person ::_count << endl;
}

改变个别成员的可访问性(使用 using声明)

二.多态(polymorphism)

概念:

        多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。


那么在继承中要构成多态还有两个条件:

  1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
  2. 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型(dynamic type)才有可能与静态类型(static type)不同。

虚函数(virtual function)

        在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数( virtual function)

        

关于虚函数的重写(override)

        虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
 
 /*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
 /*void BuyTicket() { cout << "买票-半价" << endl; }*/
};

void Func(Person& p)
{ p.BuyTicket(); }

int main()
{
 Person ps;
 Student st;
 
 Func(ps);
 Func(st);

    return 0;
}

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型不同)

        派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)

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

class Person {
public:
    virtual A* f() {return new A;}
};

class Student : public Person {
public:
    virtual B* f() {return new B;}
};

 2.析构函数的重写(基类与派生类析构函数的名字不同)

        如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

        事实上,基类通常都应该定义一个虚析构函数,如果一个基类定义了析构函数,但没有将其声明为虚函数,那么在执行派生类对象的析构函数时,只会调用派生类本身的析构函数,而不会调用基类的析构函数。这可能会导致基类对象成员没有被正确地销毁,从而导致内存泄漏等问题。

class Base {
public:
    Base() {
        std::cout << "Base Constructor" << std::endl;
    }

    virtual ~Base() {
        std::cout << "Base Destructor" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived Constructor" << std::endl;
    }

    ~Derived() {
        std::cout << "Derived Destructor" << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    delete basePtr;
    return 0;
}

/*
在这个例子中,我们有一个基类Base和一个派生类Derived。我们在main函数中创建一个指向
Derived类对象的Base类指针。当我们调用delete basePtr;时,如果Base类的析构函数是虚
函数,则会先调用Derived类的析构函数,然后再调用Base类的析构函数。这样,对象会被完
全销毁,资源得到正确释放。

如果Base类的析构函数不是虚函数,那么只会调用Base类的析构函数,
而不会调用Derived类的析构函数。这将导致派生类对象的部分资源未被正确释放,造成资源泄露。

因此,在多态情况下,为了确保对象能够被完全销毁并释放资源,析构函数应该被声明为虚函数。
*/

override 和 final 

        从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  • final:修饰虚函数,表示该虚函数不能再被重
  • override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

虚函数和默认实参 

        和其他函数一样,虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。        
        换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符。

     

          ps:如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

回避虚函数的机制

纯虚函数(pure virtual) 和 抽象(abstract)类

        在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

        包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

        派生类继承后也不能实例化出对象,只有重写纯虚数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
 virtual void Drive() = 0;
};

class Benz :public Car
{
public:
    virtual void Drive()
 {
    cout << "Benz-舒适" << endl;
 }
};

class BMW :public Car
{
public:
    virtual void Drive()
 {
    cout << "BMW-操控" << endl;
 }
};

void Test()
{
 Car* pBenz = new Benz;
    pBenz->Drive();

    Car* pBMW = new BMW;
    pBMW->Drive();
}

接口继承和实现继承 

        普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


+++++++++++++++++++++进阶知识+++++++++++++++++++++

多态的原理(虚函数表 vfptr 的解析)

多态

        C++中虚函数的作用主要是为了实现多态机制。多态,简单来说,是指在继承层次中,父类的指针可以具有多种形态——当它指向某个子类对象时,通过它能够调用到子类的函数,而非父类的函数。

class Base {     virtual void print(void);    }
class Drive1 :public Base{    virtual void print(void);    }
class Drive2 :public Base{    virtual void print(void);    }

Base * ptr1 = new Base; 
Base * ptr2 = new Drive1;  
Base * ptr3 = new Drive2;

ptr1->print(); //调用Base::print()
prt2->print();//调用Drive1::print()
prt3->print();//调用Drive2::print()

        这是一种运行期多态,即父类指针唯有在程序运行时才能知道所指的真正类型是什么。这种运行期决议,是通过虚函数表来实现的。

使用指针访问虚表

class Base{
public:

    virtual void f(){ cout << "Base::f" << endl; }

    virtual void g(){ cout << "Base::g" << endl; }

    virtual void h(){ cout << "Base::h" << endl; }
};
typedef void(*Fun)(void);
int main(){
    
    Base b;
    Fun pFun = NULL;
    cout << "虚函数表地址:" << (int*)(&b) << endl;   //即对象的起始地址
    cout << "虚函数表 第一个函数地址:" << (int*)*(int*)(&b) << endl;
    // Invoke the first virtual function 
    pFun = (Fun) * ((int*)*(int*)(&b));
    pFun();

    //通过指针偏移依次访问虚函数表中的函数

    ((Fun)* ((int*)*(int*)(&b) + 1))();  // Base::g()
    ((Fun)* ((int*)*(int*)(&b) + 2))();  // Base::h()

    cout << "===================" << endl;
    Base b2;
    
    cout << "虚函数表地址:" << (int*)(&b2) << endl;   //即对象的起始地址
    cout << "虚函数表 第一个函数地址:" << (int*)*(int*)(&b2) << endl;
    pFun = (Fun) * ((int*)*(int*)(&b));
    pFun();
    //通过指针偏移依次访问虚函数表中的函数
    ((Fun) * ((int*)*(int*)(&b) + 1))();  // Base::g()
    ((Fun) * ((int*)*(int*)(&b) + 2))();  // Base::h()
    return 0;
}

运行结果:

小结:

        当一个类本身定义了虚函数,或其父类有虚函数时,为了支持多态机制,编译器将为该类添加一个虚函数指针(vfptr)。虚函数指针一般都放在对象内存布局的第一个位置上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。

关于虚函数表在其他情况的下总结:

一般继承(无虚函数覆盖)

  • 虚函数按照其声明顺序放于表中。
  • 父类的虚函数在子类的虚函数前面。

一般继承(有虚函数覆盖)

  • 覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
  • 没有被覆盖的函数依旧。

多重继承(无虚函数覆盖)

  • 每个父类都有自己的虚表。

  • 子类的虚函数被放在声明的第一个基类的虚函数表中。(所谓的第一个父类是按照声明顺序来判断的)
  • 内存布局中,父类按照其声明顺序排列。

多重继承(有虚函数覆盖)

  • overwrite时,所有基类的虚函数都被子类的虚函数覆盖。

//测试验证
#include <iostream>
#include <string>

using namespace std;


class Base1{
public:
    int ibase1;
    Base1() :ibase1(10){}

    virtual void f(){ cout << "Base1::f()" << endl; }
    virtual void g(){ cout << "Base1::g()" << endl; }
    virtual void h(){ cout << "Base1::h()" << endl; }
};


class Base2{
public:
    int ibase2;
    Base2() :ibase2(20){}

    virtual void f(){ cout << "Base2::f()" << endl; }
    virtual void g(){ cout << "Base2::g()" << endl; }
    virtual void h(){ cout << "Base2::h()" << endl; }
};


class Base3{
public:
    int ibase3;
    Base3() :ibase3(30){}

    virtual void f(){ cout << "Base3::f()" << endl; }
    virtual void g(){ cout << "Base3::g()" << endl; }
    virtual void h(){ cout << "Base3::h()" << endl; }
};


class Derive : public Base1, public Base2, public Base3{
public:
    int iderive;
    Derive() :iderive(100){}

    virtual void f(){ cout << "Derive::f()" << endl; }
    virtual void g1(){ cout << "Derive::g1()" << endl; }
};


int main(){

    typedef void(*Fun)(void);
    Fun pFun = NULL;
    Derive d;
    int** pVtab = (int**)&d;

    cout << "[0] Base1::_vfptr->" << endl;
    pFun = (Fun)pVtab[0][0];
    cout << "     [0] "; pFun();
    pFun = (Fun)pVtab[0][1];
    cout << "     [1] "; pFun();
    pFun = (Fun)pVtab[0][2];
    cout << "     [2] "; pFun();
    pFun = (Fun)pVtab[0][3];
    cout << "     [3] "; pFun();
    pFun = (Fun)pVtab[0][4];
    cout << "     [4] "; cout << pFun << endl;
f
    cout << "[1] Base1.ibase1 = " << (int)pVtab[1] << endl;
    int s = sizeof(Base1) / 4;
    cout << "[" << s << "] Base2::_vfptr->" << endl;
    pFun = (Fun)pVtab[s][0];
    cout << "     [0] "; pFun();
    pFun = (Fun)pVtab[s][1];
    cout << "     [1] "; pFun();
    pFun = (Fun)pVtab[s][2];
    cout << "     [2] "; pFun();
    pFun = (Fun)pVtab[s][3];
    cout << "     [3] ";
    cout << pFun << endl;

    cout << "[" << s + 1 << "] Base2.ibase2 = " << (int)pVtab[s + 1] << endl;
    s = s + sizeof(Base2) / 4;
    cout << "[" << s << "] Base3::_vfptr->" << endl;f
    pFun = (Fun)pVtab[s][0];
    cout << "     [0] "; pFun();
    pFun = (Fun)pVtab[s][1];
    cout << "     [1] "; pFun();
    pFun = (Fun)pVtab[s][2];
    cout << "     [2] "; pFun();
    pFun = (Fun)pVtab[s][3];
    cout << "     [3] ";
    cout << pFun << endl;

    s++;
    cout << "[" << s << "] Base3.ibase3 = " << (int)pVtab[s] << endl;
    s++;
    cout << "[" << s << "] Derive.iderive = " << (int)pVtab[s] << endl;


    return 0;
}

 运行结果:

关于虚函数表的创建和销毁时间


典型的构造函数执行以下操作:

  • 调用基类的构造函数。
  • 调用复杂类成员的构造函数。
  • 初始化 vfptr(如果类有虚函数)
  • 执行程序员编写的构造函数体。

典型的析构函数几乎以相反的顺序工作:

  • 初始化 vfptr 如果类有虚函数
  • 执行程序员编写的析构函数主体。
  • 调用复杂类成员的析构函数
  • 基类的调用析构函数

        此规则很简单:构造函数调用其他构造函数(基类和成员变量),析构函数调用其他析构函数。

关于虚函数安全性

一、通过父类型的指针访问子类自己的虚函数

        我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

Base1 *b1 = new Derive();
b1->f1();  //编译出错

        任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。((关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

二、访问non-public的虚函数

        另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。如:

class Base {
    private:
            virtual void f() { cout << "Base::f" << endl; }

};

class Derive : public Base{

};

typedef void(*Fun)(void);

void main() {
    Derive d;
    Fun  pFun = (Fun)*((int*)*(int*)(&d)+0);
    pFun();
}

运行结果

关于菱形继承和虚继承(virtual inheritance)

菱形继承

        菱形继承也称为钻石型继承或重复继承,它指的是基类被某个派生类简单重复继承了多次。这样,派生类对象中拥有多份基类实例(这会带来一些问题)

        D类对象内存布局中,图中青色表示b1类子对象实例,黄色表示b2类子对象实例,灰色表示D类子对象实例。从图中可以看到,由于D类间接继承了B类两次,导致D类对象中含有两个B类的数据成员ib,一个属于来源B1类,一个来源B2类。这样不仅增大了空间,更重要的是引起了程序歧义: 

class B{
 
public:
    int ib;
 
public:
    B(int i=1) :ib(i){}
    virtual void f() { cout << "B::f()" << endl; }
    virtual void Bf() { cout << "B::Bf()" << endl; }
};
 
class B1 : public B{
public:
    int ib1;
 
public:
    B1(int i = 100 ) :ib1(i) {}
    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() { cout << "B1::f1()" << endl; }
    virtual void Bf1() { cout << "B1::Bf1()" << endl; }

};
 
class B2 : public B{
public:
    int ib2;
 
public:
    B2(int i = 1000) :ib2(i) {}
    virtual void f() { cout << "B2::f()" << endl; }
    virtual void f2() { cout << "B2::f2()" << endl; }
    virtual void Bf2() { cout << "B2::Bf2()" << endl; }
};
  
class D : public B1, public B2{
public:
    int id;

public:
    D(int i= 10000) :id(i){}
    virtual void f() { cout << "D::f()" << endl; }
    virtual void f1() { cout << "D::f1()" << endl; }
    virtual void f2() { cout << "D::f2()" << endl; }
    virtual void Df() { cout << "D::Df()" << endl; }
 
};
D d;
 
d.ib =1 ;               //二义性错误,调用的是B1的ib还是B2的ib?
 
d.B1::ib = 1;           //正确
 
d.B2::ib = 1;           //正确

/*
尽管我们可以通过明确指明调用路径以消除二义性,
但二义性的潜在性还没有消除,我们可以通过虚继承来使D类只拥有一个ib实体。
*/

虚继承

        虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:

  • 虚继承的子类,如果本身定义了新的虚函数,则编译器为其生成一个虚函数指针(vfptr)以及一张虚函数表。该vptr位于对象内存最前面。
    • vs非虚继承:直接扩展父类虚函数表。
  • 虚继承的子类也单独保留了父类的vprt与虚函数表。这部分内容接与子类内容以一个四字节的0来分界。
  • 虚继承的子类对象中,含有四字节的虚表指针偏移值。

为了分析最后的菱形继承,我们还是先从单虚继承继承开始。

虚基类表解析

        在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
        一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是
偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。我们通过一张图来更好地理解。

         虚基类表的第二、第三...个条目依次为该类的最左虚继承父类、次左虚继承父类...的内存地址相对于虚基类表指针的偏移值,这点我们在下面会验证。

简单虚继承

 

#include <iostream>
#include <string>

using namespace std;


class B{

public:
    int ib;

public:
    B(int i = 1) :ib(i){}
    virtual void f(){ cout << "B::f()" << endl; }
    virtual void Bf(){ cout << "B::Bf()" << endl; }
};

class B1 :virtual public B{
public:
    int ib1;

public:
    B1(int i = 100) :ib1(i){}
    virtual void f(){ cout << "B1::f()" << endl; }
    virtual void f1(){ cout << "B1::f1()" << endl; }
    virtual void Bf1(){ cout << "B1::Bf1()" << endl; }

};


typedef void(*Fun)(void);

int main(){
    B1 a;
    cout << "B1对象内存大小为:" << sizeof(a) << endl;

    //取得B1的虚函数表
    cout << "[0]B1::vptr";
    cout << "\t地址:" << (int*)(&a) << endl;

    //输出虚表B1::vptr中的函数
    for (int i = 0; i < 2; ++i){
        cout << "  [" << i << "]";
        Fun fun1 = (Fun) * ((int*)*(int*)(&a) + i);
        fun1();
        cout << "\t地址:\t" << *((int*)*(int*)(&a) + i) << endl;
    }

    //[1]
    cout << "[1]vbptr ";
    cout << "\t地址:" << (int*)(&a) + 1 << endl;  //虚表指针的地址
    //输出虚基类指针条目所指的内容
    for (int i = 0; i < 2; i++){
        cout << "  [" << i << "]";

        cout << *(int*)((int*)*((int*)(&a) + 1) + i);

        cout << endl;
    }


    //[2]
    cout << "[2]B1::ib1=" << *(int*)((int*)(&a) + 2);
    cout << "\t地址:" << (int*)(&a) + 2;
    cout << endl;

    //[3]
    cout << "[3]值=" << *(int*)((int*)(&a) + 3);
    cout << "\t\t地址:" << (int*)(&a) + 3;
    cout << endl;

    //[4]
    cout << "[4]B::vptr";
    cout << "\t地址:" << (int*)(&a) + 3 << endl;

    //输出B::vptr中的虚函数
    for (int i = 0; i < 2; ++i){
        cout << "  [" << i << "]";
        Fun fun1 = (Fun) * ((int*)*((int*)(&a) + 4) + i);
        fun1();
        cout << "\t地址:\t" << *((int*)*((int*)(&a) + 4) + i) << endl;
    }

    //[5]
    cout << "[5]B::ib=" << *(int*)((int*)(&a) + 5);
    cout << "\t地址: " << (int*)(&a) + 5;
    cout << endl;
    return 0;

}

运行结果:

        这个结果与我们的C++对象模型图完全符合。这时我们可以来分析一下虚表指针的第二个条目值12的具体来源了,回忆上文讲到的:

第二、第三...个条目依次为该类的最左虚继承父类、次左虚继承父类...的内存地址相对于虚基类表指针的偏移值。

        在我们的例子中,也就是B类实例内存地址相对于vbptr的偏移值,也即是:[4]-[1]的偏移值,结果即为12,从地址上也可以计算出十进制数正是12。现在,我们对虚基类表的构成应该有了一个更好的理解。

菱形虚拟继承

        菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:

  • 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)
  • D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
  • 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
  • 超类B的内容放到了D类对象内存布局的最后。

菱形虚拟继承下的C++对象模型为:


 

运行结果

参考资料

《C++ Primer(中文版)》(第5版)

C++ 虚函数表解析_c++虚函数表解析_haoel的博客-CSDN博客

C++ 对象的内存布局

图说C++对象模型:对象内存布局

Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI — OpenRCE

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值