longshanks
C++博客
::
首页
::
联系
::
聚合
::
管理
11 Posts :: 0 Stories :: 177 Comments :: 0 Trackbacks
常用链接
我的随笔
我的评论
我参与的随笔
留言簿
(1)
给我留言
查看公开留言
查看私人留言
我参与的团队
随笔档案
2008年2月 (3)
2008年1月 (2)
2007年12月 (2)
2007年11月 (3)
2007年5月 (1)
搜索
最新评论
1. order valium online
We cannot destroy kindred: Our chains stretch a little sometimes, but they never break.
--order valium online
2. buy phentermine
Keep your broken arm inside your sleeve.
--buy phentermine
3. vardenafil
Whoso would be a man must be a nonconformist.
--vardenafil
4. cheap xanax
Prove all things; hold fast that which is good.
--cheap xanax
5. order vicodin abrin irritable
How we treasure (and admire) the people who acknowledge us!
--order vicodin abrin irritable
阅读排行榜
1. 两类程序员(1910)
2. OOP的黄昏(1550)
3. C++的营养——swap手法(1489)
4. mfc大幅更新原因的推测(1453)
5. C++之歌——噢,我亲爱的++(1439)
评论排行榜
1. mfc大幅更新原因的推测(117)
2. 两类程序员(13)
3. OOP的黄昏(10)
4. C++之歌——噢,我亲爱的++(9)
5. 业务逻辑的强类型化(6)
C++的营养
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
:
handle m_fileHandle;
}
很典型的“在构造函数里获取,在析构函数里释放”。如果我写下代码:
void
fun1()
...
{
file myfile(
"
my.txt
"
);
...
//
操作文件
}
//
此处销毁对象,调用析构函数,释放资源
当函数结束时,局部对象myfile的生命周期也结束了,析构函数便会被调用,资源会得到释放。而且,如果函数中的代码抛出异常,那么析构函数也会被调用,资源同样会得到释放。所以,在RAII下,不仅仅资源安全,也是异常安全的。
但是,在如下的代码中,资源不是安全的,尽管我们实现了RAII:
void
fun2()
...
{
file pfile
=
new
file(
"
my.txt
"
);
...
//
操作文件
}
因为我们在堆上创建了一个对象(通过new),但是却没有释放它。我们必须运用delete操作符显式地加以释放:
void
fun3()
...
{
file pfile
=
new
file(
"
my.txt
"
);
...
//
操作文件
delete pfile;
}
否则,非但对象中的资源得不到释放,连对象本身的内存也得不到回收。(将来,C++的标准中将会引入GC(垃圾收集),但正如下面分析的那样,GC依然无法确保资源的安全)。
现在,在fun3(),资源是安全的,但却不是异常安全的。因为一旦函数中抛出异常,那么delete pfile;这句代码将没有机会被执行。C++领域的诸位大牛们告诫我们:如果想要在没有GC的情况下确保资源安全和异常安全,那么请使用智能指针:
void
fun4()
...
{
shared_ptr
<
file
>
spfile(
new
file(
"
my.txt
"
));
...
//
操作文件
}
//
此处,spfile结束生命周期的时候,会释放(delete)对象
那么,智能指针又是怎么做到的呢?下面的代码告诉你其中的把戏(关于智能指针的更进一步的内容,请参考std::auto_ptr,boost或tr1的智能指针):
template
<
typename T
>
class
smart_ptr
...
{
public
:
smart_ptr(T
*
p):m_ptr(p)
...
{}
~
smart_ptr()
...
{ delete m_ptr; }
...
private
:
T
*
m_ptr;
}
没错,还是RAII。也就是说,智能指针通过RAII来确保内存资源的安全,也间接地使得对象上的RAII得到实施。不过,这里的RAII并不是十分严 格:对象(所占的内存也是资源)的创建(资源获取)是在构造函数之外进行的。广义上,我们也把它划归RAII范畴。但是,Matthew Wilson在《Imperfect C++》一书中,将其独立出来,称其为RRID(Resource Release Is Destruction)。RRID的实施需要在类的开发者和使用者之间建立契约,采用相同的方法获取和释放资源。比如,如果在shared_ptr构造 时使用malloc(),便会出现问题,因为shared_ptr是通过delete释放对象的。
对于内置了GC的语言,资源管理相对简单。不过,事情并非总是这样。下面的C#代码摘自MSDN Library的C#编程指南,我略微改造了一下:
static
void
CodeWithoutCleanup()
...
{
System.IO.FileStream file
=
null
;
System.IO.FileInfo fileInfo
=
new
System.IO.FileInfo(
"
C:\file.txt
"
);
file
=
fileInfo.OpenWrite();
file.WriteByte(
0xF
);
}
那么资源会不会泄漏呢?这取决于对象的实现。如果通过OpenWrite()获得的FileStream对象,在析构函数中执行了文件的释放操作,那么资 源最终不会泄露。因为GC最终在执行GC操作的时候,会调用Finalize()函数(C#类的析构函数会隐式地转换成Finalize()函数的重 载)。这是由于C#使用了引用语义(严格地讲,是对引用类型使用引用语义),一个对象实际上不是对象本身,而是对象的引用。如同C++中的那样,引用在离 开作用域时,是不会释放对象的。否则,便无法将一个对象直接传递到函数之外。在这种情况下,如果没有显式地调用Close()之类的操作,资源将不会得到 立刻释放。但是像文件、锁、数据库链接之类属于重要或稀缺的资源,如果等到GC执行回收,会造成资源不足。更有甚者,会造成代码执行上的问题。我曾经遇到 过这样一件事:我执行了一个sql操作,获得一个结果集,然后执行下一个sql,结果无法执行。这是因为我使用的SQL Server 2000不允许在一个数据连接上同时打开两个结果集(很多数据库引擎都是这样)。第一个结果集用完后没有立刻释放,而GC操作则尚未启动,于是便造成在一 个未关闭结果集的数据连接上无法执行新的sql的问题。
所以,只要涉及了内存以外的资源,应当尽快释放。(当然,如果内存能够尽快释放,就更好了)。对于上述CodeWithoutCleanup()函数,应当在最后调用file对象上的Close()函数,以便释放文件:
static
void
CodeWithoutCleanup()
...
{
System.IO.FileStream file
=
null
;
System.IO.FileInfo fileInfo
=
new
System.IO.FileInfo(
"
C:\file.txt
"
);
file
=
fileInfo.OpenWrite();
file.WriteByte(
0xF
);
file.Close();
}
现在,这个函数是
严格资源安全
的,但却不是
严格异常安全
的。如果在文件的操作中抛出异常,Close()成员将得不到调用。此时,文件也将无法及时关闭,直到GC完成。为此,需要对异常作出处理:
static
void
CodeWithCleanup()
...
{
System.IO.FileStream file
=
null
;
System.IO.FileInfo fileInfo
=
null
;
try
...
{
fileInfo
=
new
System.IO.FileInfo(
"
C:\file.txt
"
);
file
=
fileInfo.OpenWrite();
file.WriteByte(
0xF
);
}
catch
(System.Exception e)
...
{
System.Console.WriteLine(e.Message);
}
finally
...
{
if
(file
!=
null
)
...
{
file.Close();
}
}
}
try-catch-finally是处理这种情况的标准语句。但是,相比前面的C++代码fun1()和fun4()繁琐很多。这都是没有RAII的后果啊。下面,我们就来看看,如何在C#整出RAII来。
一个有效的RAII应当包含两个部分:构造/析构函数的资源获取/释放和确定性的析构函数调用。前者在C#中不成问题,C#有构造函数和析构函数。不过, C#的构造函数和析构函数是不能用于RAII的,原因一会儿会看到。正确的做法是让一个类实现IDisposable接口,在IDisposable:: Dispose()函数中释放资源:
class
RAIIFile : IDisposable
...
{
public
RAIIFile(
string
fn)
...
{
System.IO.FileInfo fileInfo
=
new
System.IO.FileInfo(fn);
file
=
fileInfo.OpenWrite();
}
public
void
Dispose()
...
{
file.Close();
}
private
System.IO.FileStream file
=
null
;
}
下一步,需要确保文件在退出作用域,或发生异常时被确定性地释放。这项工作需要通过C#的using语句实现:
static
void
CodeWithRAII()
...
{
using
(RAIIFile file
=
new
RAIIFile(
"
C:\file.txt
"
))
...
{
...
//
操作文件
}
//
文件释放
}
一旦离开using的作用域,file.Dispose()将被调用,文件便会得到释放,即便抛出异常,亦是如此。相比CodeWithCleanup ()中那坨杂乱繁复的代码,CodeWithRAII()简直可以算作赏心悦目。更重要的是,代码的简洁和规则将会大幅减少出错可能性。值得注意的是 using语句只能作用于实现IDisposable接口的类,即便实现了析构函数也不行。所以对于需要得到RAII的类,必须实现 IDisposable。通常,凡是涉及到资源的类,都应该实现这个接口,便于日后使用。实际上,.net库中的很多与非内存资源有关的类,都实现了 IDisposable,都可以利用using直接实现RAII。
但是,还有一个问题是using无法解决的,就是如何维持类的成员函数的RAII。我们希望一个类的成员对象在该类实例创建的时候获取资源,而在其销毁的时候释放资源:
class
X
...
{
public
:
X():m_file(
"
c:\file.txt
"
)
...
{}
private
:
File m_file;
//
在X的实例析构时调用File::~File(),释放资源。
}
但是在C#中无法实现。由于uing中实例化的对象在离开using域的时候便释放了,无法在构造函数中使用:
class
X
...
{
public
X()
...
{
using
(m_file
=
new
RAIIFile(
"
C:\file.txt
"
))
...
{
}
//
此处m_file便释放了,此后m_file便指向无效资源
}
pravite RAIIFile m_file;
}
对于成员对象的RAII只能通过在析构函数或Dispose()中手工地释放。我还没有想出更好的办法来。
至此,RAII的来龙去脉已经说清楚了,在C#里也能从中汲取到充足的养分。但是,这还不是RAII的全部营养,RAII还有更多的扩展用途。在 《Imperfect C++》一书中,Matthew Wilson展示了RAII的一种非常重要的应用。为了不落个鹦鹉学舌的名声,这里我给出一个真实遇到的案例,非常简单:我写的程序需要响应一个Grid 控件的CellTextChange事件,执行一些运算。在响应这个事件(执行运算)的过程中,不能再响应同一个事件,直到处理结束。为此,我设置了一个 标志,用来控制事件响应:
class
MyForm
...
{
public
:
MyForm():is_cacul(
false
)
...
{}
...
void
OnCellTextChange(Cell
&
cell)
...
{
if
(is_cacul)
return
;
is_cacul
=
true
;
...
//
执行计算任务
is_cacul
=
false
;
}
private
:
bool
is_cacul;
}
;
但是,这里的代码不是异常安全的。如果在执行计算的过程中抛出异常,那么is_cacul标志将永远是true。此后,即便是正常的 CellTextChange也无法得到正确地响应。同前面遇到的资源问题一样,传统上我们不得不求助于try-catch语句。但是如果我们运用 RAII,则可以使得代码简化到不能简化,安全到不能再安全。我首先做了一个类:
class
BoolScope
...
{
public
:
BoolScope(
bool
&
val,
bool
newVal)
:m_val(val), m_old(val)
...
{
m_val
=
newVal;
}
~
BoolScope()
...
{
m_val
=
m_old;
}
private
:
bool
&
m_val;
bool
m_old;
}
;
这个类的作用是所谓“域守卫(scoping)”,构造函数接受两个参数:第一个是一个bool对象的引用,在构造函数中保存在m_val成员里;第二个 是新的值,将被赋予传入的那个bool对象。而该对象的原有值,则保存在m_old成员中。析构函数则将m_old的值返还给m_val,也就是那个 bool对象。有了这个类之后,便可以很优雅地获得异常安全:
class
MyForm
...
{
public
:
MyForm():is_cacul(
false
)
...
{}
...
void
OnCellTextChange(Cell
&
cell)
...
{
if
(is_cacul)
return
;
BoolScope bs_(is_cacul,
true
);
...
//
执行计算任务
}
private
:
bool
is_cacul;
}
;
好啦,任务完成。在bs_创建的时候,is_cacul的值被替换成true,它的旧值保存在bs_对象中。当OnCellTextChange()返回 时,bs_对象会被自动析构,析构函数会自动把保存起来的原值重新赋给is_cacul。一切又都回到原先的样子。同样,如果异常抛出,is_cacul 的值也会得到恢复。
这个BoolScope可以在将来继续使用,分摊下来的开发成本几乎是0。更进一步,可以开发一个通用的Scope模板,用于所有类型,就像《Imperfect C++》里的那样。
下面,让我们把战场转移到C#,看看C#是如何实现域守卫的。考虑到C#(.net)的对象模型的特点,我们先实现引用类型的域守卫,然后再来看看如何对付值类型。其原因,一会儿会看到。
我曾经需要向一个grid中填入数据,但是填入的过程中,控件不断的刷新,造成闪烁,也影响性能,除非把控件上的AutoDraw属性设为false。为此,我做了一个域守卫类,在填写操作之前关上AutoDraw,完成或异常抛出时再打开:
class
DrawScope : IDisposable
...
{
public
DrawScope(Grid g,
bool
val)
...
{
m_grid
=
g;
m_old
=
g
->
AutoDraw;
m_grid
->
AutoDraw
=
val;
}
public
void
Dispose()
...
{
g
->
AutoDraw
=
m_old;
}
private
Grid m_grid;
private
bool
m_old;
}
;
于是,我便可以如下优雅地处理AutoDraw属性设置问题:
static
void
LoadData(Grid g)
...
{
using
(DrawScope ds
=
new
DrawScope(g,
false
))
...
{
...
//
执行数据装载
}
}
现在,我们回过头,来实现值类型的域守卫。案例还是采用前面的CellTextChange事件。当我试图着手对那个is_cacul执行域守卫时,遇到了不小的麻烦。起初,我写下了这样的代码:
class
BoolScope
...
{
private
???
m_val;
//
此处用什么类型?
private
bool
m_old;
}
;
m_val应当是一个指向一个对象的引用,C#是没有C++那些指针和引用的。在C#中,引用类型定义的对象实际上是一个指向对象的引用;而值类型定义的 对象实际上是一个对象,或者说“栈对象”,但却没有一种指向值类型的引用。(关于这种对象模型的优劣,后面的“题外话”小节有一些探讨)。我尝试着采用两 种办法,一种不成功,而另一种成功了。
C#(.net)有一种box机制,可以将一个值对象打包,放到堆中创建。这样,或许可以把一个值对象编程引用对象,构成C#可以引用的东西:
class
BoolScope : IDisposable
...
{
public
BoolScope(
object
val,
bool
newVal)
...
{
m_val
=
val;
//
#1
m_old
=
(
bool
)val;
(
bool
)m_val
=
newVal;
//
#2
}
public
void
Dispose()
...
{
(
bool
)m_val
=
m_old;
//
#3
}
private
object
m_val;
private
bool
m_old;
}
使用时,应当采用如下形式:
class
MyForm
...
{
public
MyForm()
...
{
is_cacul
=
new
bool
(
false
);
//
boxing
}
...
void
OnCellTextChange(Cell
&
cell)
...
{
if
(is_cacul)
return
;
using
(BoolScope bs
=
new
BoolScope(is_cacul,
true
))
...
{
...
//
执行计算任务
}
}
private
object
is_cacul;
}
;
很可惜,此路不通。因为在代码#1的地方,并未执行引用语义,而执行了值语义。也就是说,没有把val(它是个引用)的值赋给m_val(也是个引用), 而是为m_val做了个副本。以至于在代码#2和#3处无法将newVal和m_old赋予val(也就是is_cacul)。或许C#的设计者有无数理 由说明这种设