C++菱形继承对象内存布局实战讲解和分析

本文主要讲解C++对象模型中的菱形继承的对象模型,分别讨论基类对象变量和函数的继承问题。
何为菱形继承:
菱形继承是指一个基类(Base)派生出两个派生类(Derived1,Derived2),然后这两个派生类(Derived1,Derived2)派生出一个最终的派生类,如1.1的下图所示。

一、菱形继承之非虚继承

1.1类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的UML结构图

在这里插入图片描述

1.2类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的代码定义

NonVirtualDerivedDiamondClass.cpp

#include <iostream>

using namespace std;

class Base
{
    
    
public:
	Base(int x) : x(x) {
    
    }

protected:
	int x;

};

class Derived1 : public Base
{
    
    
public:
	Derived1(int y1) : Base(1), y1(y1) {
    
    }

protected:
	int y1;
};

class Derived2 : public Base
{
    
    
public:
	Derived2(int y2) : Base(1), y2(y2) {
    
    }

protected:
	int y2;
};

class DDerived : public Derived1, public Derived2
{
    
    
public:
	DDerived(int z) : Derived1(11), Derived2(22), z(z) {
    
    }
	void callX()
	{
    
    
		cout << this->x << endl;
	}

protected:
	int z;
};

1.3最终派生类DDerived对象模型

由于最终的派生类包含了基类Base、派生类Derived1,Derived2的对象模型,因此只分析最终派生类DDerived对象模型即可。
VS2017开发者模式查看C++对象模型方法可以参考这篇博客:C++单继承类对象内存布局实战讲解和分析

  • DDerived在内存中的布局
    在这里插入图片描述
    由上图可知,菱形继承派生类Derived1和Derived2对象的内存中各自继承和保存基类Base的成员变量int x;。当我们在最终派生类DDerived上调用成员变量x时,会出现歧义,DDerived不知道调用那个类对象的x。此时编译会报错,成员变量x调用歧义,如下图所示。
    在这里插入图片描述
    如果我们要在最终派生类DDerived调用继承而来的x,那么就要显示指定调用的作用域(“::”)限定符,指明是调用哪个基类继承过来的x,即this->Derived2::x,如下代码所示:
class DDerived : public Derived1, public Derived2
{
    
    
public:
	DDerived(int z) : Derived1(11), Derived2(22), z(z) {
    
    }
	void callX()
	{
    
    
		cout << this->Derived2::x << endl; // 显示指定作用域Derived2::x,调用Derived2的成员变量x
	}

protected:
	int z;
};

从DDerived内存布局中可以看出,派生类Derived1和Derived2的类对象都各自保存了一份从基类Base继承而来的成员变量int x;这样不但会造成最终派生类DDerived获取变量x出现歧义,同时也会造成内存浪费。那么,是否有办法解决这些问题呢?答案是肯定的,那就是采用虚继承。

二、菱形继承之虚继承

2.1类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的UML结构图

在这里插入图片描述
由上图可知,只有派生类Derived1和派生类Derived2继承类Base时采用虚继承,而最终派生类DDerived继承Derived1和Derived2时采用普通继承。即

Derived1 : public virtual Base {
    
     ... };
Derived2 : public virtual Base {
    
     ... };
DDerived : public Derived1, public Derived2 {
    
     ... };

2.2类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的代码定义

VirtualDerivedDiamondClass.cpp

#include <iostream>

using namespace std;

class Base
{
    
    
public:
	Base() = default;
	Base(int x) : x(x) {
    
    }

protected:
	int x;

};

class Derived1 : public virtual Base
{
    
    
public:
	Derived1(int y1) : Base(1), y1(y1) {
    
    }

protected:
	int y1;
};

class Derived2 : public virtual Base
{
    
    
public:
	Derived2(int y2) : Base(1), y2(y2) {
    
    }

protected:
	int y2;
};

class DDerived : public Derived1, public Derived2
{
    
    
public:
	DDerived(int z) : Derived1(11), Derived2(22), z(z) {
    
    }
	void callX()
	{
    
    
		cout << this->Derived2::x << endl;
	}

protected:
	int z;
};

2.3最终派生类DDerived对象模型

由于最终的派生类包含了基类Base、派生类Derived1,Derived2的对象模型,因此只分析最终派生类DDerived对象模型即可。
VS2017开发者模式查看C++对象模型方法可以参考这篇博客:C++单继承类对象内存布局实战讲解和分析

  • DDerived在内存中的布局 在这里插入图片描述
    由上图可知,最终派生类DDerived的对象模型中,派生类Derived1和派生类Derived2都没有产生一份基类Base的成员变量int x;的内存,而是多了一个虚指针。该虚指针分别指向各自的虚函数表。虚函数表中存放了变量x的偏移地址。通过该偏移地址派生类Derived1和派生类Derived2就可以获取变量x。此时最终派生类可以直接用this指针调用变量x而不会产生歧义,如下图所示。
    在这里插入图片描述
    因此,虚拟继承主要是继承基类成员变量的偏移地址,该偏移地址是保存在虚指针指向的虚函数表上,排列顺序为按照变量的声明顺序进依次排列。如下图所示:
    在这里插入图片描述
    该虚继承的类都没有虚函数,那么假如基类存在虚函数,那么虚继承后的菱形继承最终派生类的类对象模型是怎么样的呢?接下来继续分析和讨论。

三、基类有虚函数的菱形继承之虚继承

3.1类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的UML结构图

在这里插入图片描述
由上图可知,基类Base和派生类Derived1、Derived2都有虚析构函数和一个虚函数vfun1();,说明这是一个继承中有虚函数的类,即非POD类型的类,内存对象不可逐字节拷贝memcpy(…)。

3.2类Base、派生类Derived1、派生类Derived2、最终派生类DDerived的代码定义

#include <iostream>

using namespace std;

class Base
{
    
    
public:
	Base() = default;
	virtual ~Base() {
    
    }
	
	Base(int x) : x(x) {
    
    }

protected:
	int x;

private:
	virtual void vfun1() = 0;
};

class Derived1 : public virtual Base
{
    
    
public:
	Derived1(int y1) : Base(1), y1(y1) {
    
    }
	virtual ~Derived1() {
    
    }
	virtual void vfun1() override
	{
    
    
		cout << "virtual Derived1::vfun1()" << endl;
	}

protected:
	int y1;
};

class Derived2 : public virtual Base
{
    
    
public:
	Derived2(int y2) : Base(1), y2(y2) {
    
    }
	virtual ~Derived2() {
    
    }
	virtual void vfun1() override
	{
    
    
		cout << "virtual Derived2::vfun1()" << endl;
	}

protected:
	int y2;
};

class DDerived : public Derived1, public Derived2
{
    
    
public:
	DDerived(int z) : Derived1(11), Derived2(22), z(z) {
    
    }

	virtual void vfun1() override
	{
    
    
		cout << "virtual DDerived::vfun1()" << endl;
	}

	void callX()
	{
    
    
		cout << this->x << endl;
	}

protected:
	int z;
};

3.3最终派生类DDerived对象模型

在这里插入图片描述
图3-1 有虚函数的菱形继承之虚继承图
在这里插入图片描述
图3-2 没有虚函数的菱形继承之虚继承图
由上图3-1和对比图3-2可知,有虚函数的菱形继承之虚继承的最终派生类DDerived对象模型跟没有虚函数的菱形继承之虚继承的最终派生类DDerived基本一样,差别只有一个,那就是基类Base多了一个虚指针,该虚指针指向DDerived自身的虚函数表。这个虚函数表跟单继承的虚函数表一样,里面存放的都是DDerived自身的虚函数或者继承而来的虚函数。虚函数表的定义规则是,先将基类虚函数表内容拷贝一份到DDerived自身虚函数表中,然后用DDerived自身的虚函数覆盖虚函数表中同名的虚函数。
同理,当有静态成员函数和静态成员变量、普通成员函数时,DDerived的类内存模型也同样不受影响,具体代码博主就不贴出来了,留一个小作业各位读者自己验证。

四、总结

  • 菱形虚继承后基类的成员变量只有一份内存,不会在派生类中拷贝一份同样的成员变量占内存;
  • 虚继承后派生类不会拷贝基类成员变量,而是产生一个虚指针指向自身的虚函数表,该虚函数表存放获取基类成员变量的偏移地址;
  • 虚继承中的类存在虚函数,跟没有虚函数的虚继承只有一个差别,那就是产生当前类的虚指针,该虚指针指向最终的派生类的虚函数表,该虚函数表存放最终派生类的所有替换后的虚函数或者继承而来的虚函数地址;
  • 只有非静态成员才占对象模型的内存;
  • 类对象的静态变量和静态函数都不占用对象模型的内存,存放在静态储存区;
  • 类对象的普通成员函数也不占用对象模型的内存,存放在普通数据区

五、参考内容

c++之菱形继承问题
C++对象模型和布局(三种经典类对象内存布局)
C++中菱形继承的基本概念及内存占用问题
C++之继承(多重继承+多继承+虚继承+虚析构函数+重定义)
《深度探索C++对象模型》 侯捷 page:83-134

猜你喜欢

转载自blog.csdn.net/naibozhuan3744/article/details/114192980