C++单继承,多重继承,虚拟继承与内存布局

Multiple Inheritance - C++

  1. 多继承比单继承更复杂,引入了歧义的问题,以及虚继承的必要性;
  2. 虚继承在大小,速度,初始化,复制的复杂性上有不小的代价,当虚基类中没有数据时还是比较合适的;
  3. 多继承有时也是有用的。典型的场景是:public继承自一些接口类,private继承自哪些实现相关的类。

歧义

class A{
public:
    void func();
};
class B{
private:
    bool func() const;
};
class C: public A, public B{ ... };

C c;
c.func();           // 歧义!

解决冲突的时候就必须

c.A::func();

多继承菱形

在这里插入图片描述

class File{};
class InputFile: public File{};
class OutputFile: public File{};
class IOFile: public InputFile, public OutputFile{};

这样的层级在C++标准库中也存在,例如basic_ios, basic_istream, basic_ostream, basic_iostream。

IOFile的两个父类都继承自File,那么File的属性(比如filename)应该在IOFile中保存一份还是两份呢? 这是取决于应用场景的,就File::filename来讲显然我们希望它只保存一份,但在其他情形下可能需要保存两份数据。 C++还是一贯的采取了自己的风格:都支持!默认是保存两份数据的方式。如果你希望只存储一份,可以用virtual继承:

class File{};
class InputFile: virtual public File{};
class OutputFile: virtual public File{};
class IOFile: public InputFile, public OutputFile{};

代价:

  1. 虚继承类的对象会更大一些;
  2. 虚继承类的成员访问会更慢一些;
  3. 虚继承类的初始化更反直觉一些。继承层级的最底层(most derived class)负责虚基类的初始化,而且负责整个继承链上所有虚基类的初始化。

对于这些复杂性,建议

  1. 如果能不使用多继承,就不用他;
  2. 如果一定要多继承,尽量不在里面放数据,也就避免了虚基类初始化的问题。

接口类

这样一个不包含数据的虚基类和Java或者C#提供的interface有很多共同之处,这样的类在C++中称为接口类,一个Person的接口类是这样的:

class IPerson {
public:
    virtual ~IPerson();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};

内存布局

普通多重继承内存布局

class Base
{
public:
 
    Base(int i) :baseI(i){};
    virtual ~Base(){}
 
    int getI(){ return baseI; }
 
    static void countI(){};
 
    virtual void print(void){ cout << "Base::print()"; }
 
private:
 
    int baseI;
 
    static int baseS;
};
class Base_2
{
public:
    Base_2(int i) :base2I(i){};

    virtual ~Base_2(){}

    int getI(){ return base2I; }

    static void countI(){};

    virtual void print(void){ cout << "Base_2::print()"; }
 
private:
 
    int base2I;
 
    static int base2S;
};
 
class Drive_multyBase :public Base, public Base_2
{
public:

    Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){};
 
    virtual void print(void){ cout << "Drive_multyBase::print" ; }
 
    virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; }
 
private:
    int Drive_multyBaseI;
};

在这里插入图片描述
可见内存布局为,Base,Base_2, 然后再是自己的变量。
Base和Base_2中都有自己的虚函数指针,都有的print方法被重写。两个虚函数表都被重写,继承类的虚函数指针跟在Base类的后面,析构函数也是虚函数。
注意x64架构下指针为8个字节,而x86架构下指针为4个字节。

验证

using Fun = void(*)();
int main()
{
	Drive_multyBase d(3000);
	cout << sizeof(Base) << "\n";
	cout << sizeof(d) << "\n";
	//[0]
	cout << "[0]Base::vptr";
	cout << "\t地址:" << (int *)(&d) << endl;

	//vprt[0]析构函数无法通过地址调用,故手动输出
	cout << "  [0]" << "Derive::~Derive" << endl;

	//vprt[1]
	cout << "  [1]";
	Fun fun1 = (Fun)*((int *)*((int *)(&d)) + 1);
	fun1();
	cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 1) << endl;


	//vprt[2]
	cout << "  [2]";
	Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);
	fun2();
	cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl;


	//[1]
	cout << "[1]Base::baseI=" << *(int*)((int *)(&d) + 1);
	cout << "\t地址:" << (int *)(&d) + 1;
	cout << endl;


	//[2]
	cout << "[2]Base_::vptr";
	cout << "\t地址:" << (int *)(&d) + 2 << endl;

	//vprt[0]析构函数无法通过地址调用,故手动输出
	cout << "  [0]" << "Drive_multyBase::~Derive" << endl;

	//vprt[1]
	cout << "  [1]";
	Fun fun4 = (Fun)*((int *)*((int *)(&d)) + 1);
	fun4();
	cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 1) << endl;

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

	//[4]
	cout << "[4]Drive_multyBase::Drive_multyBaseI=" << *(int*)((int *)(&d) + 4);
	cout << "\t地址:" << (int *)(&d) + 4;
	cout << endl;

	getchar();
}

在x86下可以执行,因为指针和int占的内存相同,但在x64下不可以执行。
x86下sizeof(d)为20字节,x64下sizeof(d)为40字节,因为虚函数指针为8字节,然后内存对齐。

菱形继承(钻石型继承,或者称重复继承)

在这里插入图片描述
在这里插入图片描述
这时候D中有两个ib, 有两个ib2,在调用的时候,要加上类::来消除二义性

D d;
 
d.ib =1 ;               //二义性错误,调用的是B1的ib还是B2的ib?
 
d.B1::ib = 1;           //正确
 
d.B2::ib = 1;           //正确

内存布局如图所示,无法访问内存是因为存在二义性?VS日常抽风。
在这里插入图片描述
x86下sizeof是28 x64下sizeof是56.

虚继承

在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr), **在VS中,虚基类指针总是在虚函数表指针之后,**因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vfptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的偏移值,第一个条目存放虚基类所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。
在这里插入图片描述
在这里插入图片描述
虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值,这点我们在下面会验证。

虚继承的精彩在于引入虚基类指针,虚基类指针记录了虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,这样保证了虚基类在子类中只出现一次。这样做,可以解决二义性,但时间和空间效率上都会受到较大的影响。

简单虚继承

将上面的改为虚继承
在这里插入图片描述
其中虚基类指针跟在b1的__Vfptr后面。

在这里插入图片描述
这里为什么是24和48呢?
为什么会加个0呢?

复杂虚继承

#include<iostream>  
using namespace std;

class A  //大小为4  
{
public:
	int a;
};
class B :virtual public A  //大小为12,变量a,b共8字节,虚基类表指针4  
{
public:
	int b;
};
class C :virtual public A //与B一样12  
{
public:
	int c;
};
class D : virtual public B, public C //24,变量a,b,c,d共16,B的虚基类指针4,C的虚基类指针  
{
public:
	int d;
};

int main()
{
	A a;
	B b;
	C c;
	D d;
	d.a = 1;
	cout << sizeof(a) << endl;
	cout << sizeof(b) << endl;
	cout << sizeof(c) << endl;
	cout << sizeof(d) << endl;
	system("pause");
	return 0;
}

x64下
在这里插入图片描述
貌似就是B,C会有虚基类指针,来记录偏移量,但D不会有,所以才会出现这个结果,虚基类指针对debug是隐藏的,也看不到,这样看上去A似乎存在D上,B,C上的A只是指针。
在这里插入图片描述
但在b中,却又是实实在在的实体,这时指针被忽略了
在这里插入图片描述
在这个例子中,貌似又没有那个补充的0了。
也许是因为没有虚函数指针吧。

总结

在虚基类继承中,有虚函数的条件下会多加一个0,没有虚函数的条件下不会多加一个0.
在这里插入图片描述
而且内存布局是倒置的,先有B1,再有B基类在最下面,呵呵哒~
所幸调用顺序还是对的哈
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/fairyloycine/article/details/88144571