Effective C++读书笔记 第二部分 构造/析构/赋值运算

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

本条款告诉程序员,编译器自动为你做了哪些事情。

用户定义一个empty class (空类),当C++ 处理过它之后,如果你自己没声明,编译器就会为它声明(编译器版本的)一个copy 构造函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default 构造函数。所有这些函数都是public 且inline 。举例,如果你写下:

class Empty{ };

这就好像你写下这样的代码:

class Empty { 
 public: 
  Empty() { ... } 
  Empty(const Empty& rhs) { ... ) 
  -Empty( ) { ... } 
  Empty& operator=(const Empty& rhs) { ... } 
};

需要注意的是,只要你显式地定义了一个构造函数(不管是有参构造函数还是无参构造函数),编译器将不再为你创建default构造函数。

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

请记住:

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

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

通常如果你不希望类支持某一特定技能,只要不说明对应函数就是了。但这个策略对拷贝构造函数和拷贝赋值操作符却不起作用。因为编译器会“自作多情”的声明它们,并在需要的时候调用它们。

由于编译器产生的函数都是public类型,因此可以将拷贝构造函数或拷贝赋值操作符声明为private。通过这个小“伎俩”可以阻止人们在外部调用它,但是类中的成员函数和友元函数还是可以调用private函数。解决方法可能是在一个专门为了阻止拷贝动作而设计的基类。(Boost提供的那个类名为noncopyable)。


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

(1) 定义一个公共基类,让所有独一无二的对象继承它,具体如下:

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

为阻止HomeForSale对象被拷贝,唯一需要做的就是继承Uncopyable:

class HomeForSale: private Uncopyable { 
  … 
};

这种方法带来的问题是,可能造成多重继承,这回导致很多麻烦。

(2) 创建一个宏,并将之放到每一个独一无二对象的private中,该宏为:

// 禁止使用拷贝构造函数和 operator= 赋值操作的宏 
// 应该类的 private: 中使用 
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \ 
TypeName(const TypeName&); \ 
void operator=(const TypeName&)

这种方法比第一种方法好,google C++编程规范中提倡使用该方法。


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

本条款阐述了一个程序员易犯的可能导致内存泄漏的错误,总结了两个程序员应遵守的编程原则:
(1)polymorphic (带多态性质的) base classes 应该声明一个virtual 析构函数。如果class 带有任何virtual 函数,它就应该拥有一个virtual 析构函数。这样,当用户delete基类指针时,会自动调用派生类的析构函数(而不是只调用基类的析构函数)。
(2)Classes 的设计目的如果不是作为base classes 使用,或不是为了具备多态性(polymorphically) ,就不该声明virtual 析构函数。这是因为,当用户将一个函数声明为virtual时,C++编译器会创建虚函数表以完成动态绑定功能,这将带来时间和空间上的花销。


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

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。C++不喜欢析构函数吐出异常。
     如果可能导致异常:

  • 如果抛出异常,就结束程序。(强迫结束程序是个合理选项,毕竟它可以阻止异常从析构函数传播出去。)
  • 捕获异常,但什么也不做。   
     如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。

请记住:

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。 
  条款09:决不让构造和析构过程中调用virtual函数

你不该在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你预想的结果。
     因为:基类的构造函数的执行要早于派生类的构造函数,当基类的构造函数执行时,派生类的成员变量尚未初始化。派生类的成员变量没初始化,即为指向虚函数表的指针vptr没被初始化又怎么去调用派生类的virtual函数呢?析构函数也相同,派生类先于基类被析构,又如何去找派生类相应的虚函数?
     唯一好的做法是:确定你的构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也不应该调用虚函数。
     解决的方法可能是:既然你无法使用虚函数从基类向下调用,那么我们可以使派生类将必要的构造信息向上传递至基类构造函数。即在派生类的构造函数的成员初始化列表中显示调用相应基类构造函数,并传入所需传递信息。
    请记住:

  • 在构造和析构函数期间不要调用虚函数,因为这类调用从不下降至派生类。
条款10: 令operator= 返回一个reference to *this

本条款告诉程序员一个默认的法则:为了实现“连锁赋值“,应令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。
条款11:在operator =中处理“自我赋值”

本条款讨论了几种编写复制构造函数的正确方法。给出的结论是:确保当对象自我赋值时operator= 有良好行为。其中技术包括比较”来源对象”和”目标对象”的地址、精心周到的语句顺序、以及 copy-and-swap。

(1) 复制构造函数的一种编写方式如下:

Widget& Widget::operator=(const Widget& rhs) 

  if (this == &rhs) return *this; //判断是否为同一个对象,如果是自我复制,直接返回 
  delete pb; 
  pb = new Bitmap(*rhs.pb); 
  return *this; 
}

这个版本存在异常方面的麻烦,即,如果”new Bitmap” 导致异常(不论是因为分配时内存不足或因为Bitmap 的copy构造函数抛出异常) , Widget 最终会持有一个指针指向被删除的Bitmap 。

(2) 让operator= 具备”异常安全性”往往自动获得”自我赋值安全”的回报。因此愈来愈多人对”自我赋值”的处理态度是倾向不去管它,把焦点放在实现”异常安全性” (exception safety) 上,即:

widget& Widget::operator=(const Widget& rhs) 

  Bitmap* pOrig = pb; 
  pb = new Bitmap(*rhs.pb); 
  delete pOrig; 
  return *this; 
}

如果”newBitmap” 抛出异常, pb (及其栖身的那个Widget) 保持原状。即使没有证同测试(identity test) ,这段代码还是能够处理自我赋值,但这种方法效率比较低。

(3) 另外一种比较高效的方法是:

class Widget { 
  …… 
  void swap(Widget& rhs); //交换*this 和rhs 的数据:详见条款29 
  …… 
};
 
Widget& Widget::operator=(Widget rhs) //rhs是被传对象的一份复件(副本),注意这里是pass by value. 

  swap(rhs); //将*this 的数据和复件/副本的数据互换 
  return *this; 
}

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

本条款阐释了复制对象时容易犯的一些错误,给出的教训是:

(1) Copying 函数应该确保复制”对象内的所有成员变量”及”所有base class 成分”。

(2) 不要尝试以某个copying 函数实现另一个copying 函数。应该将共同机能放进第三

个函数中,并由两个coping 函数共同调。换句话说,如果你发现你的copy 构造函数和copy assignment 操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private 而且常被命名为init。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值