C++的优秀特性3:构造函数和析构函数
(转载请注明原创于潘多拉盒子)
构造函数和析构函数是C++中再熟悉不过的概念了,几乎每个了解一点C++的人都知道这两个概念是什么意思。一个对象的全部生命期中构造函数和析构函数执行的时机如下:
1. 为对象分配空间。这个空间可能是在栈上(函数内的局部变量),可能是在数据区(静态变量、全局变量),也可能分配在堆上(new出来的变量)。
2. 执行对象对应的构造函数。如果继承有父类或有成员对象,则先执行父类的构造函数和成员对象的构造函数。
3. 对象生命期内的各种成员函数调用。
4. 执行析构函数。和#2中构造的过程相反,先执行自身的析构函数,再执行父类和成员对象的析构函数。
5. 释放为对象分配的空间。这个过程与#1相反。
对象的整个生命期中构造函数和析构函数的执行是具有非常精确的镜像对称性,也就是说,构造过程(包括构造函数和继承类、成员对象的构造函数)中的各个构造操作和析构过程(包括构造函数、成员对象、成员对象的析构函数)完全对称。这个过程在《C++对象模型》中有详细的说明。
对于#1,对象占有的空间,包括对象直接占有的空间,其大小也就是sizeof操作符给出的对象大小。C++要求对象的大小在编译时确定,因此该大小是在对象定义时确定下来的。
考虑如下的一段代码:
std::vector<int> u(10); std::vector<int> v(20); memcpy(&v, &u, sizeof(u));
首先第一个问题是:sizeof(u)和sizeof(v) 哪个更大?我见过有一部分C++程序员认为sizeof(v)会更大,原因是v里面存着20个int型变量,而v中存着10个int型变量。实际上,u和v的sizeof运算结果是一样大的!具体的大小可能跟编译器有关,但在一种编译器下,它们的大小是完全相等的!因为它们的类型是相同的!
由此也就知道了第3行代码是有问题的,将两个vector的对象按地址和大小拷贝,虽然本身可以编译并运行,甚至编译器也不会报告警告!但是会导致v持有u在构造函数中从堆上分配的空间,一方面导致double free,另一方面会导致v原先持有的堆上分配的空间泄漏!
有趣的是,可以利用构造函数和析构函数执行的时机,去利用编译器产生一些非常智能的代码。这里有一些典型的应用,比如std::auto_ptr:
std::auto_ptr<Widget> widget = new Widget(); // 像指针一样使用widget widget->foo(); (*widget).bar(); if (widget->invalid()) { return false; // 会在析构widget对象时自动释放new出来的Widget } // 即使有exception抛出,widget也会自动析构 return true; // 尤其是有多个return的时候,威力更大。
我们经常在实际中使用线程锁,在每个return前面都释放锁实在是一件麻烦的事情,而且也不是异常安全的,如果利用构造函数和析构函数,则能比较好的解决这个问题:
#include <pthread.h> class ScopedLock { public: ScopedLock(pthread_mutex_t& lock) : _lock(&lock) { pthread_mutex_lock(_lock); } ~ScopedLock() { pthread_mutex_unlock(_lock); } private: pthread_lock_t* _lock; }; // 用例 int needForSafety() { static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; ScopedLock scopedLock(&lock); // 加锁 // throw an exception:自动解锁 // return 1: 自动解锁 // return 2: 自动解锁 // …… }
构造函数和析构函数的这种特性,在实际中是很有用的。