6.1 对象的构造和析构
全局对象
Matrix identity;
int main(){
Matrix m1 = identity;
return 0;
}
C++保证一定会在main()
函数第一次用到 identity 之前,把 identity 构造出来的是,在main()
函数接收之前把 identity 销毁。类似identity
的全局变量如果有构造和析构的话,称它需要静态的初始化和内存释放操作。
C++所有的全局变量都放在程序的数据段中。如果显式指定一个值,那么将以它为初值;否则对象配置到的内存内容为 0。
对象数组
e.g. Point knots[10]
如果 Point 没有定义构造函数,也没有定义析构函数,那么只需要配置一个足够的内存存储10个连续的 Point 对象即可。
如果 Point 定义了构造函数和析构函数,那么必须轮流地施行于每个元素之上。一般是由一个或多个runtime library 函数完成的。
在 cfront 中,使用vec_new()
函数,产生出以类对象构造而成的数组:
void *vec_new(void *array,
size_t elem_size, //每个类对象的大小
int elem_count,
void (*constructor)(void*),
void (*destructor)(void*, char)
)
constructor
和destructor
是这个类的默认构造和析构的函数指针。- 参数
array
如果不是数组的地址,那么就是0,如果是0,那么数组将由应用程序的new运算符被动态配置于heap中。 - 参数
elem_size
表示数组中元素个数。
6.2 new和delete运算符
程序在调用new
之后,总是会调用operator new
分配内存 然后再调用构造函数初始化那部分内存。delete
同理。
在类中重载的operator new
和operator delete
是隐式静态的,因为前者运行于对象构造之前,后者运行于对象析构之后,所以不能也不应该拥有一个 this 指针来存取数据。
当使用 new expression 来动态分配数组的时候,Lippman 在《深度探索C++对象模型》中指出:当分配的类型有一个默认构造函数的时候,new expression将调用一个所谓的vec_new()函数来分配内存,而不是operator new内存。
现在测试发现:不论有没有构造函数,new expression都是调用operator new
来分配内存,并在此之后,调用默认构造函数逐个初始化它们,而不调用所谓的vec_new(),也许 cfront 确实离我们有点遥远。
operator new
运算符总是以malloc()
函数实现,operator delete
总是以free()
函数实现
int *pi = new int(5);
实际上由两个步骤完成:
- 通过适当的new运算符函数实例配置内存
- 将配置得来的对象设立初值
初始化操作应该在内存配置成功后才执行。int *pi;
if( pi = malloc(sizeof(int)))
*pi = 5;
delete
必须确保指针的值不是0,否则不会执行delete操作:
delete pi;
//实际操作:
if(pi != 0)
free(pi);
指针所指对象的生命周期会因为delete
而结束,但是可能那块内存还有值,所以后续对其的操作是未定义的,而不是错误的!这一点很有可能引发无法预料的bug。
针对数组的 new 语意学
当写下:
int *p_array = new int[5];
vec_new()
不会被调用,因为它的主要作用好似将默认构造函数施行于类对象组成的数组的每一个元素上。而是new运算符函数
会被调用:
int *p_array = (int*)__new(5*sizeof(int));
但是如果定义了默认构造函数的类进行这样的操作,那么vec_new()
函数就会被调用。
寻找数组维度会对delete
运算符的效率带来冲击;所以只有在中括号出现的时候,编译器才会寻找数组的维度,否则它便假设只有一个对象要被删除,这会造成只有第一个对象被析构,其他元素仍然存在。
Placement Operator new的语意
有一个重载的new
运算符,称为placement operator new
,需要第二个参数,类型为 void* :
仅仅只传回一个指针。
void* operator new(site_t,void *p)
{ return p;}
先明确调用了 placement operator new :
point pt = (point )operator new(sizeof(point), p) ; 输出结果显示此时 point的默认构造函数并不会被调用。
然后通过new expression 的方式来间接调用placement operator new:point *pt=new(p) point();
这个时候 point 的默认的构造函数被调用了。
可见 placement operator new并没有什么奇特的地方,它与一般的operator new
不同处在于,它不会申请内存。它也不会在指定的地址调用构造函数,而调用构造函数的的全部原因在于 new expression 总是先调用一个匹配参数的 operator new
然后再调用指定类型的匹配参数的构造函数,而说到底 placement operator new 也是一个 operator new
。
Point2w *ptw = new(arena) Point2w;
其中,arena
指向内存中的一个区块,用来存放新产生的Point2w的对象。
如果 placement operator new 在原来已经存在一个对象的空间上构造新的对象,那么原来对象的析构函数不会被自动调用。调用该析构函数的方式是将那个指针 delete 掉,但是 delete 了之后还怎么使用呢?
所以应该先显式调用那个对象的析构函数,再进行 placement operator new:
p2w->~Point2w;
p2w = new(arena) Point2w;
新鲜的存储空间可以这样配置而来:
char *arena = new char[sizeof(Point2w)];
new(arena) Point2w;
但是一般而言 placement new不支持多态,因为预先配置好的空间是一定的,如果派生类比基类大,那么派生类的构造函数将会被严重破坏。
此处最后的b.f()
会调用基类的。因为此时b中存储的实际上是一个派生类对象,但是通过一个对象.
调用虚函数,而不是多态指针,将被静态决议出来,而不会走虚函数机制。
6.3 临时性对象
注意下面二者的区别:
T operator+(const T&,const T&);
//1st implementation
T c = a + b;
//2nd implementation
T c;
c = a+b;
1st 版本 其实是 a+b 的值直接通过 c 的拷贝构造给予 c ;
2nd 版本 其实是 拷贝赋值运算符参与其中,不能忽略临时对象的产生。它的调用会导致下面的结果:
(2)的代码:直接将 c 传递到运算符函数中是有问题的。因为运算符函数并不为其外加参数调用一个析构,所以必须再此调用之前先调用析构函数。为什么不能省略那个临时对象,比如直接这样:
c = a + b; //c.operator(a + b)
//convert to..C++伪代码:
c.T::~T();
c.T::T(a + b);
不行,原因在于拷贝构造函数、析构函数以及赋值操作符都可以由使用者提供,没有人能保证,析构函数加拷贝构造函数的组合和赋值操作符具有相同的含义。所以产生一个临时对象来存储运算结果是非常有必要的。
一般而言 初始化操作总比拷贝赋值操作更有效率。
a + b
这种情况没有目标对象,也会产生一个临时对象,以放置运算后的结果:
string s("hello"), t("world"), u("!");
//1st
string v;
v = s + t + u;
//2nd
printf("%s\n", s + t);
无论哪种版本,都会产生一个临时对象。与s+t
关联。
临时对象的生命周期
- 临时对象被摧毁应该是对完整表达式求值过程的最后一个步骤。该完整表达式造成临时对象的产生:
任何一个子表达式所产生的任何一个临时对象,都应该在完整表达式被求值完成后,才可以销毁。
但是如果出现:if( s + t || u + v )
这样的表达式,由于短路求值,u + v
只有在s + t
被评估为false时才会开始评估,所以如果前者为true,后者的临时对象不就应该销毁吗?因此,我们只希望在临时对象产生出来的情况下,才去销毁它。 临时对象是根据程序运行期语意,有条件的产生。
临时对象的生命规则例外:
①当表达式用于初始化一个 object
凡是持有表达式执行结果的临时性对象,应该存留到object初始化操作完成为止。
②当一个临时性对象被一个 reference 绑定
绑定到引用的临时性对象将一直存留到该引用的生命结束,或者直到临时对象的生命结束。