`
helloyesyes
  • 浏览: 1304019 次
  • 性别: Icon_minigender_2
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论

C++的营养——RAII

阅读更多

C++的营养

莫华枫
动物都会摄取食物,吸收其中的营养,用于自身生长和活动。然而,并非食物中所有的物质都能为动物所吸收。那些无法消化的物质,通过消化道的另一头(某些动 物消化道只有一头)排出体外。不过,一种动物无法消化的排泄物,是另一种动物(生物)的食物,后者可以从中摄取所需的营养。
一门编程语言,对于程序员而言,如同食物那样,包含着所需的养分。当然也包含着无法消化的东西。不同的是,随着程序员不断成长,会逐步消化过去无法消化的那些东西。
C++可以看作一种成分复杂的食物,对于多数程序员而言,是无法完全消化的。正因为如此,很多程序员认为C++太难以消化,不应该去吃它。但是,C++的 营养不可谓不丰富,就此舍弃,而不加利用,则是莫大的罪过。好在食物可以通过加工,变得易于吸收,比如说发酵。鉴于程序员们的消化能力的差异,也为了让C ++的营养能够造福他人,我就暂且扮演一回酵母菌,把C++的某些营养单独提取出来,并加以分解,让那些消化能力不太强的程序员也能享受它的美味。:)
(为了让这些营养便于消化,我将会用C#做一些案例。选择C#的原因很简单,因为我熟悉。:))

RAII

RAII,好古怪的营养啊!它的全称应该是“Resource Acquire Is Initial”。这是C++创始人Bjarne Stroustrup发明的词汇,比较令人费解。说起来,RAII的含义倒也不算复杂。用白话说就是:在类的构造函数中分配资源,在析构函数中释放资源。 这样,当一个对象创建的时候,构造函数会自动地被调用;而当这个对象被释放的时候,析构函数也会被自动调用。于是乎,一个对象的生命期结束后将会不再占用 资源,资源的使用是安全可靠的。
下面便是在C++中实现RAII的典型代码:
classfile
{
public:
file(
stringconst&name){
m_fileHandle
=open_file(name.cstr());
}
~file(){
close_file(m_fileHandle);
}
...
private:
handlem_fileHandle;
}
很典型的“在构造函数里获取,在析构函数里释放”。如果我写下代码:
voidfun1()...{
filemyfile(
"my.txt");
...
//操作文件
}
//此处销毁对象,调用析构函数,释放资源
当函数结束时,局部对象myfile的生命周期也结束了,析构函数便会被调用,资源会得到释放。而且,如果函数中的代码抛出异常,那么析构函数也会被调用,资源同样会得到释放。所以,在RAII下,不仅仅资源安全,也是异常安全的。
但是,在如下的代码中,资源不是安全的,尽管我们实现了RAII:
voidfun2()...{
filepfile
=newfile("my.txt");
...
//操作文件
}
因为我们在堆上创建了一个对象(通过new),但是却没有释放它。我们必须运用delete操作符显式地加以释放:
voidfun3()...{
filepfile
=newfile("my.txt");
...
//操作文件
deletepfile;
}
否则,非但对象中的资源得不到释放,连对象本身的内存也得不到回收。(将来,C++的标准中将会引入GC(垃圾收集),但正如下面分析的那样,GC依然无法确保资源的安全)。
现在,在fun3(),资源是安全的,但却不是异常安全的。因为一旦函数中抛出异常,那么delete pfile;这句代码将没有机会被执行。C++领域的诸位大牛们告诫我们:如果想要在没有GC的情况下确保资源安全和异常安全,那么请使用智能指针:
voidfun4()...{
shared_ptr
<file>spfile(newfile("my.txt"));
...
//操作文件
}
//此处,spfile结束生命周期的时候,会释放(delete)对象
那么,智能指针又是怎么做到的呢?下面的代码告诉你其中的把戏(关于智能指针的更进一步的内容,请参考std::auto_ptr,boost或tr1的智能指针):
template<typenameT>
classsmart_ptr
...{
public:
smart_ptr(T
*p):m_ptr(p)...{}
~smart_ptr()...{deletem_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#编程指南,我略微改造了一下:
staticvoidCodeWithoutCleanup()
...{
System.IO.FileStreamfile
=null;
System.IO.FileInfofileInfo
=newSystem.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()函数,以便释放文件:
staticvoidCodeWithoutCleanup()
...{
System.IO.FileStreamfile
=null;
System.IO.FileInfofileInfo
=newSystem.IO.FileInfo("C:\file.txt");
file
=fileInfo.OpenWrite();
file.WriteByte(
0xF);
file.Close();
}
现在,这个函数是严格资源安全的,但却不是严格异常安全的。如果在文件的操作中抛出异常,Close()成员将得不到调用。此时,文件也将无法及时关闭,直到GC完成。为此,需要对异常作出处理:
staticvoidCodeWithCleanup()
...{
System.IO.FileStreamfile
=null;
System.IO.FileInfofileInfo
=null;
try
...{
fileInfo
=newSystem.IO.FileInfo("C:\file.txt");
file
=fileInfo.OpenWrite();
file.WriteByte(
0xF);
}

catch(System.Exceptione)
...{
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()函数中释放资源:
classRAIIFile:IDisposable
...{
publicRAIIFile(stringfn)...{
System.IO.FileInfofileInfo
=newSystem.IO.FileInfo(fn);
file
=fileInfo.OpenWrite();
}


publicvoidDispose()...{
file.Close();
}


privateSystem.IO.FileStreamfile=null;
}
下一步,需要确保文件在退出作用域,或发生异常时被确定性地释放。这项工作需要通过C#的using语句实现:
staticvoidCodeWithRAII()
...{
using(RAIIFilefile=newRAIIFile("C:\file.txt"))
...{
...
//操作文件
}
//文件释放
}
一旦离开using的作用域,file.Dispose()将被调用,文件便会得到释放,即便抛出异常,亦是如此。相比CodeWithCleanup ()中那坨杂乱繁复的代码,CodeWithRAII()简直可以算作赏心悦目。更重要的是,代码的简洁和规则将会大幅减少出错可能性。值得注意的是 using语句只能作用于实现IDisposable接口的类,即便实现了析构函数也不行。所以对于需要得到RAII的类,必须实现 IDisposable。通常,凡是涉及到资源的类,都应该实现这个接口,便于日后使用。实际上,.net库中的很多与非内存资源有关的类,都实现了 IDisposable,都可以利用using直接实现RAII。
但是,还有一个问题是using无法解决的,就是如何维持类的成员函数的RAII。我们希望一个类的成员对象在该类实例创建的时候获取资源,而在其销毁的时候释放资源:
classX
...{
public:
X():m_file(
"c:\file.txt")...{}
private:
Filem_file;
//在X的实例析构时调用File::~File(),释放资源。
}
但是在C#中无法实现。由于uing中实例化的对象在离开using域的时候便释放了,无法在构造函数中使用:
classX
...{
publicX()...{
using(m_file=newRAIIFile("C:\file.txt"))
...{
}
//此处m_file便释放了,此后m_file便指向无效资源
}

praviteRAIIFilem_file;
}
对于成员对象的RAII只能通过在析构函数或Dispose()中手工地释放。我还没有想出更好的办法来。
至此,RAII的来龙去脉已经说清楚了,在C#里也能从中汲取到充足的养分。但是,这还不是RAII的全部营养,RAII还有更多的扩展用途。在 《Imperfect C++》一书中,Matthew Wilson展示了RAII的一种非常重要的应用。为了不落个鹦鹉学舌的名声,这里我给出一个真实遇到的案例,非常简单:我写的程序需要响应一个Grid 控件的CellTextChange事件,执行一些运算。在响应这个事件(执行运算)的过程中,不能再响应同一个事件,直到处理结束。为此,我设置了一个 标志,用来控制事件响应:
classMyForm
...{
public:
MyForm():is_cacul(
false)...{}
...
voidOnCellTextChange(Cell&cell)...{
if(is_cacul)
return;
is_cacul
=true;
...
//执行计算任务
is_cacul=false;
}

private:
boolis_cacul;
}
;
但是,这里的代码不是异常安全的。如果在执行计算的过程中抛出异常,那么is_cacul标志将永远是true。此后,即便是正常的 CellTextChange也无法得到正确地响应。同前面遇到的资源问题一样,传统上我们不得不求助于try-catch语句。但是如果我们运用 RAII,则可以使得代码简化到不能简化,安全到不能再安全。我首先做了一个类:
classBoolScope
...{
public:
BoolScope(
bool&val,boolnewVal)
:m_val(val),m_old(val)
...{
m_val
=newVal;
}

~BoolScope()...{
m_val
=m_old;
}


private:
bool&m_val;
boolm_old;
}
;
这个类的作用是所谓“域守卫(scoping)”,构造函数接受两个参数:第一个是一个bool对象的引用,在构造函数中保存在m_val成员里;第二个 是新的值,将被赋予传入的那个bool对象。而该对象的原有值,则保存在m_old成员中。析构函数则将m_old的值返还给m_val,也就是那个 bool对象。有了这个类之后,便可以很优雅地获得异常安全:
classMyForm
...{
public:
MyForm():is_cacul(
false)...{}
...
voidOnCellTextChange(Cell&cell)...{
if(is_cacul)
return;
BoolScopebs_(is_cacul,
true);
...
//执行计算任务
}

private:
boolis_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,完成或异常抛出时再打开:
classDrawScope:IDisposable
...{
publicDrawScope(Gridg,boolval)...{
m_grid
=g;
m_old
=g->AutoDraw;
m_grid
->AutoDraw=val;
}

publicvoidDispose()...{
g
->AutoDraw=m_old;
}

privateGridm_grid;
privateboolm_old;
}
;
于是,我便可以如下优雅地处理AutoDraw属性设置问题:
staticvoidLoadData(Gridg)...{
using(DrawScopeds=newDrawScope(g,false))
...{
...
//执行数据装载
}

}
现在,我们回过头,来实现值类型的域守卫。案例还是采用前面的CellTextChange事件。当我试图着手对那个is_cacul执行域守卫时
分享到:
评论

相关推荐

    C++ 中的 RAII(资源获取即初始化)是什么

    在C++中,RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种管理资源的编程技术,它通过将资源的生命周期绑定到对象的生命周期来确保资源的正确获取和释放。这种技术利用了C++的构造函数和...

    C++笔记——iNotes

    这本"C++笔记——iNotes"很可能是为了帮助学习者掌握C++的基础和进阶概念而编写的。在深入探讨之前,我们需要明确C++的核心特性,包括面向对象编程(OOP)、泛型编程以及底层系统编程能力。 1. **基础概念** - **...

    C++实例——垃圾清理系统

    此外,C++11引入了RAII(Resource Acquisition Is Initialization)原则,这是一种利用对象生命周期管理资源的设计模式。例如,`std::unique_lock`用于管理互斥锁,当锁离开作用域时,会自动解锁,避免死锁。 在...

    C++中的RAII机制详解

    在写C++设计模式——单例模式的时候,在写到实例销毁时,设计的GC类是很巧妙的,而这一巧妙的设计就是根据当对象的生命周期结束时会自动调用其析构函数的,而这一巧妙的设计也是有专业的名词的——RAII。那以下将...

    30天掌握C++精髓——经典教程

    - **RAII资源管理**:掌握Resource Acquisition Is Initialization模式,实现自动资源管理。 #### 2. STL标准库 - **容器**:介绍vector、list、map等常用容器的使用方法及其特性。 - **算法**:学习STL中提供的...

    C++标准库——自学教程与参考手册(第2版)英文版

    ### C++标准库——自学教程与参考手册(第2版)知识点详解 #### 一、书籍基本信息概述 《C++标准库——自学教程与参考手册(第2版)》是一本由Nicolai M. Josuttis编写的权威指南,旨在帮助读者深入理解并掌握C++...

    C++编程惯用法——高级程序员常用方法和技巧

    在C++编程中,掌握高级程序员的常用方法和技巧是提升代码质量和效率的关键。...在阅读《C++编程惯用法——高级程序员常用方法和技巧》这本书时,结合实际项目进行练习,将有助于更好地掌握这些技巧。

    nanoalarm:C++ 的简单 RAII 警报

    纳米警报 C++ 的简单 RAII 警报概要 int main() { nanoalarm::Alarm a(1); pause(); ok(1, "passed"); done_testing();}执照 The MIT License (MIT)Copyright (C) 2015 Tokuhiro Matsuno, ...

    C++程序的设计机制3 RAII机制

    C++ 程序的设计机制 3 RAII 机制 RAII(Resource Acquisition Is Initialization)机制是一种常用的C++设计机制,旨在解决资源管理的问题。该机制是由Bjarne Stroustrup 首先提出的,以解决在C++中异常处理时资源...

    奇妙的C++——智能指针

    在C++编程中,智能指针(Smart Pointer)是一种非常重要的概念,它为管理动态内存提供了更加安全和方便的方式。智能指针本质上是封装了原始指针的对象,它能够自动管理所指向的对象生命周期,防止内存泄漏。在C++11...

    超越c++标准库——boost程序库导论.rar

    《超越C++标准库——Boost程序库导论》是一本深度探讨C++编程技术的书籍,专注于Boost程序库的应用。Boost库是C++社区广泛认可的一组开源库,它为C++程序员提供了许多强大而高效的工具,以解决实际编程中的各种问题...

    高质量C++编程规范——林锐博士编写

    了解RAII(Resource Acquisition Is Initialization)原则,以保证资源的正确释放。 5. **模板和泛型编程**:模板可以实现代码复用,但过度使用可能导致编译器生成大量代码。理解模板元编程,避免使用复杂模板...

    C++编程惯用法——高级程序员常用方法和技巧.rar

    本资源“C++编程惯用法——高级程序员常用方法和技巧”是一份珍贵的学习资料,它涵盖了C++语言中的一些核心概念、最佳实践以及高效编程策略。下面将对这些关键知识点进行详细的阐述。 1. **模板(Templates)**:C++...

    掌握C++的RAII:资源安全的编码艺术

    C++是一种通用的、面向对象的编程语言,它是由Bjarne Stroustrup在20世纪80年代初作为C语言的扩展而创建的。C++是C语言的一个超集,这意味着它包含了C语言的所有特性,并增加了面向对象编程的特性,如类和继承。C++...

    C++编程规范——101条规则、准则与最佳实践,英文版,高清。

    《C++编程规范——101条规则、准则与最佳实践》是一本旨在提升C++开发者编程质量和效率的指南。本书涵盖了从基本语法到高级设计原则的诸多方面,旨在帮助程序员编写出更清晰、更安全、更易于维护的代码。以下是对...

    c++编程惯用法——高级程序员惯常用方法和技巧

    《C++编程惯用法——高级程序员惯常用方法和技巧》是一本面向有一定C++编程基础的开发者的专业书籍,由知名作者Robet B. Murray撰写。这本书深入探讨了C++编程中的高级技巧和最佳实践,旨在提升程序员的编程效率和...

    c++编程惯用法——高级程序员常用方法和技巧

    以下是一些核心的C++高级编程知识点,这些内容可能会在"C++编程惯用法——高级程序员常用方法和技巧"这本书中详细阐述: 1. **模板(Templates)**:C++中的模板是一种强大的工具,可以用于实现泛型编程。它允许我们...

Global site tag (gtag.js) - Google Analytics