多重继承
概念: 一个派生类如果只继承一个基类,称作单继承,那么如果继承了多个基类 ,就称作多继承。 比如:
class C:public A,public B{};
多重继承的优点:
多重继承可以做更多的代码复用! 派生类通过多重继承,可以得到多个基类的数据和方法,更大程度的实现了代码复用 。
多重继承有优点,那就也会存在缺陷:
首先我们通过菱形继承了解一下多重继承的缺陷:
菱形继承
菱形继承是多继承的一种: 如下图所示: 图中的继承关系会产生什么样的问题呢? 我们通过如下的代码示例理解一下:
class A
{
public:
A(int data) :ma(data) { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
protected:
int ma;
};
class B :public A
{
public:
B(int data) :A(data), mb(data) { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
protected:
int mb;
};
class C :public A
{
public:
C(int data) :A(data), mc(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
protected:
int mc;
};
class D :public B, public C
{
public:
D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
~D() { cout << "~D()" << endl; }
protected:
int md;
};
int main()
{
D d(10);
return 0;
}
运行结果如下:
对于基类A而言,构造了两次,析构了两次! 并且,通过分析各个派生类的内存布局我们可以看到: 于是我们得出结论: 如果多继承的数量增加,那么派生类中重复的数据也会增加!
半圆形继承
除了菱形继承外,还有其他多重继承的情况,也会出现响应的问题:
解决多重继承
通过分析我们知道了,多重继承的主要问题是,通过多重继承,有可能得到重复的基类数据 ,并且可能重复的构造和析构同一个基类对象 。 那么如何能够避免重复现象的产生呢? 答:虚基类。
虚基类
对于如下的这个示例而言,B虚继承了A,所以把A称作虚基类 。
class A
{
private:
int ma;
};
class B : virtual public A
{
private:
int mb;
};
注意区分:抽象类是指有纯虚函数的类,而虚基类是指被虚继承的类。
未使用虚继承方式之前,对于类B实例化出的一个对象而言,其内存结构大致如下: 在使用了虚继承方式之后,其内存结构发生了如下的变化,变成12个字节: 大致了解了上述这样一个过程,我们来探究一下这个将虚函数和虚继承结合会出现什么问题?
class A
{
public:
virtual void fun(){cout<<"call A::fun"<<endl;}
private:
int ma;
};
class B:virtual public A
{
public:
void fun(){cout<<"call B::fun"<<endl;}
private:
int mb;
};
int main()
{
A *p = new B();
p->fun();
delete p;
return 0;
}
为什么delete会出现这种问题呢?
完成B对象的构建,那么依照我们之前了解到的内存分布规则后,我们指针p开辟的内存应该是指向vbptr 还是vfptr 呢? 基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址。 即便是虚继承方式,在对象B的内存结构中,它的成员被挪动到最下方并在最上方添加一个虚基类指针后,p指向的还是基类的首地址。 所以在进行delete时,并没有完全释放干净所有内存。 (注意这里是堆区空间,如果是栈空间,出作用域后系统会自动回收也不会报错)
解决之前的问题
了解了虚基类是什么,那么虚基类又是如何解决多重继承的问题的呢? 改成虚继承,A就变成了虚基类
class A
{
public:
A(int data) :ma(data) { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
protected:
int ma;
};
class B :virtual public A
{
public:
B(int data) :A(data), mb(data) { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
protected:
int mb;
};
class C :virtual public A
{
public:
C(int data) :A(data), mc(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
protected:
int mc;
};
class D :public B, public C
{
public:
D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
~D() { cout << "~D()" << endl; }
protected:
int md;
};
我们做出修改: 将B和C的继承方式都改为虚继承;接下来继续运行代码: 此时会报错: 提示说:“A::A” : 没有合适的默认构造函数可用; 为什么会这样呢?
是因为:
- 刚开始B和C单继承A的时候,实例化对象时,会首先调用基类A的构造函数,,到了D,由于多继承了B和C,所以在实例化D的对象时,会首先调用B和C的构造函数,然后调用自己(D)的。
- 但是这样会出现A重复构造的问题,所以,采用虚继承,把有关重复的基类A改为虚基类,这样的话,对于A构造的任务就落到了最终派生类D的头上,但是我们的代码中,对于D的构造函数:
D(int data) : B(data), C(data), md(data) { cout << “D()” << endl;} 并没有对A进行构造。 - 所以会报错。 那么我们就给D的构造函数,调用A的构造函数: D(int data) :A(data), B(data), C(data), md(data) { cout << “D()” << endl; }
想想我们之前的内存结构改变的方式: 虚继承后,挪动B和C的成员变量至内存结构的最下方且添加了vbptr 后,发现A::ma挪动不需要两份,因此只挪过去一份,由此其内存结构如下:
说两句题外话 查看对象的内存结构的命令:
cl 源文件名称 /d1reportSingleClassLayout类名称
《剑指offer》经典题目
class A{};
sizeof(A) = 1;
class B:public A{};
sizeof(B) = 1;
class A
{
virtual void fun(){};
};
class B:public A{};
sizeof(B) = 4;
class A
{
virtual void fun(){};
};
class B:virtual public A{};
sizeof(B) = 8;
总之,虚继承可以解决多重继承的问题,虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅 最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。 间接性表明了在访问虚基类的成员时同样也必须通过某种间接机制来完成,共享性表象在虚基类会在虚继承体系中被共享,而不会出现多份拷贝。
|