C++的营养
莫华枫
动物都会摄取食物,吸收其中的营养,用于自身生长和活动。然而,并非食物中所有的物质都能为动物所吸收。那些无法消化的物质,通过消化道的另一头(某些动 物消化道只有一头)排出体外。不过,一种动物无法消化的排泄物,是另一种动物(生物)的食物,后者可以从中摄取所需的营养。
一门编程语言,对于程序员而言,如同食物那样,包含着所需的养分。当然也包含着无法消化的东西。不同的是,随着程序员不断成长,会逐步消化过去无法消化的那些东西。
C++可以看作一种成分复杂的食物,对于多数程序员而言,是无法完全消化的。正因为如此,很多程序员认为C++太难以消化,不应该去吃它。但是,C++的 营养不可谓不丰富,就此舍弃,而不加利用,则是莫大的罪过。好在食物可以通过加工,变得易于吸收,比如说发酵。鉴于程序员们的消化能力的差异,也为了让C ++的营养能够造福他人,我就暂且扮演一回酵母菌,把C++的某些营养单独提取出来,并加以分解,让那些消化能力不太强的程序员也能享受它的美味。:)
(为了让这些营养便于消化,我将会用C#做一些案例。选择C#的原因很简单,因为我熟悉。:))
RAII
RAII,好古怪的营养啊!它的全称应该是“Resource Acquire Is Initial”。这是C++创始人Bjarne Stroustrup发明的词汇,比较令人费解。说起来,RAII的含义倒也不算复杂。用白话说就是:在类的构造函数中分配资源,在析构函数中释放资源。 这样,当一个对象创建的时候,构造函数会自动地被调用;而当这个对象被释放的时候,析构函数也会被自动调用。于是乎,一个对象的生命期结束后将会不再占用 资源,资源的使用是安全可靠的。下面便是在C++中实现RAII的典型代码:
class
file
{
public :
file( string const & name){
m_fileHandle = open_file(name.cstr());
}
~ file(){
close_file(m_fileHandle);
}
...
private :
handlem_fileHandle;
}
{
public :
file( string const & name){
m_fileHandle = open_file(name.cstr());
}
~ file(){
close_file(m_fileHandle);
}
...
private :
handlem_fileHandle;
}





但是,在如下的代码中,资源不是安全的,尽管我们实现了RAII:











现在,在fun3(),资源是安全的,但却不是异常安全的。因为一旦函数中抛出异常,那么delete pfile;这句代码将没有机会被执行。C++领域的诸位大牛们告诫我们:如果想要在没有GC的情况下确保资源安全和异常安全,那么请使用智能指针:


















对于内置了GC的语言,资源管理相对简单。不过,事情并非总是这样。下面的C#代码摘自MSDN Library的C#编程指南,我略微改造了一下:








所以,只要涉及了内存以外的资源,应当尽快释放。(当然,如果内存能够尽快释放,就更好了)。对于上述CodeWithoutCleanup()函数,应当在最后调用file对象上的Close()函数,以便释放文件:




































一个有效的RAII应当包含两个部分:构造/析构函数的资源获取/释放和确定性的析构函数调用。前者在C#中不成问题,C#有构造函数和析构函数。不过, C#的构造函数和析构函数是不能用于RAII的,原因一会儿会看到。正确的做法是让一个类实现IDisposable接口,在IDisposable:: Dispose()函数中释放资源:

























但是,还有一个问题是using无法解决的,就是如何维持类的成员函数的RAII。我们希望一个类的成员对象在该类实例创建的时候获取资源,而在其销毁的时候释放资源:






















至此,RAII的来龙去脉已经说清楚了,在C#里也能从中汲取到充足的养分。但是,这还不是RAII的全部营养,RAII还有更多的扩展用途。在 《Imperfect C++》一书中,Matthew Wilson展示了RAII的一种非常重要的应用。为了不落个鹦鹉学舌的名声,这里我给出一个真实遇到的案例,非常简单:我写的程序需要响应一个Grid 控件的CellTextChange事件,执行一些运算。在响应这个事件(执行运算)的过程中,不能再响应同一个事件,直到处理结束。为此,我设置了一个 标志,用来控制事件响应:





















































这个BoolScope可以在将来继续使用,分摊下来的开发成本几乎是0。更进一步,可以开发一个通用的Scope模板,用于所有类型,就像《Imperfect C++》里的那样。
下面,让我们把战场转移到C#,看看C#是如何实现域守卫的。考虑到C#(.net)的对象模型的特点,我们先实现引用类型的域守卫,然后再来看看如何对付值类型。其原因,一会儿会看到。
我曾经需要向一个grid中填入数据,但是填入的过程中,控件不断的刷新,造成闪烁,也影响性能,除非把控件上的AutoDraw属性设为false。为此,我做了一个域守卫类,在填写操作之前关上AutoDraw,完成或异常抛出时再打开:























