【深度探索C++对象模型】第一章 关于对象
第一章 关于对象(Object Lessons)
—— 本书作者:Stanley B.Lippman
一、前言
什么是 C++ 对象模型:简单的说。就是 C++ 中面向对象的底层实现机制。
本书组织:
第 1 章,关于对象(Object Lessons),介绍 C++ 对象的基础概念,给读者一个粗略的了解。
第 2 章,构造函数语意学(The Semantics of Constructors),构造函数什么时候会被编译器合成?它给我们的程序效率带来了如何的影响?
第 3 章。Data语意学(The Semantics of Data),讨论 data members 的处理。
第 4 章,Function语意学(The Semantics of Function),讨论类的各种成员函数,特别是 Virtual 。
第 5 章,构造、析构、拷贝语意学(Semantics of Construction, Destruction, and Copy),探讨怎样支持 class 对象模型,以及 object 的生命周期。
第 6 章,运行期语意学(Runtime Semantics),暂时对象的生与死,new 与 delete 的支持。
第 7 章,在对象模型的顶端(On the Cusp of the Object Model),专注于 exception handling, template support, runtime type identification(RTTI)。
读完此书,或者此系列blogs,会让你对 C++ 的 class 有更深的了解。你将知道虚函数的实现方式,以及它所带来的负担。等等等等,这里有你想知道关于 class 的一切。
在 C 语言中,“数据”和“处理数据的操作(函数)”是分开来声明的。也就是说,语言本身并没有支持“数据和函数”之间的关联性。
我们把这样的程序方法称为“程序性的”。比如。我们声明一个 struct Point3d:
typedef struct _Point3d
{
float x;
float y;
float z;
} Point3d;
|
void Point3d_print( const Point3d* pd )
{
printf("(%g, %g, %g)", pd->x, pd->y, pd->z);
}
//%g和%G是实数的输出格式符号。它是自己主动选择%f和%e两种格式中较短的格式输出。而且不输出数字后面没有意义的零。
|
class Point
{
public:
Point( float x = 0.0 ) : _x(x) {}
float x() { return _x; }
void x( float val ) { _x = val; }
// ...
protected:
float _x;
};
class Point2d : public Point
{
public:
Point2d( float x = 0.0, float y = 0.0 ) : Point( x ), _y( y ) {}
// ...
protected:
float _y;
}
class Point3d : public Point2d
{
public:
Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) : Point2d( x, y ), _z( z ) {}
// ...
protected:
float _z;
}
|
当一个 Point3d 转换到 C++ 之后,第一个可能会问的问题是:加上了封装之后,布局成本添加了多少呢?答案是: class Point3d 并没有添加成本。
三个 data members 直接内涵在每个 class Object 之中。而 成员函数(member functions)尽管在 class 的声明之内,但却不会出如今 class 的对象实体(Object)中。每个非 inline member function 仅仅会诞生一个函数实体。而 inline function。会在其每个使用者身上产生一个函数实体。
后面你将看到,C++ 在布局和存取时间上基本的负担 是由 virtual 引起的。包含 虚函数 以及 虚基类。
二、C++ 的对象模型
首先,C++ 中。
2种成员变量(class data members):静态的(static) 和 非静态的(non-static);
3种成员函数(class member functions):静态的、非静态的 和 虚拟的(virtual)。
|
class Point
{
public:
Point( float valx );
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print( ostream &os ) const;
float _x;
static int _point_count;
};
|
【注】原书这里介绍了 简单对象模型 和 表格驱动的对象模型 。这里跳过这两个,直接看 C++ 对象模型。
在 C++ 对象模型中。
非静态的(non-static)成员变量 被配置于每个 class object 之内;
静态的(static)成员变量 则被存放在全部 class object 之外。也就是全局数据区。(问:假设是这样。我们的 class 怎么样去全局数据区找到属于它的 static 成员变量?别急,后面会有答案)。
静态和非静态的成员函数,也被配置于 每个 class 的实体之外。
虚函数的配置方法是:
1. 每个 class 产生出一堆指向 virtual functions 的指针,并把这些指针放在表格之中。这个表。既是所谓的 虚函数表(virtual table), 或 vtbl;
2. 每个 class 的实体(object) 被加入了一个指针。指向相关的(注意不一定是同一个) virtual table。
通常这个指针被称为 vptr。
vptr 的设定和重置都有每个 class 的 构造函数、析构函数、拷贝以及复制运算符。每个 class 所关联的 type_info object( 用以支持 runtime type identification, RTTI )也经由 virtual table 被指出来,一般是放在表格的第一个 slot 处。
三、C++ 怎样支持多态
1. 经由一组隐含的转化操作。比如。把一个 派生类 的指针转化为一个指向其 public base type 的指针:
shape* ps = new circle();
2. 经由 virtual functions 机制:
ps->rotate();
3. 经由 dynamic_cast 和 typeid 运算符:
if ( circle *pc = dynamic_cast< circle* >(ps) )...
多态的主要用途。是经由一个共同的接口。来影响类型的封装,我们一般会把这个接口定义在一个抽象基类里面。然后再在派生类里重写这个接口。
四、须要多少内存来表现一个 class object?
猜想以下的代码的 sizeof 结果会是?
.eg.1.
class Base
{
public:
Base();
~Base();
};
// sizeof(Base) = ?
|
class Base
{
public:
Base();
~Base();
protected:
double m_Double;
int m_Int;
char m_BaseName;
};
// sizeof(Base) = ?
|
1. 其 非静态的成员变量( non-static data members ) 的总和大小。
2. 加上不论什么因为 内存对齐 的需求而填补上去的控件。
3. 加上为了支持 virtual 而由内部产生的不论什么额外的负担。
此外,须要注意的是。一个指针(或是一个 reference)。无论它仅仅想哪一种数据类型。指针本身所需内存大小是固定的。比方。在 win32下,一个指针的大小就是4个字节(byte)。
问题的答案:
第一题: 答案是1。
class Base 里仅仅有构造函数和析构函数,由前面的内容所知,class 的 member functions 并不存放在 class 以及事实上例内,因此,sizeof(Base) = 1。是的,结构不是0,而是1,原因是由于,class 的不同实例,在内存中的地址各不同样,一个字节仅仅是作为占位符,用来给不同的实例分配不同的内存地址的。
第二题:答案是16。
double 类型的变量占用8个字节。int 占了4个字节,char 仅仅占一个字节。但这里它会按 int 进行对齐,Base 内部会填补3个字节的内存大小。
最后的大小就是 8 + 4 + 1 + 3 = 16。 大家能够调整三个成员变量的位置,看看结果会有什么不同。
|
五、指针的类型
Base* p_Base;
int* p_Int;
vector<string> * p_vs;
|
请问,一个指向 Base class 的指针和一个指向 int 的指针是怎样产生不同的呢?
1. 以内存需求的观点来说,没有不同。在32位机器上,它们都须要4个字节的内存空间。
2. “指向不同内存的各指针”间的差异。在于其所寻址出来的 object 的类型不同。
也就是说,“指针类型”会教导编译器怎样解释某个特定地址中的内存内容及其涵盖大小。
比方:一个指向 int 的指针,如果其地址是 1000。在32位及其上。将涵盖地址空间 1000~1003.
那么。一个指向地址 1000 的 void* 的指针,将涵盖如何的地址空间呢?没错,我们并不知道。这就是为什么一个类型为 void* 的指针。仅仅可以含有一个地址,而不可以通过它操作所指的 object 的缘故。
所以。转型(cast)事实上是一种编译器指令,它所做的,并非改变指针所含的真正地址,而是教导编译器该去怎样解释指针所涵盖的地址空间。
六、小结
第一章——关于对象。本章初步介绍了C++的对象模型是如何的,后面的章节将继续讨论这个对象模型的底层实现机制。
在读完本篇文章之后。你应该理解:
- 怎样计算 sizeof(classA) 的大小;
- 了解 class 的内存布局。
在下一章——构造函数语意学中。我们将了解关于类的构造函数的很多其它知识。