例如有如下一段C++代码:
#include <iostream>
#include <conio.h>
using namespace std;
class A
{
public:
int m;
A();
A(int i);
};
A::A()
{
cout<<"A()"<<endl;
}
A::A(int i)
{
m = i;
cout<<"A(int)"<<endl;
}
void main()
{
clrscr();
int i =5;
A a; //注意这里
getch();
}
编译后能够正确显示“A()”,但如果我们把倒数第三行改为A a()呢?
我们都知道,如果采用A *a = new A()动态分配,会默认执行构造函数A::A()的内容,所以会显示“A()”,但如果改为A a()呢?上述代码编译也能通过,但如果语句中有试图访问A的成员变量或函数时候,编译器会报错,报告并没有这个成员函数或变量。似乎并没有给a在栈上分配空间,但似乎我们又并不确定,接下来我们通过代码的汇编语句来揭示我们的猜想。我们分别对A a;和 A a();的情况做反汇编。
编译器方便起见我用BCC 3.1,其他编译器类似。反汇编后,我们提取关键部分:
(1)、 A a情况下的:
push bp
mov bp,sp ;给帧指针赋值
sub sp,2 ;在栈上分配2个字节空间(后面的局部变量a的大小,本质就是一个int m)
;
; {
; clrscr();
;
call near ptr _clrscr
;
; A a;
;
lea ax,word ptr [bp-2] ;把刚分配的2个字节的Class A的首地址赋给AX
push ax ;寄存器AX的值入栈(),防止被下面函数修改
call near ptr @A@$bctr$qv ;调用构造函数A()
pop cx ;出栈保存到CX,因为AX通常用来做函数返回值,所以这里用CX
;
; getch();
;
call near ptr _getch
;
; }
;
mov sp,bp ;恢复调用者的栈指针
pop bp ;恢复调用者的帧指针
ret ;栈指针和帧指针恢复完毕,该函数(main)返回
(2)、 A a()情况下的:
push bp
mov bp,sp
;
; {
; clrscr();
;
call near ptr _clrscr
;
; A a();
; getch();
;
call near ptr _getch
;
; }
;
pop bp
ret
很明显,并没有对A a()做任何操作,也没有在堆栈上分配一个属于a的空间,更谈不上调用构造函数,编译器只是简单的把该定义给忽略。
至此,我们不再怀着猜测的预期从幕后回到台前,一切都已经了然于胸,印证了我们先前的猜想。另外,A a()后调用A的成员时候编译器报错的问题也能够很好解释了。
结论就是:ClassA A()什么都没做。 (此时无数鸡蛋一起向我扔来,闪!)
注:上述汇编代码由BCC3.1生成,BCC 3.1是一个16位的编译器,在处理某些类型的长度上(例如int,指针)和32位编译器有出入。
2006年12月30日 #
// use an object to represent a resource ("resource acquisition is initialization")
class File_handle { // belongs in some support library
FILE* p;
public:
File_handle(const char* pp, const char* r)
{ p = fopen(pp,r); if (p==0) throw Cannot_open(pp); }
File_handle(const string& s, const char* r)
{ p = fopen(s.c_str(),r); if (p==0) throw Cannot_open(pp); }
~File_handle() { fclose(p); } // destructor
// copy operations and access functions
};
void f(string s)
{
File_handle file(s, "r");
// use file
}
这段代码是一个用RAII规则的很好的简例。简单说,RAII是一种C++中关于资源生命周期管理的一种编程范式,RAII提倡把资源作为类的一部分,并且在构造函数中分配资源,析构函数中释放资源。看到这里,新手可能会说,我能够确保fopen成功后,在必要时候fclose掉。但,您真的能够100%确保您头脑能在纷繁复杂的代码中或者反复修改中保持100%的清新吗?If yes,恭喜您,您将是该神话的缔造者,被封为KFC(King From Cpper),并被全世界CPPER膜拜。 :) 而用RAII后,您不必老是担心还有什么资源没有被释放而忧心忡忡,封装的资源会在对象脱离代码SCOPE之后自动释放,因为对象脱离SCOPE之后,会先调用析构函数(释放资源),然后从STACK中被CLEAR。您或许还要问,你说的情况,只有静态分配时候有效,很多时候资源可能需要动态分配,如果是动态分配又该怎么办?总要delete掉吧,还是会有可能忘掉呀,并且似乎RAII并没有什么好处带给我呀。恩,的确没错,所以下面我还可以凑一点字数继续讲。
有时候,资源的分配与回收的配对并不是表面上看起来那么简单和容易。举个简单例子,对以上CLASS代码我们作动态分配:
void f()
{
File_handle *fhp = new File_handle("file.txt","r");
// ... use file
// forget delete fhp, cause memory leak, result maybe uncertain
}
我们先不提如何解决,再接着看另外一种情况,假设有如下代码:
ClassA *a = new ClassA();
ClassB *b = new ClassB();
// a,b do something here
delete a;
delete b;
如果恰好在第2行出错了该怎么办,那a的资源何时释放?也许按耐不住内心激动的您又要问,这里我们可以捕捉异常并进行处理呀,类似于:
ClassA *a = NULL;
ClassB *b = NULL;
try()
{
a = new ClassA();
b = new ClassB();
//do something
delete a;
delete b;
}
catch(...)
{
delete a; //Even if a wasn't allocated, it can be still delete.
delete b;
}
很好,这样写也没错,但我们有更好的方案:
C++提供的智能指针auto_ptr可以很好解决上述情况。
auto_ptr a(new ClassA);
auto_ptr b(new ClassB);
//a,b do something
这样是不是更加简洁了呢?
写到这里,我简单介绍RAII的使命终于完成,当然您充满好奇的您可能还有很多延伸的疑问,例如:RAII和JAVA、.NET提供的GC有什么联系、相比有何区别、有何优劣?如果我的类中还有共享对象应该如何处理?如何编写异常安全的代码?下面的几篇出自高人的文章可以让您继续探索,但小弟我得先睡觉去了 :)
RAII和垃圾收集(上)
http://dev.csdn.net/article/24/24495.shtm
RAII和垃圾收集(下)
http://dev.csdn.net/article/24/24496.shtm
如何编写异常安全的C++代码
http://www.uml.org.cn/c%2B%2B/200604245.htm

