Effective C++ 总结(一)

一.让自己习惯C++


   条款01:视C++为一个语言联邦
 
    为了更好的理解C++,我们将C++分解为四个主要次语言:
  • C。说到底C++仍是以C为基础。区块,语句,预处理器,内置数据类型,数组,指针统统来自C。
  • Object-Oreinted C++。这一部分是面向对象设计之古典守则在C++上的最直接实施。类,封装,继承,多态,virtual函数等等...
  • Template C++。这是C++泛型编程部分。
  • STL。STL是个template程序库。容器(containers),迭代器(iterators),算法(algorithms)以及函数对象(function objects)...
   请记住:
  • 这四个次语言,当你从某个次语言切换到另一个,导致高效编程守则要求你改变策略。C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。

    条款02:尽量以const,enum,inline替换#define

    这个条款或许可以改为“宁可以编译器替换预处理器”。即尽量少用预处理。

     编译过程:.c文件--预处理-->.i文件--编译-->.o文件--链接-->bin文件

     预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。可见预处理过程先于编译器对源代码进行处理。预处理指令是指在编译之前进行处理的命令,以#号开头,包含3个方面的内容:宏定义、文件包含、条件编译(#ifdef #endif)。

     例:#define ASPECT_RATIO 1.653

     记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源代码之前它就被预处理器移走了。即编译源代码时ASPECT_RATIO已被1.653取代。ASPECT_RATIO可能并未进入记号表(symbol table)。

     替换:const double AspectRatio = 1.653;

     好处应该有:多了类型检查,因为#define 只是单纯的替换,而这种替换在目标码中可能出现多份1.653;改用常量绝不会出现相同情况。

    常量替换#define两点注意:
  • 定义常量指针:
       const char *authorName = “Shenzi”;
       cosnt std::string authorName("Shenzi");
  • 类专属常量:

     static const int NumTurns = 5;//static 静态常量 所有的对象只有一份拷贝。
    万一你编译器不允许“static int class常量”完成“in calss初值设定”(即在类的声明中设定静态整形的初值),我们可以通过枚举类型予以补偿:
      enum { NumTurns = 5 };
    *取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获取一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。
     例:#define CALL_WITH_MAX(a,b)    f((a) > (b)) ? (a) : (b))
     宏看起来像函数,但不会招致函数调用带来的额外开销,而是一种简单的替换。
     替换:
      template<typename T>
      inline void callWithMax(cosnt T &a, cosnt T &b)
      {
            f(a > b ? a : b);
       }
      callWithMax是个真正的函数,它遵循作用于和访问规则。
      请记住:

  • 对于单纯常量,最好以const对象或enums替换#defines;
  • 对于形似函数的宏,最好改用inline函数替换#defines。   

 


 

    条款03:尽可能使用const


    const允许你告诉编译器和其他程序员某值应保持不变,只要“某值”确实是不该被改变的,那就该确实说出来。
    关键字const多才多艺:
    例:
        char greeting[] = "Hello";
        char *p = greeting;    //指针p及所指的字符串都可改变;
        const char *p = greeting;    //指针p本身可以改变,如p = &Anyother;p所指的字符串不可改变;
        char * cosnt p = greeting;    //指针p不可改变,所指对象可改变;
        const char * const p = greeting;    //指针p及所致对象都不可改变;
     说明:
  • 如果关键字const出现在星号左边,表示被指物事常量。const char *p和char const *p两种写法意义一样,都说明所致对象为常量;
  • 如果关键字const出现在星号右边,表示指针自身是常量。   

    STL例子:

        声明迭代器为const就像声明指针为const一样(即声明一个T*const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果希望迭代器所指的东西是不可被改动的,需要的是const_iterator。

        const std::vector<int>::interator iter = vec.begin();//iter是const常量, ++iter 错误
        std::vector<int>::const_iterator cIter = vec.begin();//iter所指的元素是const常量,*cIter = 10 错误
    以下几点注意:

  • 令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而不至于放弃安全性和高效性。
    例:const Rational operator*(const Rational& lhs, const Rational& rhs)
   {
            return Rational(lhs.numerator() * rhs.numerator(),
            lhs.denominator() * rhs.denominator());
   }
    返回值用const修饰可以防止允许这样的操作发生:
   Rational a,b;
   Radional c;
   (a*b) = c;
   一般用const修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候。
    类中的成员函数:A fun4()  const; 其意义上是不能修改所在类的的任何变量。 
  • const成员函数使class接口比较容易被理解,它们使“操作const对象”称为可能;
    说明:声明为const的成员函数,不可改变non-static成员变量,在成员变量声明之前添加 mutable 可让其在const成员函数中可被改变。
    class Block
   {
       public:
         ....
       private:
       char* ch;
     mutable int val  //这个成员变量可能总是会被改变,即使在const成员函数内
    }
    请记住:
  • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体;
  • 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness);
  • 当cosnt和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

    条款04:确定对象被使用前已先被初始化

    永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。至于内置类型以外的任何其它东西,初始化责任落在构造函数身上,确保每一个构造函数都将对象的每一个成员初始化。
    赋值和初始化:
    C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。所以应将成员变量的初始化置于构造函数的初始化列表中。
    在构造函数体内进行初始化实际上包含了2个阶段:
   (1)调用默认构造函数进行初始化 
   (2)调用赋值函数覆盖默认构造函数的值
     ABEntry::ABEntry(const std::string& name, const std::string& address,
                                     const std::list<PhoneNumber>& phones)
     { 
            theName = name;                    //这些都是赋值,而非初始化
            theAddress = address;          //这些成员变量在进入函数体之前已调用默认构造函数,接着又调用赋值函数,
            thePhones = phones;            //即要经过两次的函数调用。            
            numTimesConsulted = 0;
    } 

    ABEntry::ABEntry(const std::string& name, const std::string& address,
                                    const std::list<PhoneNumber>& phones) 
        : theName(name),                       //这些才是初始化 
        theAddress(address),                //这些成员变量只以相应的值为参数调用拷贝构造函数,所以通常效率更高。
        thePhones(phones),
        numTimesConsulted(0)
        {    } 
    所以,对于非内置类型变量的初始化应在初始化列表中完成,以提高效率。而对于内置类型对象,如numTimesConsulted(int),其初始化和赋值的成本相同,但为了一致性最好也通过成员初始化表来初始化。如果成员变量时const或reference,它们就一定需要初值,不能被赋值。
     注意:对于const或引用类型的成员变量(声明引用和const常量时,必须对其进行初始化),以及没有默认构造函数的类的任何成员必须使用初始化式。

      C++有着十分固定的“成员初始化次序”。基类总是在派生类之前被初始化,而类的成员变量总是以其说明次序被初始化。所以:当在成员初始化列表中列各成员时,最好总是以其声明次序为次序
所以类的构造函数初始化列表上的初始化顺序跟类真实的成员初始化顺序是没有关系的。

      static对象,其寿命从被构造出来直到程序结束为止。

      函数内的 static 对象称为 local static 对象(因为它们对函数而言是local),其他 static 对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。

      当我们的某个编译单元内的某个non-local static对象的初始化动作使用了另一编译单元的某个non-local static对象,它所用到的这个对象可能尚未被初始化。因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。
 
      比如在a.cpp里我们定义一个类 FileSystem,一个该类的对象
class FileSystem 
{ 
public: 
    size_t numDisks()const; 
}; 
extern FileSystem tfs;

  现在同一个项目下的b.cpp文件中有一个类 Directory,类构造函数用到了tfs对象。

class Directory   
{   
public:   
    Directory(params);   
};   
Directory::Directory(params)   
{   
    size_t disks = tfs.numDisks();   
}  

  现在如果我们创建了一个Directory对象 Directory tempDir(params);

  上面的代码就可能会出问题,除非能保证 tfs 在 tempDir 之前先初始化,否则 tempDir 的构造函数会用到尚未初始化的tfs。
  解决方案:
      C++保证,函数内的local static对象会在该函数被调用期间,首次遇到该对象的定义的时候被初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个已经初始化的对象。
  所以如我们把 tfs 和 tempDir 设计为一个函数,函数返回该类的一个static对象引用就可以解决问题了。

  所以我们可以改写上面的代码:

class FileSystem{...};

FileSystem& tfs()                //用这个函数替换tfs对象,它在
{                                //FileSystem class中可能是个static.
    static FileSystem fs;        //定义并初始化一个local static对象
    return fs;                   //返回一个reference指向上述对象
}

class Directory{...};
Directory::Directory(params)     //同前,但原本的reference to tfs
{                                //现在改为tfs()
   ...
   int disks = tfs().numDisks();
   ...
}
Directory& tempDir()             //用这个函数替换tempDir对象,它在
{                                //tempDir class中可能是个static.
    static Directory td;         //定义并初始化一个local static对象
    return td;                   //返回一个reference指向上述对象
}
请记住:
  • 为内置对象进行手工初始化,因为C++不保证初始化它们;
  • 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同; 
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

 

二.构造/析构/赋值运算


    条款05:了解C++默默编写并调用哪些函数

    如果你自己没有声明,编译器就会为类声明(编译器版本的)一个拷贝构造函数,一个拷贝赋值操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会成为你声明一个默认构造函数。所有这些函数都是public且inline
     惟有当这些函数被需要(被调用),它们才会被编译器创建出来。即有需求,编译器才会创建它们。
     默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用基类和非静态成员变量的构造函数和析构函数。
     注意:编译器产生的析构函数是个non-virtual,除非这个类的基类自身声明有virtual析构函数。
     至于拷贝构造函数和拷贝赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。
     如一个类声明了一个构造函数(无论有没有参数),编译器就不再为它创建默认构造函数。 
     编译器生成的拷贝赋值操作符:对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为指向同块内存的指针是个潜在危险,引用不可改变,常量不可改变。

请记住

  • 编译器可以暗自为类创建默认构造函数、拷贝构造函数、拷贝赋值操作符,以及析构函数

    条款06:若不想使用编译器自动生成的函数,就该明确拒绝

     通常如果你不希望类支持某一功能,只要不声明对应函数就是了。但这个策略对拷贝构造函数和拷贝赋值操作符却不起作用。因为编译器会“自作多情”的声明它们,并在需要的时候调用它们。
     由于编译器产生的函数都是public类型,因此可以将拷贝构造函数或拷贝赋值操作符声明为private。通过这个小“伎俩”可以阻止人们在外部调用它,但是类中的成员函数和友元函数还是可以调用private函数。解决方法是设计一个专门为了阻止拷贝动作而设计的基类。(Boost提供的那个类名为noncopyable)。

     即如果某些对象是独一无二的(比如房子),你应该禁用 copy 构造函数或copy assignment 操作符,可选的方案有两种:

    (1)将copy构造函数和copy assignment操作符声明为private,并不予实现

    (2)在(1)中,当member函数或friend函数调用copy构造函数(或copy assignment操作符),会出现链接错误。我们可以将连接期间的错误移到编译期间(这是好事,越早侦测出错误越好)。我们可以定义一个Uncopyable公共基类,并将copy构造函数和copy assignment操作符声明为private,并不予实现,然后让所有独一无二的对象继承它。

class Uncopyable {
protected:                          //允许derived对象构造和析构
 Uncopyable() {}
 -Uncopyable(){}
private:
 Uncopyable(const Uncopyable&};   //但阻止copying
 Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale: private Uncopyable{
 …
};

  这样是行得通的,因为我们知道,当外部对象(甚至是member函数或者friend函数)尝试拷贝HomeForSale对象时,编译器都会尝试着生成一个默认的copy构造函数和copy assignment操作符,而这些默认生成的函数会调用其基类base class中对应的函数,因为基类已经将其声明为private,所以这些调用会被编译器拒绝。 

     请记住:

  • 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像noncopyable这样的基类也是一种做法。    

    条款07:为多态基类声明 virtual 析构函数

     当基类的指针指向派生类的对象的时候,当我们使用完,对其调用delete的时候,其结果将是未有定义——基类成分通常会被销毁,而派生类的成分还留在堆里,于是造成一个诡异的“局部销毁”对象,这样就会造成资源泄漏。
     解决方法:给基类一个virtual析构函数,此后删除派生类对象就会如你想要的那般,它会销毁整个对象,包括所有的derived class成分。因为析构函数的运作方式是,最深层派生(most derived)的那个class其析构函数最先被调用,接着是其每一个base class的析构函数被调用。
     注意:如果在高层的基类含有一个纯虚析构函数(不实现的话该类变成抽象类,是不允许存在实例的),那么必须给他提供一份定义,否则当derived对象调用最高层基类的虚构函数时,就会出现未定义的行为。
class AWOV{  
public:  
    virtual ~AWOV() = 0;    //声明纯虚析构函数  
};  
AWOV::~AWOV(){} // pure virtual析构函数的定义  
  任何类只要带有 virtual 函数都几乎确定应该也有一个virtual析构函数。
     如果一个类不含virtual函数,通常表示它并不意图被用做一个基类,当类不企图被当做基类的时候,令其析构函数为virtual往往是个馊主意。因为,当用户将一个函数声明为virtual时,C++编译器会创建虚函数表(vtbl, virtual table)以完成动态绑定功能,这将带来时间和空间上的花销。
     STL容器都不带virtual析构函数,所以最好别派生它们。
     请记住:
  • 带有多态性质的基类应该声明一个virtual析构函数。如果一个类带有任何virtual函数,它就应该拥有一个virtual析构函数。
  • 一个类的设计目的不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。

    条款08:别让异常逃离析构函数

    C++并不禁止析构函数抛出异常,但它不鼓励你这样做。C++不喜欢析构函数抛出异常。

 (1)析构函数绝对不要抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

 (2)如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class 应该提供一个普通函数(而非在析构函数中)执行该操作。即如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。如下面程序所示:

class DBConn{
public:
 ...
 void close()
 {
  db.close();
  closed = true;
 }
 ~DBConn()
 {
  if (!closed)      
  {
   try{        //如果客户没有关闭连接
    db.close();
   }
   catch(...){
    //结束程序或吞下异常
    std::abort()
   }
  }
 }
private:
 DBConnection db;
 bool closed;
};

    条款09:决不让构造和析构过程中调用virtual函数

    你不该在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你预想的结果。
    因为:基类的构造函数的执行要早于派生类的构造函数,当基类的构造函数执行时,由于其包含了一个virtual函数,而该virtual函数除非被定义,否则连接器将找不到该函数的实现代码,从而出现未定义行为。而且,base class构造期间,virtual函数绝不会下降到derived层,他被当做base class的函数直接调用。这样做的原因在于,当base class构造函数执行时,derived class的成员变量尚未初始化,如果此期间调用的virtual函数下降到derived class(derived class成员尚未初始化),就会出现使用未初始化成员变量的危险行为。

    唯一好的做法是:确定你的构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也不应该调用虚函数。
    解决的方法可能是:既然你无法使用虚函数从基类向下调用,那么我们可以使派生类将必要的构造信息向上传递至基类构造函数。即在派生类的构造函数的成员初始化列表中显示调用相应基类构造函数,并传入所需传递信息。
    请记住:

  • 在构造和析构函数期间不要调用虚函数,因为这类调用从不下降至派生类。

    条款10:令operator= 返回一个reference to *this

    对于赋值操作符,我们常常要达到这种类似效果,即连续赋值:
      int x, y, z;
      x = y = z = 15;
      为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。
      即:
        Widget & operator = (const Widget &rhs)
        {
            ...
            return *this;
        }
      所有内置类型和标准程序库提供的类型如string,vector,complex或即将提供的类型共同遵守。

      如果不返回引用,那么就会调用拷贝构造函数,从而增加了系统开销。

请记住:

  • 令赋值操作符返回一个reference to *this。   
 

    条款12:复制对象时勿忘其每一个成员

    还记得条款5中提到编译器在必要时会为我们提供拷贝构造函数和拷贝赋值函数,它们也许工作的不错,但有时候我们需要自己编写自己的拷贝构造函数和拷贝赋值函数。如果这样,我们应确保对“每一个”成员进行拷贝(复制)。
    如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符)。
    在派生类的构造函数,拷贝构造函数和拷贝赋值操作符中应当显示调用基类相对应的函数,否则编译器可能又“自作聪明了”。
     当你编写一个copying函数,请确保:    

    (1)复制所有local成员变量;
    (2)调用所有基类内的适当copying函数。   
    但是,我们不该令拷贝赋值操作符调用拷贝构造函数,也不该令拷贝构造函数调用拷贝赋值操作符。想想,一个是拷贝(建立对象),一个是赋值(对象已经存在)。
Class Base{..}
class Derived:public Base{
    Derived(const Derived& rhs);
    Derived& operator=(const Derived& rhs);
}
 
Derived::Derived(const Derived& rhs)
       :Base(rhs)    //调用base class的copy构造函数    
{
        ...
}

Derived& Derived::Derived(const Derived& rhs)        
{
        ...
    Base::operator=(rhs); //对base class成分进行赋值操作
    ...
}
请记住:
  • Copying函数应该确保复制“对象内的所有成员变量”及“所有基类成员”;
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。 

 


三.资源管理

    条款13:以对象管理资源

    例:
     void f()
     { 
         Investment *pInv = createInvestment(); 
         ...                   //这里存在诸多“不定因素”,可能造成delete pInv;得不到执行,这可能就存在潜在的内存泄露。
         delete pInv;
     } 
    解决方法:把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。
    许多资源被动态分配于堆内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形势而设计的特制产品。auto_ptr是个“类指针对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。
    void f()
     { 
         std::auto_ptr<Investment> pInv(createInvestment()); 
         ... 
     }          //函数退出,auto_ptr调用析构函数自动调用delete,删除pInv;无需显示调用delete。
    “以对象管理资源”的两个关键想法:

  • 获得资源后立刻放进管理对象内(如auto_ptr)。每一笔资源都在获得的同时立刻被放进管理对象中。“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII)。
  • 管理对象运用析构函数确保资源被释放。即一旦对象被销毁,其析构函数被自动调用来释放资源。
    由于auto_ptr被销毁时会自动删除它所指之物,所以不能让多个auto_ptr同时指向同一对象。所以auto_ptr若通过copying函数复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权!
    看下面例子:
    std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
     std::auto_ptr<Investment> pInv2(pInv1);                      //现在pInv2指向对象,而pInv1被设为NULL;
     pInv1 = pInv2;                                                               //现在pInv1指向对象,而pIn2被设为NULL;
    受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它。即“有你没我,有我没你”。
    auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;SCSP)、它可以持续跟踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。
    TR1的tr1::shared_ptr就是一个"引用计数型智能指针"。
     void f()
     { 
         ... 
         std::tr1::shared_ptr<Investment>  pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
         std::tr1::shared_ptr<Investment>  pInv2(pInv1);                     //pInv1,pInv2指向同一个对象;
         pInv1 = pInv2;                                                                            //同上,无变化
         ... 
     }         //函数退出,pInv1,pInv2被销毁,它们所指的对象也竟被自动释放。
    auto_ptr和tr1::shared_ptr都在其析构函数内做delete而不是delete[],也就意味着在动态分配而得的数组身上使用auto_ptr或tr1::shared_ptr是个潜在危险,资源得不到释放。也许boost::scoped_array和boost::shared_array能提供帮助。还有,vector和string几乎总是可以取代动态分配而得的数组。
    请记住:
  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII类分别是auto_ptr和tr1::shared_ptr。后者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL。  

    条款14:在资源管理类中小心拷贝行为

    我们在条款13中讨论的资源是在上申请的资源,而有些资源并不适合被auto_ptr和tr1::shared_ptr所管理。可能我们需要建立自己的资源管理类。
    例:
     void lock(Mutex *pm);     //锁定pm所指的互斥量
     unlock(Mutex *pm);        //将pm解除锁定
     我们建立的资源管理类可能会是这样:

 class Lock 
    {
        public: 
        explicit Lock(Mutex *pm)
        : mutexPtr(pm) 
        {
             lock(mutexPtr);        //获取资源
        } 
        ~Lock() 
        { 
             unlock(mutexPtr);   //释放资源
        } 
        private: 
        Mutex *mutexPtr; 
    }; 

  但是,如果Lock对象被复制,会发生什么事???

    Lock m1(&m)  //锁定m

    Lock m2(m1)  //将m1复制到m2上,这回发生什么??
    “当一个RAII对象被复制,会发生什么事?”大多数时候你会选择一下两种可能:

  • 禁止复制。如果复制动作对RAII类并不合理,你便应该禁止之,将copying操作声明为private。
  • 对底层资源使用”引用计数法“。有时候我们又希望保有资源,直到它的最后一个使用者被销毁。这种情况下复制RAII对象时,应该将资源的”被引用计数“递增。tr1::shared_ptr便是如此。

     通常只要内含一个tr1::shared_ptr成员变量,RAII类便可实现”引用计数“行为。

  由于tr1::shared_ptr缺省行为是”当引用计数为0时删除其所指物“,幸运的是shared_ptr允许我们指定所谓的“删除器”(deleter),那是一个函数对象,当引用次为0时被调用。

     class Lock 
    {
        public: 
            explicit Lock(Mutex *pm) 
            : mutexPtr(pm, unlock)        //以某个Mutex初始化shared_ptr,并以unlock函数为删除器
        { 
            lock(mutexPtr.get()); 
        } 
        private:
            std::tr1::shared_ptr<Mutex> mutexPtr; 
     }; 

  本例中,并没说明析构函数,因为没有必要。编译器为我们生成的析构函数会自动调用其non-static成员变量(mutexPtr)的析构函数。而mutexPtr的析构函数会在互斥量”引用计数“为0时自动调用tr1::shared_ptr的删除器(unlock)。  

  • 复制底层资源深度拷贝(复制指针和所指内存)。
  • 转移底部资源的拥有权将资源的拥有权从被复制物转移到目标物,例如auto_ptr


    Copying 函有可能被编译器自动创建出来,因此除非编译器所生成版本做了你想要做的事,否则你得自己编写它们。
    请记住:

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  • 普遍而常见的RAII类拷贝行为是:抑制拷贝,施行引用计数法。不过其它行为也可能被实现

     条款15:在资源管理类中提供对原始资源的访问

     前几个条款提到的资源管理类很棒。它们是你对抗资源泄漏的堡垒。但这个世界并不完美,许多APIs直接指涉资源,这时候我们需要直接访问原始资源。
     这时候需要一个函数可将RAII对象(如tr1::shared_ptr)转换为其所内含之原始资源。有两种做法可以达成目标:显示转换和隐式转换。
     tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是返回智能指针内部的原始指针(的复件)。就像所有智能指针一样, tr1::shared_ptr和auto_ptr也重载了指针取值操作符(operator->和operator*),它们允许隐式转换至底部原始指针。(即在对智能指针对象实施->和*操作时,实际被转换为被封装的资源的指针。)
   class Font 
    {
        public: 
        ... 
        FontHandle get() const         //FontHandle 是资源;    显示转换函数
        { 
            return f; 
        }
        operator FontHandle() const         //隐式转换    这个值得注意,可能引起“非故意之类型转换”
        { 
            return f; 
        } 
        ... 
    }; 

  是否该提供一个显示转换函数(例如get成员函数)将RAII类转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAII类被设计执行的特定工作,以及它被使用的情况。显示转换可能是比较受欢迎的路子,但是需要不停的get;而隐式转换又可能引起“非故意之类型转换”。
    请记住:

  • APIs往往要求访问原始资源,所以每一个RAII类应该提供一个“取得其所管理之资源”的方法。
  • 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。   

    条款17:以独立语句将newed对象置入智能指针
 
  为了避免资源泄漏的危险,最好在单独语句内以智能指针存储newed所得对象。
    processWidget(std::trl::shared ptr<Widget> (new Widget) , priority());
      在调用processWidget之前,编译器必须创建代码,做以下三件事:
      (1)   调用 priority
      (2)   执行 ”new Widget”
      (3)  调用 trl: : shared_ptr 构造函数
  不同的C++ 编译器执行这三条语句的顺序不一样,但(2)一定在(3)之前被调用,但对priority的调用可以排在第一或第二或第三执行。如果编译器选择以第二顺位执行且priority 函数抛出了异常,则新创建的对象Widget将导致内存泄漏,解决方法如下:
  std::trl::shared_ptr<Widget> pw(new Widget);     //在独立语句内以智能指针存储Widget对象
      processWidget(pw, priority()); //这个调用肯定不存在内存泄漏
 
    请记住:
  • 以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。
posted @ 2015-02-05 15:13  wiessharling  阅读(354)  评论(0编辑  收藏  举报