前言

上一篇博客C++对象模型的那些事儿之一为大家讲解了C++对象模型的一些基本知识,可是C++的继承,多态这些特性如何体现在对象模型上呢?单继承、多重继承和虚继承后内存布局上又有哪些变化呢?多态真正的底层又是如何实现呢?本篇博客就带大家全面理解一下C++对象模型,从而理解上述疑惑。

引例

还是以上篇博客的Animal类说起,假设我们有一个Dog类,它继承了Animal类。程序如下:

class Animal{
public:
	char name[10];//动物名字
	int weight;//体重
	virtual void eat(){
		cout<<"Animal eat"<<endl;
	}

	virtual void sleep(){
		cout<<"Animal sleep"<<endl;
	}
};

class Dog : public Animal{
public:
	int breed;//引入一个breed变量,表示狗的品种
	virtual void eat(){
		cout<<"Dog eat"<<endl;
	}
	virtual void yelp(){//狗吠
		cout<<"Dog yelp"<<endl;
	}
};

还是同样的问题,这些类占有多少内存空间呢?不急,我们在主函数中仍然添加如下代码:

Dog dog;
cout<<sizeof(dog)<<endl;//输出32

还是在这里卖个关子,为什么输出32?

C++的多态性

多态性是指相同对象收到不同消息或不同对象收到相同消息时产生的不同的实现动作,简单点说就是:“一种接口,多种方法”。C++支持两种多态性:

  • 编译时多态:通过函数重载实现
  • 运行时多态:通过虚函数实现

由于函数重载在编译时就确定了,不会影响到对象的内存布局,所以本篇博客不讨论。

通过虚函数实现的多态,由于虚表的存在就影响到了对象的内存布局,所以本篇博客着重讨论此种多态性。

C++以以下三种方式支持多态:

  • 经由一组隐式的转换操作
  • 经由virtual function机制
  • 经由dynamic_cast和typeid运算符

结合引例中定义的两个类,观察下列代码的输出:

int main(){
	//1.经由一组隐式转换
	Animal *animal = new Dog();//将一个anmial指针指向一个dog类对象virtual
	//2.经由virtual function机制
	animal->eat();
	//3.经由dynamic_cast运算符
	if(Dog *dog = dynamic_cast<Dog*>(animal){
		dog->eat()
	}
}

以上测试代码输出结果如下:

Dog eat	//由animal->eat()输出
Dog eat	//由dog->eat()输出

如果不考虑多态性的话,animal指针调用eat()应该输出Animal eat,下面的dog->eat()还是输出dog eat。

引入多态后,在运行时,编译器会检查animal的真实类型,它指向一个Dog类,于是通过虚函数多态特性输出dog eat,那么编译器在运行时是如何判断该指针指向对象的真实类型呢?又是如何定位到dog::eat函数呢?下面大家带着这些问题继续跟着我一起探索C++对象模型的底层机制。

引入继承后的C++内存布局

众所周知,C++的继承可以有以下几类:

  • 单一继承
  • 多重继承
  • 虚拟继承

下面就从这三个方面来继续探索:

单一继承

以引例中的继承关系来说明,单一继承即子类只从一个父类继承下来。如下图所示:

单一继承
单一继承

内存布局

上一篇博文中讲到,Animal类的内存布局由虚表指针和非静态成员变量name[10]和weight组成。

我们知道,在继承关系中,子类将获得父类对象所有的数据成员和成员函数。按照引例中提到的继承关系,dog类通过继承Animal类将拥有Animal类的两个非静态数据成员name[10]和weight,以及虚表指针。dog类自身有一个非静态数据成员breed和一个虚表指针,dog类里面是否直接存放两个虚表指针,还是编译器会将两个虚表合成一个呢?

答案明显是后者,dog类里面有且仅有一个虚表指针,指向自身的虚函数表,为了支持多态,编译器会根据继承关系来更新虚表。下面通过一张图来说明Dog类的内存布局。

单一继承下的内存布局
单一继承下的内存布局

如图所示,Dog类的虚表中依次存放这Dog::eat()、Animal::sleep()和Dog::yelp(),这个时候我们再来回顾一下为什么在C++多态性一节中父类指针调用eat()函数会输出Dog eat,很明显吧,虚表中虚函数Animal::eat()被替换成Dog::eat()了。

而且引例中sizeof(dog)=32也很容易理解了,Dog类的内存布局较Animal类多了一个int breed消耗,考虑到八字节对齐,dog的大小就等于32。

测试小结

在好奇心的驱使下,还是忍不住去编译器探个究竟。于是,狂敲了如下代码:

#include <stdio.h>
#include <iostream>
#include <string.h>

using namespace std;

typedef void(*Fun)(void);

class Animal{
public:
	char name[10];//动物名字
	int weight;//体重
	virtual void eat(){
		cout<<"Animal eat"<<endl;
	}

	virtual void sleep(){
		cout<<"Animal sleep"<<endl;
	}
};

class Dog : public Animal{
public:
	int breed;//引入一个breed变量,表示狗的品种
	virtual void eat(){
		cout<<"Dog eat"<<endl;
	}
	virtual void yelp(){
		cout<<"Dog yelp"<<endl;
	}
};

int main(){
	Dog dog;
	cout<<"表虚指针vptr的地址:"<<&dog<<endl;
	cout<<"虚函数表的地址:"<<(long long *)(*((long long*)&dog))<<endl;
	cout<<"测试虚表里的函数输出:"<<endl;
	cout<<"----第一个函数:";
	Fun pfun1 = NULL;
	pfun1 = (Fun)*((long long*)*(long long*)(&dog));
	pfun1();
	cout<<"----第二个函数:";
    Fun pfun2 = NULL;
 	pfun2 = (Fun)*((long long*)*(long long*)(&dog)+1);
 	pfun2();
 	cout<<"----第三个函数:";
 	Fun pfun3 = NULL;
 	pfun3 = (Fun)*((long long*)*(long long*)(&dog)+2);
 	pfun3();
 	for (int i = 0; i < 10; ++i)
    {
    	cout<<"name["<<i<<"]的地址为:"<<(long long *)&(dog.name[i])<<endl;//name每个参数的地址
    }
    cout<<"weight的地址为:"<<(long long *)&(dog.weight)<<endl;//weight的地址
    cout<<"breed的地址为:"<<(long long *)&(dog.breed)<<endl;
}

上述测试代码输出:

虚表指针vptr的地址:0x7ffd528d2100
虚函数表的地址:0x400f30
测试虚表里的函数输出:
----第一个函数:Dog eat
----第二个函数:Animal sleep
----第三个函数:Dog yelp
name[0]的地址为:0x7ffd528d2108
name[1]的地址为:0x7ffd528d2109
name[2]的地址为:0x7ffd528d210a
name[3]的地址为:0x7ffd528d210b
name[4]的地址为:0x7ffd528d210c
name[5]的地址为:0x7ffd528d210d
name[6]的地址为:0x7ffd528d210e
name[7]的地址为:0x7ffd528d210f
name[8]的地址为:0x7ffd528d2110
name[9]的地址为:0x7ffd528d2111
weight的地址为:0x7ffd528d2114
breed的地址为:0x7ffd528d2118

对比一下,与对象模型中的内存布局一致,Dog类重写了Animal类的eat()函数,所以为了支持多态性,在虚表中将Animal::eat() 替换成Dog::eat()。

注意:除了虚表指针必须在object内存最开始外,其他数据成员都是按照声明顺序依次摆放。

多重继承

多重继承,即一个类直接继承了多个基类,所以我们在引例的基础上新增一个犬科基类

class Canidae{
public:
	int age;
	virtual void eat(){
		cout<<"Canidae eat"<<endl;
	}
	virtual void jump(){
		cout<<"Canidae jump"<<endl;
	}
}

然后再修改一下说Dog类的继承关系

class Dog : public Animal,public Canidae{
public:
	int breed;//引入一个breed变量,表示狗的品种
	virtual void eat(){
		cout<<"Dog eat"<<endl;
	}
	virtual void yelp(){
		cout<<"Dog yelp"<<endl;
	}
};

这样Dog类就有两个直接基类了,如下表所示:

多重继承
多重继承

同样的,我们来看看继承两个基类后,Dog类所占的内存大小,运行cout<<sizeof(dog)<<endl后,输出40,还是在这里留个疑惑。

另外,我们来试试多态方面,执行如下代码:

Canidae * canidae = new Dog();
canidae->eat();

以上代码输出Dog eat,一样可以体现出多态性。在多重继承下,C++对象模型的内存布局又是以何种方式来支持这些特性呢?

内存布局

从上图的关系可以看出,Dog类将拥有两个父类的属性,其中从Animal类继承了name[10],weight以及一个虚表,从Canidae类继承了age和一张虚表。

如果继续按照单一继承下Dog类只拥有一张虚表的话,就无法区分eat()函数是重写了Animal::eat(),还是Canidae::eat(),可能在Dog类重写了eat()函数的情况下不好理解。假设此时Dog类没有重写eat()函数,那么如果只有一张虚表的话,怎么定位到Animal::eat()?怎么定位到Canidae::eat()?

按照上面的推论,Dog类应该是拥有两张虚表,也就是两个虚指针,分别指向从Animal类继承来的虚表和从Canidae类继承来的虚表。于是可以得出如下的布局图。

多重继承下的内存布局
多重继承下的内存布局

测试小结

下面通过一段测试代码来验证上述推论,代码如下:

#include <stdio.h>
#include <iostream>
#include <string.h>

using namespace std;

typedef void(*Fun)(void);

class Animal{
public:
	char name[10];//动物名字
	int weight;//体重
	virtual void eat(){
		cout<<"Animal eat"<<endl;
	}

	virtual void sleep(){
		cout<<"Animal sleep"<<endl;
	}
};

class Canidae{
public:
	int age;
	virtual void eat(){
		cout<<"Canidae eat"<<endl;
	}
	virtual void jump(){
		cout<<"Canidae jump"<<endl;
	}
};

class Dog : public Animal,public Canidae{
public:
	int breed;//引入一个breed变量,表示狗的品种
	virtual void eat(){
		cout<<"Dog eat"<<endl;
	}
	virtual void yelp(){
		cout<<"Dog yelp"<<endl;
	}
};

int main(){
	Dog dog;
	cout<<"从Animal继承来的虚表指针vptr的地址:"<<&dog<<endl;
	cout<<"从Animal继承来的虚表的地址:"<<(long long *)(*((long long*)&dog))<<endl;
	cout<<"测试Animal虚表里的函数输出:"<<endl;
	cout<<"----第一个函数:";
	Fun pfun1 = NULL;
	pfun1 = (Fun)*((long long*)*(long long*)(&dog));
	pfun1();
	cout<<"----第二个函数:";
    Fun pfun2 = NULL;
 	pfun2 = (Fun)*((long long*)*(long long*)(&dog)+1);
 	pfun2();
 	cout<<"----第三个函数:";
 	Fun pfun3 = NULL;
 	pfun3 = (Fun)*((long long*)*(long long*)(&dog)+2);
 	pfun3();
 	for (int i = 0; i < 10; ++i)
    {
    	cout<<"name["<<i<<"]的地址为:"<<(long long *)&(dog.name[i])<<endl;//name每个参数的地址
    }
    cout<<"weight的地址为:"<<(long long *)&(dog.weight)<<endl;//weight的地址
    cout<<"从Canidae继承来的虚表指针vptr的地址:"<<(long long*)(&dog)+3<<endl;
    cout<<"从Canidae继承来的虚表的地址:"<<(long long *)(*((long long*)&dog+3))<<endl;
    cout<<"测试Canidae虚表里的函数输出:"<<endl;
    cout<<"----第一个函数:";
	Fun pfun4 = (Fun)*((long long*)*((long long*)(&dog)+3));
	pfun4();
	cout<<"----第二个函数:";
	Fun pfun5 = (Fun)*((long long*)*((long long*)(&dog)+3)+1);
	pfun5();
	cout<<"age的地址为:"<<(long long *)&(dog.age)<<endl;
    cout<<"breed的地址为:"<<(long long *)&(dog.breed)<<endl;
}

测试结果:

从Animal继承来的虚表指针vptr的地址:0x7ffd5450e510
从Animal继承来的虚表的地址:0x4011c8
测试Animal虚表里的函数输出:
----第一个函数:Dog eat
----第二个函数:Animal sleep
----第三个函数:Dog yelp
name[0]的地址为:0x7ffd5450e518
name[1]的地址为:0x7ffd5450e519
name[2]的地址为:0x7ffd5450e51a
name[3]的地址为:0x7ffd5450e51b
name[4]的地址为:0x7ffd5450e51c
name[5]的地址为:0x7ffd5450e51d
name[6]的地址为:0x7ffd5450e51e
name[7]的地址为:0x7ffd5450e51f
name[8]的地址为:0x7ffd5450e520
name[9]的地址为:0x7ffd5450e521//因为内存对其填补了两个字节
weight的地址为:0x7ffd5450e524//weight占4个字节
从Canidae继承来的虚表指针vptr的地址:0x7ffd5450e528
从Canidae继承来的虚表的地址:0x4011f0
测试Canidae虚表里的函数输出:
----第一个函数:Dog eat
----第二个函数:Canidae jump
age的地址为:0x7ffd5450e530
breed的地址为:0x7ffd5450e534

从地址上可以看出,在内存布局上与上一小节的内存布局图相同,虚表内存放的函数指针通过测试输出也符合上图。

注意:在多重继承的内存布局中是按照继承顺序依次摆放从基类继承下来的数据成员和虚表指针。

看到这里,应该就能理解为什么sizeof(dog)会输出40了,大家可以按照内存布局推一推。

虚拟继承

再讲解虚继承之前,我先引入一个家畜(Livestock)类,这个类由Animal类派生而来

class Livestock : public Animal{
public: 
	int color;//表示家畜的颜色
	virtual void eat(){
		cout<<"Livestock eat"<<endl;
	}
	virtual void watch(){//看门
		cout<<"Livestock watch"<<endl;
	}
}

然后在调整一下上面的犬科(Canidae)类,并由Canidae类和Livestock类派生出WatchDog类,说白了就是一条看门狗。

class Canidae : public Animal{
public:
	int age;
	virtual void eat(){
		cout<<"Canidae eat"<<endl;
	}
	virtual void jump(){
		cout<<"Canidae jump"<<endl;
	}
};
class WatchDog : public Canidae,public Livestock{
public:
	int breed;//引入一个breed变量,表示狗的品种
	virtual void eat(){
		cout<<"Dog eat"<<endl;
	}
	virtual void yelp(){
		cout<<"Dog yelp"<<endl;
	}
};

上述继承关系可以如下图所示:

多重继承2
多重继承2

按照多重继承的内存布局方式的话,WatchDog类的内存布局应该如下图所示:

WatchDog类的内存布局
WatchDog类的内存布局

如果这时候你运行如下程序:

WatchDog watchdog;
watchdog.name[1];

就会出现二义性错误,编译器不知道调用Canidae::name[1]还是Livestock::name[1],虽然你可以通过显示指定watchdog.Canidae::name[1]来调用Canidae::name[1],但是两个name[10]和两个weight完全不符合常理,而且完全没有必要的增大了内存消耗。

内存布局

针对上述问题,C++引入了虚继承的概念,虚继承是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。虚继承下WatchDog类中只有一个Animal实例。其继承关系如下:

虚继承
虚继承

按照虚继承的特点,WatchDog类中只有一份Animal实例,则其内存布局推测为如下图:

WatchDog类的内存布局3
WatchDog类的内存布局3

测试小结

多重虚拟继承比较复杂,其测试代码如下:

#include <stdio.h>
#include <iostream>
#include <string.h>

using namespace std;

typedef void(*Fun)(void);

class Animal{
public:
	char name[10];//动物名字
	int weight;//体重
	virtual void eat(){
		cout<<"Animal eat"<<endl;
	}

	virtual void sleep(){
		cout<<"Animal sleep"<<endl;
	}
};

class Livestock : public virtual Animal{
public: 
	int color;//表示家畜的颜色
	virtual void eat(){
		cout<<"Livestock eat"<<endl;
	}
	virtual void watch(){//看门
		cout<<"Livestock watch"<<endl;
	}
};

class Canidae : public virtual Animal{
public:
	int age;
	virtual void eat(){
		cout<<"Canidae eat"<<endl;
	}
	virtual void jump(){
		cout<<"Canidae jump"<<endl;
	}
};
class WatchDog :  public  Canidae , public  Livestock{
public:
	int breed;//引入一个breed变量,表示狗的品种
	virtual void eat(){
		cout<<"WatchDog eat"<<endl;
	}
	virtual void yelp(){
		cout<<"WatchDog yelp"<<endl;
	}
};

int main(){
	WatchDog watchdog;
	cout<<"从Canidae继承来的虚表指针vptr的地址:"<<&watchdog<<endl;
	cout<<"从Canidae继承来的虚表的地址:"<<(long long *)(*((long long*)&watchdog))<<endl;
	cout<<"测试Canidae虚表里的函数输出:"<<endl;
	cout<<"----第一个函数:";
	Fun pfun1 = NULL;
	pfun1 = (Fun)*((long long*)*(long long*)(&watchdog));
	pfun1();
	cout<<"----第二个函数:";
    Fun pfun2 = NULL;
 	pfun2 = (Fun)*((long long*)*(long long*)(&watchdog)+1);
 	pfun2();
 	cout<<"----第三个函数:";
 	Fun pfun3 = NULL;
 	pfun3 = (Fun)*((long long*)*(long long*)(&watchdog)+2);
 	pfun3();
 	cout<<"Canidae::age的地址为:"<<(long long *)&(watchdog.age)<<endl;

    cout<<"从Livestock继承来的虚表指针vptr的地址:"<<(long long*)(&watchdog)+2<<endl;
    cout<<"从Livestock继承来的虚表的地址:"<<(long long *)(*((long long*)&watchdog+2))<<endl;
    cout<<"测试Livestock虚表里的函数输出:"<<endl;
    cout<<"----第一个函数:";
	Fun pfun5 = (Fun)*((long long*)*((long long*)(&watchdog)+2));
	pfun5();
	cout<<"----第二个函数:";
	Fun pfun6 = (Fun)*((long long*)*((long long*)(&watchdog)+2)+1);
	pfun6();
	cout<<"Livestock::color的地址为:"<<(long long *)&(watchdog.color)<<endl;
	cout<<"WatchDog::breed的地址为:"<<(long long *)&(watchdog.breed)<<endl;

	cout<<"从Animal虚继承来的虚表指针vptr的地址:"<<(long long*)(&watchdog)+4<<endl;
	cout<<"从Animal虚继承来的虚表的地址:"<<(long long *)(*((long long*)&watchdog+4))<<endl;
	cout<<"测试Animal虚表里的函数输出:"<<endl;
	cout<<"----第一个函数:";
	Fun pfun8 = (Fun)*((long long*)*((long long*)(&watchdog)+4));
	pfun8();
    cout<<"----第二个函数:";
    
	Fun pfun7 = (Fun)*((long long*)*((long long*)(&watchdog)+4)+1);
	pfun7();

	for (int i = 0; i < 10; ++i)
    {
    	cout<<"name["<<i<<"]的地址为:"<<(long long *)&(watchdog.name[i])<<endl;//name每个参数的地址
    }
    cout<<"weight的地址为:"<<(long long *)&(watchdog.weight)<<endl;//weight的地址
}

以上测试代码输出结果为:

从Canidae继承来的虚表指针vptr的地址:0x7fffebf824e0
从Canidae继承来的虚表的地址:0x4014f8
测试Canidae虚表里的函数输出:
----第一个函数:WatchDog eat
----第二个函数:Canidae jump
----第三个函数:WatchDog yelp
Canidae::age的地址为:0x7fffebf824e8
从Livestock继承来的虚表指针vptr的地址:0x7fffebf824f0
从Livestock继承来的虚表的地址:0x401528
测试Livestock虚表里的函数输出:
----第一个函数:WatchDog eat
----第二个函数:Livestock watch
Livestock::color的地址为:0x7fffebf824f8
WatchDog::breed的地址为:0x7fffebf824fc
从Animal虚继承来的虚表指针vptr的地址:0x7fffebf82500
从Animal虚继承来的虚表的地址:0x401558
测试Animal虚表里的函数输出:
----第一个函数:watchdog eat
----第二个函数:Animal sleep
name[0]的地址为:0x7fffebf82508
name[1]的地址为:0x7fffebf82509
name[2]的地址为:0x7fffebf8250a
name[3]的地址为:0x7fffebf8250b
name[4]的地址为:0x7fffebf8250c
name[5]的地址为:0x7fffebf8250d
name[6]的地址为:0x7fffebf8250e
name[7]的地址为:0x7fffebf8250f
name[8]的地址为:0x7fffebf82510
name[9]的地址为:0x7fffebf82511
weight的地址为:0x7fffebf82514

从上述测试结果中可以看出,watchdog的内存布局中依次摆放的是:

  • 从Canidae类继承来的虚表和age
  • 从Livestock类继承来的虚表和color
  • watchdog自身的bread
  • 从Animal超类继承来的虚表、name[10]和weight

最后,在运行一下sizeof(watchdog) = 56,与我们的内存布局一样。

结束语

本篇博客对引入继承关系后类的内存布局做了相对全面的分析和测试,从内存地址上一步一步推算和验证了常见继承关系下的类内存布局。不过,还有些许不完善的地方,比如单一虚拟继承这种情况就没有分析到,不过,一般虚拟继承都是将超类放在类的最后面,有且仅有一份;再比如,由于编译器的不同可能会导致内存布局上微小的差异性,这方面可以参考陈浩专栏的C++ 对象的内存布局系列文章。

About Me

由于本人也是初学,在写作过程中,难免有错误的地方,读者如果发现,请在下面留言指出。

最后,如有疑惑或需要讨论的地方,可以联系我,联系方式见我的个人博客about页面,地址:About Me

另外,本人的第一本gitbook书已整理完,关于leetcode刷题题解的,点此进入One day One Leetcode

欢迎持续关注!Thx!