1、引言:
这是对TC++PL 14.4的读书笔记,重点是资源的使用,以及异常机制下的资源正确释放。
2、笔记内容
对于内存、文件、I/O等,C++里的使用原则是“申请<-->释放”成对应用。例如使用操作符new申请内存,在使用结束后要使用操作符delete释放。当程序规模较小,并且new之后对指针使用传递的次数不多时,一般我们都能记得对申请得到的内存使用delete释放,但是当程序变得巨大,或者我们对指针的使用很复杂时,产生了A模块中申请的内存需要在B模块中释放这种情况,很可能释放内存这件事最后就被忘记了(当然,最好避免这种A模块中申请,B模块中释放的情况)。对指针的处理不当对需要连续运行的软件是个恐怖的消息,为了尽可能消除这种风险,Bjarne Stroustrup在TC++PL中给出了“resource acquisition is initialization” 的解决思路:
- 将资源包装成类
- 将申请和释放动作放在该资源对象的构造和析构函数中
- 将申请得到的资源指针设置为私有,并限制外界企图获得实际资源指针的可能
- 以自动变量的方式使用该对象
如此,当对象被定义时,申请内存,当对象生命期结束时,析构被自动调用回收内存。那么,按照“resource acquisition is initialization”,可以给出一个buffer_of_double类的例子:
typedef int iterator;
class buffer_of_double{
double* pbuf;
int n;
public:
buffer_of_double (int sz){
n = sz;
pbuf = new double[n*sizeof(double)];
// other operations
}
~buffer_of_double (){
// other operations
delete pbuf;
}
void setvalue(iterator i, double val);
double getvalue(iterator i);
friend void memset_buffer_of_double(buffer_of_double & dst, double val, int n);
friend void memcpy_buffer_of_double(buffer_of_double & dst, const double* src, int n);
friend void memcpy_buffer_of_double(double* dst, const buffer_of_double & src, int n);
};
在buffer_of_double中,完全阻止了外部直接或间接获得pbuf的可能,那么在一个matrix类中,可以这样使用buffer_of_double:
class matrix{
buffer_of_double data;
int nrows;
int ncols;
public:
matrix(int r, int c):data(r*c){
nrows = r;
ncols = c;
// others
}
~matrix();
// others
};
这样做,是以牺牲灵活性为代价,提高程序的健壮性。
这里牵出了另外一个问题,如果在buffer_of_double对象创建后,程序出现了问题,导致buffer_of_double析构无法被调用,这该怎么办?这时就需要借助C++提供的exception-handling机制了,当然exception-handling也只是降低了程序受可预见问题影响的风险,并不是万能的机制。当某个异常,即前面提到的“可预见问题”,被抛出时,exception-handling机制产生一种所谓stack unwinding(栈展开)的行为,即在当前try块中从try开始到抛出异常的throw之间产生的所有自动变量,会被按定义倒叙调用析构进行销毁,这实际是利用栈完成的一种回滚操作,因为try块的意义就是告诉编译器:这一段代码是需要监督的,请为之插入相关的回滚操作,以使其在遇到异常时,程序现场可以恢复到try块之前的状态。stack unwinding如果和“resource acquisition is initialization”相配合,那么当自动变量为资源类对象时,该对象的析构被调用,从而释放了定义时申请的资源。例如在下面的main()中,当throw抛出时,在跳转至catch前,以下操作依次被执行,(列出的过程缺少细节,或缺少某些执行步骤,但已列出的步骤之间按下述顺序进行):
- b从栈中弹出
- m的析构被调用,m.nrows和m.ncols从栈中被弹出
- m.data的析构被调用,m.data.pbuf指向内存被释放,m.data.pbuf和m.data.n从栈中被弹出
- a从栈中弹出
int main()
{
try{
int a = 0;
matrix m = matrix(3,3);
int b;
if (异常为真) throw user_e(“a exception”);
}
catch(user_e e)
{
// handling
}
return 0;
}
如果try与throw被应用在matrix的构造函数中会发生什么事情呢?这没有什么特别的,就向下边的matrix构造函数一样:
matrix(int r, int c):data(r*c){
try{
nrows = r;
ncols = c;
if (异常为真) throw user_e("exception");
}
catch(user_e e){
// handling
}
}
异常处理后matrix对象仍然被创建,因为C++对一个对象被创建与否的评判标准为,构造函数有没有被执行完毕,因为构造函数执行完意味着对象的成员变量均已被创建成功,对象已近存在于内存中了。
那如果在初始化成员变量的时候,出现问题怎么办,data申请的内存能得到释放吗?答案是能,只要我们将初始化列表也一同包括进try块中,则当遇到一个异常并将其抛出时,从当前要产生的对象的成员变量到构造函数中throw之前的自动变量均会被调用析构销毁。如下边的例子,在触发异常后,nrows、ncols和data均被stack unwinding,即使有成员变量并没有出现在初始化列表中,但它们的析构仍然会被调用。然后程序跳至catch,matrix的构造函数没有完成就被中断,成员变量的被stack unwinding销毁,matrix对象并没有创建成功。
matrix(int r, int c)
try
:data(r*c){
nrows = r;
ncols = c;
if (异常为真) throw user_e("matrix exception");
}
catch(user_e e){
// handling
std::cout << e.p <
除了我们自己实现“resource acquisition is initialization”以及其与exception-handling的配合使用以保证资源被正确释放外,STL中模板类auto_ptr是一种现成的resource acquisition is initialization实现。auto_ptr一如它的名字,自动指针,暗指其具有栈对象的特点,即在auto_ptr作用域结束时自动对其所拥有的指针进行delete,在遇到throw,发生stack unwinding时也会有这种行为。一般都将auto_ptr称为一种智能指针,但它并不足够聪明,使用时要注意一下事宜:
- 定义方式:std::auto_ptr
p(new int(1)); 该语句表示申请一块内存,存储值为1的int型变量,该内存的指针被auto_ptr变量p占有,对p的使用同int * p = new int(1)得到的p没有任何区别。注意std::auto_ptr p = new int(1)为错误语法。 - 对指针的占有关系不能共享,当有赋值发生时,源auto_ptr变量对指针的占有关系解除;当对新auto_ptr变量使用另一auto_ptr变量初始化时:std::auto_ptr
p1 = p,源auto_ptr变量对指针的占有关系解除。这样做是为了避免多次对指针delete的发生。 - 不能在数组上使用auto_ptr,即std::auto_ptr
p(new int[10])是错误的。 - 不能在STL标准容器和标准算法中使用auto_ptr
- 在对指针的占有关系被解除后,该auto_ptr变量无法再对指针指向的变量或内存做任何操作。
- const类型auto_ptr在初始化后完全绑定,无法再对其他auto_ptr赋值传递占有关系。
3、结语
记得在哪看到过,Bjarne Stroustrup表示,虽然C++提供了程序员亲自控制资源何时回收的权利,但对于大型软件来说,最好使用垃圾自动回收器。