Effective C++: 02构造、析构、赋值运算

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

         1:一个空类,如果你自己没声明,编译器就会为它声明(编译器版本的)一个copy构造函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。所有这些函数都是public且inline的。

         2:只有当这些函数被调用时,它们才会被编译器创建出来。

         3:编译器生成的default构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,比如用base classes和non-static成员变量的构造函数和析构函数。至于copy构造函数和copy assignment操作符,编译器创建的版本只是单纯的将来源对象的每一个non-static成员变量拷贝到目标对象。

         4:编译器创建的析构函数是个non-virtual,除非这个class的base class自身声明有virtual析构函数。

         5:以下情况下,编译器会拒绝为class生出operator=:

a、类中具有引用成员或者const 常量成员:

template<class T>
class NamedObject {
public:
  NamedObject(std::string& name, const T& value);
  ...                               
private:
  std::string& nameValue;           // this is a reference
  const T objectValue;              // this is const
};

 引用和常量必须在定义时进行初始化,不支持赋值操作。因此编译器拒绝为这样的类创建operator=函数;

如果你打算在一个包含reference成员或const成员的class内支持赋值操作,你必须自己定义copy assignment操作符。

 

b、如果某个base class将copy assignment操作符声明为private,编译器也拒绝为其derived class生成一个copy assignment操作符。

 

 

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

         1:如果不希望class支持复制初始化或者赋值操作,因为不定义copy构造函数和copy assignment操作符,编译器会自动创建一个,因此不定义这俩函数达不到这个目的。

         2:因为编译器创建的函数都是public的,为了阻止这些函数被创建出来,可以将copy构造函数或copy assignment操作符声明为private。这样便阻止了编译器创建这些函数,而且类的用户也无法调用它们。

         3:上面的做法还是有漏洞,因为类的成员函数和友元函数还是可以调用你的private函数。这种情况下,可以仅仅声明而不去定义它们。这种情况下,如果有成员函数或友元函数调用它们的话,将会产生一个连接错误。

         因此,将复制构造函数和赋值操作符声明为private且不去定义它们,当类的用户企图拷贝时,编译器会阻止他;如果在成员函数或友元函数中这么做,连接器会发出抱怨。

         4:将连接期错误移至编译期是可能的(而且那时好事,毕竟越早侦测出错误越好),只要定义一个Uncopyable类,并将自己的类继承该类就好:

class Uncopyable {
protected:                                   // allow construction
  Uncopyable() {}                            // and destruction of
  ~Uncopyable() {}                           // derived objects...

private:
  Uncopyable(const Uncopyable&);             // ...but prevent copying
  Uncopyable& operator=(const Uncopyable&);
};

          任何人(包括成员函数或友元函数)尝试拷贝Uncopyable类的派生类对象时,编译期便试着生成一个copy构造函数和一个copy assignment操作符。这些函数的编译器生成版会尝试调用其base class的对应函数,那些调用会被编译器拒绝,因为其base class的拷贝函数是private。

 

 

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

         1:当derived class对象经由一个base class指针删除,而该base class带着一个non-virtual析构函数,则其结果是未定义的。实际执行时通常发生的是对象的derived成分没被销毁,而其base class成分通常会被销毁,于是造成一个诡异的“局部销毁”对象。

         2:任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。

         3:如果class不含virtual函数,通常表示它并不愿意被用做一个base class。当class不企图被当作base class时,令其析构函数为virtual往往是个馒主意。

欲实现出virtual函数,对象必须携带某些信息,主要用来在在运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓vptr ( virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl ( virtual table );每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtb----编译器在其中寻找适当的函数指针。

因此,无端的将某个class的析构函数声明为virtual,会增加对象的体积(vptr)。因此许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数。

        4:标准string,以及所有STL容器如vector,list,set,map等等,都不virtual析构函数,因此,不应该将它们当做base class。

        5:如果抽象基类声明了virtual析构函数,则必须为它提供一份定义:

class AWOV { 
public:
  virtual ~AWOV() = 0;   // declare pure virtual destructor
};

AWOV::~AWOV() {}         // definition of pure virtual    dtor

         这是因为,派生类继承该抽象基类后,派生类对象销毁时,会首先调用派生类的析构函数,然后是基类的析构函数。因此编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用,所以必须提供一份定义,否则会连接错误。

 

 

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

         C++并不禁止析构函数吐出异常,但它不鼓励这么做。如果某个类的析构函数有可能抛出异常,则要么:抛出异常时直接调用abort退出程序;要么抛出异常时吞下异常,仅记录日志。

 

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

         1:不要再构造函数和析构函数中,调用virtual函数。

2:在base class构造期间,如果构造函数中调用了virtual函数,即使当前正在构造derived class(构造派生类对象时,需要首先构造其基类部分),virtual函数也是base class中的版本。也就是说;在base class构造期间,virtual函数不是virtual函数。

在derived class对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至(resolve to)base class,若使用运行期类型信息(runtime type information,例如dynamic_cast和typeid),也会把对象视为base class类型。

3:相同道理也适用于析构函数。一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入base class析构函数后对象就成为一个base class对象。

4:确定你的构造函数和析构函数都没有调用virtual函数,而它们调用的所有函数也都要服从这一约束。

 

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

         1:赋值时,可以将其写成连锁形式:x = y = z = 15;赋值采用右结合律,因此这个表达式等价于:x = ( y = ( z = 15 ) );

         2:为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议:

Widget& operator=(const Widget& rhs)   // return type is a reference to
{                                      // the current class
  ...
  return *this;                        // return the left-hand object
}

          3:这个协议不仅适用于标准的赋值形式,也适用于所有赋值相关运算,比如+=。

 

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

         1:“自我赋值”发生在对象被赋值给自己时,不要认定客户绝不会那么做,而且自我赋值并不总是那么可被一眼辨识出来,例如:a[i] = a[j];这条语句中,如果i和j相同,这便是自我赋值;再比如:*px = *py;如果px和py恰巧指向相同,这也是自我赋值。

         2:“自我赋值”时,可能会掉进“在停止使用资源之前意外释放了它”的陷阱。比如:

Widget& Widget::operator=(const Widget& rhs)
{
  delete pb;    
  pb = new Bitmap(*rhs.pb); 
  return *this;  
}

 这里的自我赋值问题是,operator=函数内的*this和rhs有可能是同一个对象。果真如此delete就不只是销毁当前对象的bitmap,它也销毁rhs的bitmap。

欲阻止这种错误,传统做法是藉由operator=最前面的一个“证同测试(identity test )”达到“自我赋值”的检验目的:

Widget& Widget::operator=(const Widget& rhs)
{
  if (this == &rhs) return *this;   // identity test: if a self-assignment,
                                    // do nothing
  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}

  

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

令人高兴的是,让operator=具备“异常安全性”往往自动获得“自我赋值安全”的回报。因此愈来愈多人对“自我赋值”的处理态度是倾向不去管它,把焦点放在实现“异常安全性”上。例如,我们只需注意在复制pb所指东西之前别删除pb:

Widget& Widget::operator=(const Widget& rhs)
{
  Bitmap *pOrig = pb;               // remember original pb
  pb = new Bitmap(*rhs.pb);       // make pb point to a copy of *pb
  delete pOrig;                     // delete the original pb
  return *this;
}

 现在,如果”new Bitmap”抛出异常,pb保持原状。即使没有证同测试,这段代码还是能够处理自我赋值,因为我们对原bitmap做了一份复件、删除原bitmap、然后指向新制造的那个复件。它或许不是处理“自我赋值”的最高效办法,但它行得通。

 

         4:在operator=函数内确保代码不但“异常安全”而且“自我赋值安全”的一个替代方案是,使用所谓的copy and swap技术。

它是一个常见而够好的operator=撰写办法:

Widget& Widget::operator=(const Widget& rhs)
{
  Widget temp(rhs);             // make a copy of rhs's data

  swap(temp);                   // swap *this's data with the copy's
  return *this;
}

 或者,可能更常见的是下面这种写法:

Widget& Widget::operator=(Widget rhs)
{
  swap(rhs);
  return *this;
}

 

 

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

1:如果自己写复制构造函数或赋值操作符而不使用编译器的版本,则需要注意的是:如果你为class添加一个成员变量,你必须同时修改复制构造函数和赋值操作符函数(你也需要修改class的所有构造函数,以及任何非标准形式的operator=(比如+=))。如果你忘记,编译器不太可能提醒你。

2:任何时候只要你承担起“为derived class撰写copying函数”的重责大任,必须很小心地也复制其base class成分。那些成分往往是private,所以你应该让derived class的copying函数调用相应的base class函数。

posted @ 2017-09-22 09:07  gqtc  阅读(150)  评论(0编辑  收藏  举报