这次是这一个系列技术分享的第二部分,主要介绍的是C++对象模型。这里所讲的对象模型,是指如何在内存中(空间上)表达对象的性质和特征,以及如何在运行时(时间上)提供完成面向对象系统中的必要操作的能力。
Table of Content
C++并不是天生地庞大而迟缓
—— Stanley B. Lippman
- 概述
- 对象的内存布局
-
运行期环境
1. 概述
对于传统的C语言程序来讲,数据与算法是分离的:
struct Circle
{
int x, y;
int radius;
};
Circle MakeCircle(int x, int y, int radius) { //.... } void DrawCircle(Circle c) { //.... }
在C++中,将这样的与传统C语言结构体内存布局兼容的对象叫做Plain Old Data(POD)。
此外,我们还可以将数据和算法整合在一起:
class Circle
{
public:
Circle(int x, int y, int radius);
void Draw();
private:
int x, y;
int radius;
};
对于这样的代码,在时空上是否增加了什么负担?同时,C++还支持继承、多重继承等很多机制,它们呢?
class Point {
public:
Point(int x, int y);
private:
int x, y;
};
class Circle : public Point { //... };
我们先从最朴素的模型开始,看一看如何来安排对象的内存空间:
- 简单对象模型
在简单对象模型中,对象中只存储指向成员(包括数据成员和方法成员)的指针,所有操作均通过指针索引来完成。
- (双)表格驱动模型
双表格驱动模型是指在对象中仅存储两个指针:一个指向数据成员table的指针,一个指向方法成员table的指针。这种模型的优点在于所有对象的内存空间占用及内存布局均完全一致。早期的某款C++编译器采用了这一种模型。
以上两种模型都是可以在一定程度上满足需求的,但并未被大多数编译器所采用。目前主流的模型如下:
- 非静态数据存放于对象内部
- 静态数据、非virtual函数存放于对象之外
- 每一个class至少有一张虚函数表
- 每一个object中包含一个指向该表的指针
本文将探讨在单继承、多继承、虚拟继承下C++对象模型是如何应对的。
2. 对象的内存布局
2.1 类
对于如下的代码来说:
class Circle
{
public:
Circle(int x, int y, int radius);
void Draw();
static void Print();
private:
int x, y;
int radius;
static int count;
};
根据前述的对象模型,其其布局与在C语言中使用结构体时是一致的:
同时C++标准保证,在同一个权限声明块中(即同一个public、protected或private块中),后声明的成员在内存中位于高地址(更靠后)。但并没有规定其一定紧邻,其间可以插入其他编译器所需的数据。
2.2 不包含虚函数的单继承
而对于单继承来说:
class Point
{
public:
Point(int x, int y);
protected:
int x, y;
};
class Circle : public Point { protected: int radius; };
其内存布局如下:
可以看出,其占用内存的成本并没有增加。然而是否一定不会增加呢?如果我们有如下代码:
class Point {
public:
//...
protected:
int x, y;
char c1, c2;
};
class Circle : public Point {
public:
//...
protected:
char c3, c4;
int radius;
};
此时的内存布局将为:
由于需要对其内存,编译器很可能会插入padding字节。这时,继承的内存占用将会比数据全部在同一个类中的时候大。不过,我们是否可以取消掉padding呢?
答案自然是否定的。取消padding后,Point对象和Circle对象中Point部分在内存中占用的大小将会不同,此时如果存在以下代码:
void some_func(Point* point) { Point a; *point = a; }
由于Circle和Point存在继承关系,我们不能在编译期确定代码中point的真实类型是什么。如果point一旦为Circle类型,那么在执行到*point=a这一句代码时,将会多覆盖两个字节的内存,即c3和c4将被内容不确定的内存覆盖。
2.3 包含虚函数的单继承
对于包含虚函数的单继承来说,其内存中将会增加一个指向虚表的指针。
class Point {
public:
virtual void Print();
protected:
int x, y;
};
class Circle {
public:
virtual void Print();
protected:
int radius;
};