《Effective C++》(二)

来源:互联网 发布:知乎 完颜亮 编辑:程序博客网 时间:2024/03/29 01:13

  • 构造析构赋值运算
    • 条款05了解C默默编写并调用了哪些函数
    • 条款06若不想使用编译器自动生成的函数就该明确拒绝
    • 条款07为多态基类声明virtual析构函数
    • 条款08别让异常逃离析构函数
    • 条款09绝不在构造和析构过程中调用virtual函数
    • 条款10令operator 返回一个 reference to this
    • 条款11在 operator 中处理自我赋值
    • 条款12复制对象时勿忘其每一个成分

2 构造/析构/赋值运算

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

1.C++编译器会自动给空类声明默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数
2.只有到上述函数被调用时,其才会被编译器创建,如下:

Empty e1;        //default构造函数 和 析构函数 会被编译器产出 Empty e2(e1);    // copy构造函数被产出  e2 = e1;         // copy assignment操作符 被产出  

3.编译器创建的default构造函数和析构函数主要是给编译器一个地方用来放置“幕后”代码,比如,调用 base classes 和 non-static 成员变量的构造函数和析构函数。(注意:编译器产出的析构函数是 non-virtual 的, 除非这个类的基类自身声明有virtual 析构函数)
4.编译器创建的copy构造函数 和 copy assignment 操作符只是单纯的将来源对象的每一个non-static 成员变量拷贝到目标对象
5.如果打算在一个“内含reference成员”的 class 内支持赋值操作,必须自己定义copy assignment操作符;面对“内含const成员”的classes,编译器的反应也一样(拒绝编译该行动作),因为更改const成员是不合法的;如果某个 base classes 将copy assignment操作符声明为 private,编译器将拒绝为其 derived classes 生成 copy assignment 操作符。
6.小结:编译器可以暗自为 class 创建 default构造函数、copy构造函数、copy assignment操作符 以及析构函数

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

1.将copy构造函数或copy assignment操作符声明为private,以阻止编译器暗自创建其专属版本,进而阻止了其他人调用它。这样,当用户企图拷贝对象时,编译器会报错,阻止这种行为
2.上述做法并不绝对安全,因为member函数和friend函数依旧可以调用这个private函数,所以,我们声明它们为private,然后不做定义,这种行为在C++中iostream程序中被应用很多。这样,如果不慎在member函数或者friend函数中误操作企图拷贝对象时,连接器会报错,阻止这种行为
3.这样做已经不错了,但是还可以更好,不必等到连接器,在编译器检测过程中就把,member函数或friend函数拷贝行为报错出来。 这种做法就是——建立一个专门阻止这种行为的 base class(基类),比如:

class Uncopyable  {  protected:      Uncopyable()  {}  // 允许派生对象构造和析构      ~Uncopyable()  {}  private:      Uncopyable( const Uncopyable& );// 阻止copying行为      Uncopyable& operator= ( const Uncopyable& );  };  class HomeForSale : private Uncopyable  {      ....  };  

这样,如果HomeForSale 的member函数或者friend函数 企图copy行为,编译器会尝试调用它基类的相应函数,但因为基类的相应函数为private,所以请求被拒绝,因此这种行为会在编译器上报错处理。
4.小结:为了阻止编译器 自动(暗自) 提供的机能,可将相应的成员函数声明为private 并且不予以实现,也可以使用上述的Uncopyable这样的基类来解决。

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

1.给基类一个virtual析构函数,以解决当派生类的对象经由一个基类指针被删除,而该基类带一个non-virtual析构函数,则其基类部分被销毁了,但是派生类部分没有被销毁,这种局部销毁的现象会导致资源泄露等问题。
2.当一个类内含至少一个 virtual 函数,才为其声明 virtual 析构函数;如果一个类没有virtual函数,说明这个类不想被用作为base class, 但仍给它安上一个virtual函数,这会使对象占用空间增加等问题。
3.要注意所有不带virtual析构函数的类,比如 所有的STL容器(vector、list、set等等),不能将其作为base class,然后经由基类指针删除派生类对象。
4.pure virtual 析构函数会导致 abstract class (抽象类)——也就是不能实体化的类,往往被当做base class来用,注意给纯虚析构函数提供一份定义,否则连接器会报错。

class AWOV  {  public:      virtual ~AWOV() = 0;  //声明纯虚析构函数}; AWOV::~AWOV()  {  } //定义纯虚析构函数

5.小结
<1>polymorphic(多态性的)基类应该声明一个 virtual 析构函数。如果 class 有任何一个virtual函数,它就应该有一个virtual析构函数。
<2>Classes 的设计目的如果不是作为 基类 使用,或不是为了具备多态性的,那就不该声明为virtual析构函数。

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

1.C++并不禁止析构函数吐出异常,但它不鼓励你这样做。
2.如果析构函数必须执行某个动作,但这个动作可能导致异常,这时可参考解决方案,将该动作转移到用户手上,同时设置了一个双保险,让程序更加安全。
3.小结:
<1>析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(让它们不能传播出去)或者 结束程序。
<2>如果客户需要对某个操作函数运行期间抛出异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

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

1.不能在构造函数和析构函数期间调用virtual函数。因为,在基类构造函数调用期间,virtual函数并非是virtual函数(base class构造函数的执行早于derived class构造函数,此时derived class的成员变量尚未初始化,如果此时调用derived class阶层的virtual函数可能使用未初始化的成员变量,进而导致不明确行为)。
2.在 derived class 对象的 base class构造期间的对象类型是 base class 而不是 derived class,不仅仅virtual函数的东西会被编译器指向 base class,如果使用运行期类型信息,也会把对象视作base class类型。再简单化一些:对象在derived class构造函数开始执行前不会成为一个 derived class 对象。
3.因为你无法使用 virtual 函数 从 base class 向下调用,在构造期间,可以令derived class将必要信息传给基类构造函数加以弥补。
4.小结:在构造和析构期间不要调用virtual函数,因为这类调用从不下降至 derived class( 比起当前执行构造函数和析构函数的那层)

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

1.为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。

class Widget  {  public:      ...      Widget& operator=( const Widget& rhs )// 返回类型是个reference,指向当前对象    {          ...          return* this;// 返回左侧对象      }      ...  };

2.这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关的运算,比如+=、-=、*= 等。但是,要注意这只是一个协定,并无强制性,如果不遵循它,代码一样可以通过编译。
3.小结:令 assignment(赋值) 操作符返回一个 reference to *this。

条款11:在 operator= 中处理“自我赋值”

1.自我赋值发生在对象被赋值给自己时。
2.利用“证同测试”检验自我赋值:

Widget& Widget::operator=(const Widget& rhs )  {      if( this == &rhs )    return *this;// 证同测试(identity test)      //如果赋值的源端和目的端是同一个对象,直接执行以下delete,删除pb的同时,rhs也被删除了    //所以下面第二行的赋值动作就会指向一个已经被删除的对象    delete pb;      pb = new Bitmap(*rhs.pb);      return *this;  }

3.上述方法仍存在“异常安全性”,不能防止new Bitmap导致的异常。可采用先赋值,再删除的方法:

Widget& Widget::operator=(const Widget& rhs)  {      Bitmap* pOrig = pb;// 记住原先的pb    pb = new Bitmap(*rhs.pb);// 赋值,令pb指向*pb的一个副本    delete pOrig;// 删除原先的pb    return *this;  }  

只需要注意在复制pb所指东西之前别删除原来的pb,先备份一下。现在如果new Bitmap抛出异常,pb保持原状,同时其也能处理自我赋值。这或许不是处理“自我赋值”最高效的办法,但它行得通。也可以将证同测试放在函数起始处,不过会增加成本。
4.copy and swap技术

class Widget  {      ...      void swap( Widget& rhs );// 交换*this和rhs的数据,在后面条款29会有详解      ...  };  Widget& Widget::operator=( const Widget& rhs )  {      Widget temp(rhs);// 为rhs数据制作一份副本    swap(temp);  //将*this数据和上述副本的数据交换    return *this;  }  

或者另一种形式:

Widget& Widget::operator= ( Widget rhs )//注意这里是pass by value,rhs是被传递对象的一份复件  {    swap(rhs);// 将*this的数据和复件的数据互换      return *this;} 

这种方法牺牲了 代码的清晰性,可读性略低。但将 copying 动作从函数本体移至函数参数构造阶段却可以让编译器产生更高效的代码。
5.小结:
<1>确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
<2>确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

1.copying 函数包括:copy构造函数 和 copy assignment操作符。编译器会在必要时自动为你生成copying函数,将被拷对象的所有non-static 成员变量都做一份拷贝。
2.当自己实现 copying函数并且其执行的是“局部拷贝”时,编译器不会提醒错误。一旦在派生类中使用,则有可能漏掉对基类中的成员变量的复制。
3.任何时候,只要你承担起为 derived class撰写 copying函数的责任。必须很小心的复制它基类的成分。但这些成分往往是 private ,所以无法直接访问,这就需要让派生类的copying函数调用相应的基类函数
4.不能因为避免代码重复而“令copy assignment操作符 调用 copy构造函数”或“令 copy构造函数 调用 copy assignment操作符”。通常,如果怕这两者代码重复,你可以通过建立一个新的private成员函数,把同代码写在里面,然后copy assignment操作符和copy构造函数调用它。
5.小结:
<1>Copying函数应该确保复制“对象内的所有成员变量”及“所有 base class 成分”
<2>不要尝试以某个 copying函数实现 另一个 copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

原创粉丝点击