Effective C++ (札记) : 条款05 - 条款10

Effective C++ (笔记) : 条款05 -- 条款10

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

编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。

只有这些函数需要(被调用)时,它们才会被编译器创建出来。在编译器产生的复制构造函数和赋值运算符执行的都是浅拷贝。当数据成员是引用或者常量的时候,编译器不知道该怎么处理,两手一摊,无能为力。

当某个基类将copy assignment操作符声明为私有的,编译器将拒绝为派生类生成copy assignment操作符,因为它无权(也不能)处理基类部分。

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

在一些类中不允许拷贝和赋值,因为这是不允许和无意义的。因此可以将拷贝和赋值声明为private,这样当客户端调用的时候编译器会阻挠它。同时,成员函数和友元函数能调用,但是在链接的时候会报错。因此这种方法能明确拒绝我们不想编译器自动生成的函数。

将链接期错误提前到编译期是个不错的注意,因为这会更早发现问题,不会在联调的时候才暴漏问题。在一个基类中将这些函数声明为私有的,那么当前类(即派生类)便不再生成这些函数。达到了目的。这些“编译期生成版”会尝试调用其base class的对应兄弟,那些调用会被编译期拒绝,因为其base class的拷贝函数时private

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

基类指针指向一个派生类对象,而这个对象却经由基类指针被删除,而目前的基类有一个non-virtual析构函数。想想会发生什么?

结果将是未定义的,实际执行时通常发生的是对象的derived成分没有被销毁。基类部分能够销毁,这是形成资源泄露和调试时间浪费的一个原因。

解决这个问题的方式很简单,只要为基类创建一个virtual析构函数即可。

如果class不含virtual函数,通常表示它并不愿意被用作一个base class。欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。无端的将所有classes的析构函数声明为virtual,也是错误的。心得是:只有当class内含至少一个virtual函数,才为它声明virtual函数。

带多态性质的基类应该声明一个virtual析构函数,如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。 
Classes的设计目的如果不是作为base classes使用,或者不是为了多态性质,就不应该声明virtual析构函数。

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

如果有一个类负责数据库的连接,那么将释放连接的操作放在析构函数中是一个不错的主意。前提是释放操作不会抛出异常,如果释放操作可能抛出异常那么我们应该做的更过以避免这种情况。

  1. DB::~DBConn()
  2. {
  3. try{ db.close(); }
  4. catch(...){
  5. 制作运转记录,记下对close的调用失败
  6. }

通常情况下,将异常吞掉是个坏主意,因为它因为它压制了“某些动作失败”的重要信息。

一个较佳的策略是重新设计DBConn的接口,使客户有机会对可能出现的问题做出反应。比如可以添加一个close函数,供客户显示调用,赋予客户一个得以处理“因该操作而引发的异常”的机会。让客户有时间相应可能出现的错误。在析构函数中,我们再检测是否成功关闭,如果客户不显示关闭连接,那么也就是他们认为不会发生关闭错误,那么就忽略它,让析构函数去释放就好,即使有错误发生,那么也是无关紧要的。

  1. class DBConn{
  2. public:
  3. void close()
  4. {
  5. db.close();
  6. closed = true;
  7. }
  8. ~DBConn()
  9. {
  10. if(!closed){
  11. try{
  12. db.close();
  13. }
  14. catch(...){
  15. 制作运转记录,记下对close的调用失败
  16. }
  17. }
  18. }
  19. };
  20. private:
  21. DBConnection db;
  22. bool closed;
  23. };

析构函数绝对不要突出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该能够捕获任何catch(...)异常,然后吞下(不传播)它们或结束程序。 
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而不是在析构函数中)执行该操作。

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

派生类对象内的基类部分会在派生类自身成分被构造之前先构造妥当。在构造函数内调用虚函数将是基类中的版本,不是派生类的版本---即使目前即将建立的对象类型是派生类。是的,在基类构造期间虚函数绝不会下降到派生类的阶层。非正式的算法比较传神:在基类构造期间,虚函数不是虚函数。

相同的情况也适用于析构函数,一旦派生类析构函数开始执行,对象内的派生类成员变量将呈现未定义的值,所以C++视它们仿佛不再存在。

有时候发现这个隐藏的问题比较晦涩难懂,因为析构函数可能没有直接调用虚函数,而是通过其他函数间接调用虚函数,程序能运行,但是结果却不是你想要的。要调试发现这样的问题将是耗神费力的。唯一能做的就是避免这样的事情发生。

如果有这样的业务需求又应该怎么办呢?一种做法就是在基类中将函数改为non-virtual,然后要求派生类构造函数传递必要信息(参数)给基类的构造函数。

换句话说,由于你无法使用虚函数从基类向下调用,在构造期间,你可以藉由"令派生类将必要的构造信息向上传递至基类的构造函数"替换之而加以弥补。

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

为了实现“连锁赋值”这是必须的。

+=*=-=中也要使用这个协议。

要使重载的运算符表现出内置类型和使用习惯上的一致性。