C++的营养——RAII

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/longshanks/article/details/2097854

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#的设计者有无数理 由说明这种设计的合理性,但是在这里,却扼杀了一个非常有用的idom。而且,缺少对值对象的引用手段,大大限制了语言的灵活性和扩展性。
    第二种方法就非常直白了,也绝对不应当出问题,就是使用包装类:
        class BoolVal
        
{
            
public BoolVal(bool v)
            
{
                m_val
=v;
            }

            
public bool getVal() {
                
return m_val;
            }

            
public void setVal(bool v) {
                m_val
=v;
            }

            
private bool m_val;
        }

        
class BoolScope : IDisposable
        
{
            
public IntScope(BoolVal iv, bool v)
            
{
                m_old 
= iv.getVal();
                m_Val 
= iv;
                m_Val.setVal(v);
            }

            
public virtual void Dispose()
            
{
                m_Val.setVal(m_old);
            }

            
private BoolVal m_Val;
            
private bool m_old;
        }
    这里,我做了一个包装类BoolVal,是个引用类。然后以此为基础,编写了一个BoolScope类。然后,便可以正常使用域守卫:
        class MyForm
        
{
            
public MyForm() {
                m_val.setVal(
false); //boxing
            }

            ...
            
void OnCellTextChange(Cell& cell) {
                
if(is_cacul)
                    
return;
                
using(BoolScope bs=new BoolScope(m_val, true))
                
{
                    ... 
//执行计算任务
                }

            }

            
private BoolVal m_val;
        }
;
    好了,一切都很不错。尽管C#的对象模型给我们平添了不少麻烦,使得我多写了不少代码,但是使用域守卫类仍然是一本万利的事情。作为GP fans,我当然也尝试着在C#里做一些泛型,以免去反复开发包装类和域守卫类的苦恼。这些东西,就留给大家做练习吧。:)
    在某些场合下,我们可能会对一些对象做一些操作,完事后在恢复这个对象的原始状态,这也是域守卫类的用武之地。只是守卫一个结构复杂的类,不是一件轻松的 工作。最直接的做法是取出所有的成员数据,在结束后再重新复制回去。这当然是繁复的工作,而且效率不高。但是,我们将在下一篇看到,如果运用swap手 法,结合复制构造函数,可以很方便地实现这种域守卫。这我们以后再说。
    域守卫作为RAII的一个扩展应用,非常简单,但却极具实用性。如果我们对“资源”这个概念加以推广,把一些值、状态等等内容都纳入资源的范畴,那么域守卫类的使用是顺理成章的事。

题外话:C#的对象模型

    C#的设计理念是简化语言的学习和使用。但是,就前面案例中出现的问题而言,在特定的情况下,特别是需要灵活和扩展的时候,C#往往表现的差强人意。C# 的对象模型实际上是以堆对象和引用语义为核心的。不过,考虑到维持堆对象的巨大开销和性能损失,应用在一些简单的类型上,比如int、float等等,实 在得不尝失。为此,C#将这些简单类型直接作为值处理,当然也允许用户定义自己的值类型。值类型拥有值语义。而值类型的本质是栈对象,引用类型则是堆对 象。
    这样看起来应该是个不错的折中,但是实际上却造成了不大不小的麻烦。前面的案例已经明确地表现了这种对象模型引发的麻烦。由于C#抛弃值和引用的差异(为 了简化语言的学习和使用),那么对于一个引用对象,我们无法用值语义访问它;而对于一个值对象,我们无法用引用语义访问。对于前者,不会引发本质性的问 题,因为我们可以使用成员函数来实现值语义。但是对于后者,则是无法逾越的障碍,就像在BoolScope案例中表现的那样。在这种情况下,我们不得不用 引用类包装值类型,使得值类型丧失了原有的性能和资源优势。
    更有甚者,C#的对象模型有时会造成语义上的冲突。由于值类型使用值语义,而引用类型使用引用语义。那么同样是对象定义,便有可能使用不同的语义:
        int i, j=10;  //值类型
        i=j;            //值语义,两个对象复制内容
        i=5;           //i==5, j==10
        StringBuilder s1, s2 = new StringBuilder("s2");   //引用类型
        s1 = s2;        //引用语义,s1和s2指向同一个对象
        s1.Append(" is s1");    //s1==s2=="s1 is s2"
    同一个形式具有不同语义,往往会造成意想不到的问题。比如,在软件开发的最初时刻,我们认为某个类型是值类型就足够了,还可以获得性能上的好处。但是,随 着项目进入后期阶段,发现最初的设计有问题,值类型限制了该类型的某些特性(如不能拥有析构函数,不能引用等等),那么需要把它改成引用类型。于是便引发 一大堆麻烦,需要检查所有使用该类型的代码,然后把赋值操作改成复制操作。这肯定不是讨人喜欢的工作。为此,在实际开发中,很少自定义值类型,以免将来自缚手脚。于是,值类型除了语言内置类型和.net库预定义的类型外,成了一件摆设。
    相比之下,传统语言,如Ada、C、C++、Pascal等,区分引用和值的做法尽管需要初学者花更多的精力理解其中的差别,但在使用中则更加妥善和安全。毕竟学习是暂时的,使用则是永远的。 
展开阅读全文

C++营养——swap手法

02-26

[align=center][size=16px]C++的营养[/size][/align]rn[align=center]莫华枫[/align]rn 上一篇《C++的营养——RAII》中介绍了RAII,以及如何在C#中实现。这次介绍另一个重要的基础技术——swap手法。rn[b][size=12px]swap手法[/size][/b]rn swap手法不应当是C++独有的技术,很多语言都可以实现,并且从中得到好处。只是C++存在的一些缺陷迫使大牛们发掘,并开始重视这种有用的手法。这个原本被用来解决C++的资源安全和异常保证问题的技术在使用中逐步体现出越来越多的应用,有助于我们编写更加简洁、优雅和高效的代码。rn 接下来,我们先来和swap打个招呼。然后看看在C#里如何玩出swap。最后展示swap手法的几种应用,从中我们将看到它是如何的可爱。rn 假设,我要做一个类,实现统计并保存一个字符串中字母的出现次数,以及总的字母和数字的个数。rn class CountStrrn ...rn public:rn explicit CountStr(std::string const& val)rn :m_str(val), m_nLetter(0), m_nNumber(0) ...rn do_count(val);rn rn CountStr(CountStr const& cs)rn :m_str(cs.m_str), m_counts(cs.m_counts)rn , m_nLetter(cs.m_nLetter), m_nNumber(cs.m_nNumber)rn ...rn void swap(CountStr& cs) ...rn std::swap(m_str, cs.m_str);rn m_counts.swap(m_str);rn std::swap(m_nLetter, cs.m_nLetter);rn std::swap(m_nNumber, cs.m_nNumber);rn rn private:rn std::string m_str;rn std::map m_counts;rn int m_nLetter;rn int m_nNumber;rn rn 在类CountStr中,定义了swap成员函数。swap接受一个CountStr&类型的参数。在函数中,我们可以看到一组函数调用,每一个对应一个数据成员,其任务是将相对应的数据成员的内容相互交换。此处,我使用了两种调用,一种是使用std::swap()标准函数,另一种是通过 swap成员函数执行这个交换。一般情况下,std::swap()通过一个临时变量实现对象的内容交换。但对于string、map等非平凡的对象,这种交换会引发至少三次深拷贝,其复杂度将是O(3n)的,性能极差。因此,标准库为这些类定义了swap成员函数,通过成员函数可以实现O(1)的交换操作。同时将std::swap()针对这些拥有swap()成员函数的标准容器特化,使其可以直接使用swap()成员函数,而避免性能损失。但是,对于那些拥有swap()成员,但没有被特化的用户定义,或第三方的类,则不应使用std::swap(),而改用swap()成员函数。所以,一般情况下,为了避免混淆,对于拥有swap()成员函数的类,调用swap(),否则调用标准std::swap()函数。rn 顺便提一下,在未来的C++0x中,由于引入了concept机制,可以允许一个函数模板自动识别出所有“具有swap()成员”的类型,并使用相应的特化版本。这样便只需使用std::swap(),而不必考虑是什么样的类型了。rn 言归正传。这里,swap()成员函数有两个要求,其一是复杂度为O(1),其二是具备无抛掷的异常保证。前者对于性能而言至关重要,否则swap操作将会由于性能问题而无法在实际项目中使用。对于后者,是确保强异常保证(commit or rollback语义)的基石。要达到这两个要求,有几个关键要点:首先,对于类型为内置类型或小型POD(8~16字节以内)的成员数据,可以直接使用 std::swap();其次,对于非平凡的类型(拥有资源引用,复制构造和赋值操作会引发深拷贝),并且拥有符合上述要求的swap()成员函数的,直接使用swap()成员函数;最后,其余的类型,则保有其指针,或智能指针,以确保满足上述两个要求。rn 听上去有些复杂,但在实际开发中做到并不难。首先,尽量使用标准库容器,因为标准库容器都拥有满足两个条件的swap()成员。其次,在编写的每一个类中实现满足两个条件的swap()成员。最后,对于那些不具备swap()成员函数的第三方类型,则使用指针,最好是智能指针。(也就是Sutter所谓的 PImpl手法)。只要坚持这些方针,必能收到很好的效果。rn 下面,就来看一下这个swap()的第一个妙用。假设,这个类需要复制。通常可以通过operator=操作符,或者copy(或其他有明确的复制含义的)成员函数实现,这两者实际上是等价的,只是形式不同而已。这里选择operator=,因为它比较C++:)。rn 最直白的实现方式是这样:rn class CountStrrn ...rn public:rn ...rn CountStr& operator=(CountStr& val) ...rn m_str=val.m_str;rn m_counts=val.m_counts;rn m_nLetter=val.m_nLetter;rn m_nNumber=val.m_nNumber;rn rn ...rn rn 很简单,但是不安全,或者说没有满足异常保证。rn 先解释一下异常保证。异常保证有三个级别:基本保证、强异常保证和无抛掷保证。基本保证是指异常抛出时,程序的各个部分应当处于有效状态,不能有资源泄漏。这个级别可以轻而易举地利用RAII确保,这在前一篇已经展示过了。强异常保证则更加严格,要求异常抛出后,程序非但要满足基本保证,其各个部分的数据应保持原状。也就是要满足“Commit or Rollback”语义,熟悉数据库的人,可以联想一下Transaction的行为。而无抛掷保证要求函数在任何情况下都不会抛出异常。无抛掷保证不是说用一个catch(...)或throw()把异常统统吞掉。而是说在无抛掷保证的函数中的任何操作,都不会抛出异常。能满足无抛掷保证的操作还是很多的,比如内置POD类型(int、指针等等)的复制,swap手法便以此为基础。(多说一句,用catch(...)吞掉异常来确保无抛掷并非绝对不行,在特定情况下,还是可以偶尔一用。不过这等烂事也只能在西构函数中进行,而且也只有在迫不得已的情况下用那么一下)。rn 如果这四个赋值操作中,任意一个抛出异常,便会退出这个函数(操作符)。此时,至少有一个成员数据没有正确修改,而其他的则全部或部分地发生改变。于是,一部分成员数据是新的,另一部分是旧的,甚至还有一些是不完全的。这在软件中往往会引发很多令人苦恼的bug。无论如何,此时应当运用强异常保证,使得数据要么是新的值,要么没有改变。那么如何获得强异常保证?在swap()的帮助下,惊人的简单:rn class CountStrrn ...rn public:rn ...rn CountStr& operator=(CountStr& val) ...rn swap(CountStr(val)); // 或者CountStr(val).swap(*this);rn raturn *this; rn rn ...rn rn 我想世上没有比这等代码更加漂亮的了吧!不仅仅具有简洁动人的外表,而且充满了丰富的内涵。这就叫优雅。不过,优雅之下还需要一些解释。在这两个版本中,都是先用复制构造创建一个临时对象,这个临时对象同传入的参数对象拥有相同的值。然后用swap()成员函数将this对象的内容与临时对象交换。于是, this对象拥有了临时对象的值,也就是与传入的实参对象具有相同的值(复制)。当退出函数的时候,临时对象销毁,自然而然地释放了this对象原先的资源(前提是CountStr类实现了RAII)。rn 那么抛出异常的情况又是怎样的呢?rn 先来看看operator=里执行了哪些步骤,并考察这些步骤的异常抛掷的情况。如果将代码改写成另一个等价的形式,就很容易理解了:rn CountStr& operator=(CountStr& val) ...rn CountStr t_(val); //此处可能抛出异常,但只有t_的值发生变化rn t_.swap(*this); //由于swap拥有无抛掷保证,所以不会抛出异常rn return *this;rn rn 在构造临时对象的时候,可能会抛出异常,因为此时执行了数据的复制和构造。请注意,这时候this对象的内容没有改变。如果此时抛出异常,数据发生改变的只有t_,this对象并未受到影响。而随着栈清理,t_也将被析构,在RAII的作用下,t_所占用的资源也会依次释放。而下一步,swap()成员的调用,则是无抛掷保证的,不会抛出异常,this的内容可以得到充分地、原子地交换,不会发生数据值修改一半的情况。rn 在C#中,实现swap非常容易,甚至比C++更容易。因为在C#中,大部分对象都在堆上,代码中定义的所谓对象实际上是引用。对于引用的赋值操作是无抛掷的,因此在C#中可以采用同C++几乎一样的代码实现swap:rn class CountStrrn ...rn public CountStr(string val) ...rn m_str=val;rn m_nLetter=0;rn m_nNumber=0;rn do_count(val);rn rn public CountStr(CountStr cs) ...rn m_str=new string(cs.m_str);rn m_counts=new Dictionary(cs.m_counts);rn m_nLetter=cs.m_nLetter;rn m_nNumber=cs.m_nNumberrn rnrn public void swap(CountStr& cs) ...rn utility.swap(ref m_str, ref cs.m_str);rn utility.swap(ref m_counts, ref cs.m_counts);rn utility.swap(ref m_nLetter, ref cs.m_nLetter);rn utility.swap(ref m_nNumber, ref cs.m_nNumber);rn rn public void copy(CountStr& cs) ...rn this.swap(new CountStr(cs));rn rnrn private string m_str;rn private Dictionary m_counts;rn private int m_nLetter;rn private int m_nNumber;rn 论坛

营养】关于爱情

02-18

[b]以钻戒房子汽车下跪才能打动的女人,最好别娶;用父母的钱以钻戒房rn子汽车和下跪来求婚的男人,最好别嫁。这些东西既锁不住女人的感情rn,也撑不起男人的自信。[/b]rn[img=http://hi.csdn.net/attachment/201102/18/3208149_129799023450k4.jpg][/img]rnrn爱一个人没有错,爱得自私、爱得霸道、爱得失去自己都没有错,只是rn爱几分相宜,多少温度对方能接受,却是相爱的人时时刻刻应该反省的rn。若是真爱,首先要给他的是自由,包括给他爱你与不爱你的自由。爱rn一个人,不是剥夺一个人的自由,不是限制一个人的喜好。其实,给对rn方自由,也就是给自己自由rn[img=http://hi.csdn.net/attachment/201102/18/3208149_12979902368DCA.gif][/img]rnrn[b]如果某个男人主动替你拎包,把你放在道路的里边走,主动为你拉椅子rn,不要因此而感激涕零。这只能说明他之前有无数个女朋友教过他这一rn点。而能让他记住的女人,永远是改变了他的那个女人,而不是你。所rn以,越是细节完美的男人,对女人而言越是挑战。[/b][img=http://hi.csdn.net/attachment/201102/18/3208149_1297990235NlL2.jpg][/img]rnrn研究表明:身高黄金比例:12cm的高度差 | 女配男的最佳身高差上rn12cm,这样不管是牵手、拥抱、接吻,都是最和谐的差度。年龄黄金比rn例:3岁的成熟差 | 女生一般比男生早熟,两个人要和平相处,心理年rn龄很关键,男比女大三岁就正适宜。rn[img=http://hi.csdn.net/attachment/201102/18/3208149_1297990236vr04.jpg][/img]rnrn【女人必修的十堂电影课】1.《乱世佳人》—坚强;2.《钢琴课》—rn沟通;3.《蒂凡尼的早餐》—虚荣;4.《简爱》—尊严;5.《白领丽人rn》—才华;6.《母女情深》—亲情;7.《紫色》—苦难;8.《末路狂花rn》—女权;9.《漂亮女人》—浪漫;10.《女人那话儿》—性爱。rn[img=http://hi.csdn.net/attachment/201102/17/3208149_1297918368F107.jpg][/img]rnrnrn[b]炊烟起了, 我在门口等你。 夕阳下了, 我在山边等你。 叶子黄了, rn我在树下等你。 月儿弯了, 我在十五等你。 细雨来了, 我在伞下等rn你。 流水冻了, 我在河畔等你。 生命累了, 我在天堂等你。 我们rn老了, 我在来生等你......... [/b]rn[img=http://hi.csdn.net/attachment/201102/18/3208149_1297990236xKYt.jpg][/img]rnrn[b]做一个淡淡的女子,不浮不躁,不争不抢,不去计较浮华之事,不是不rn追求,只是不去强求。淡然地过着自己的生活,不要轰轰烈烈,只求安rn安心心。[/b]rn[img=http://hi.csdn.net/attachment/201102/17/3208149_12979183693xZ9.jpg][/img]rnrnrnrn[color=#999999]摘自网络 莫默整理分享[/color] 论坛

没有更多推荐了,返回首页