Effective c++ 第二章(条款05-12)

5 篇文章 0 订阅

第二章:构造/析构/赋值运算

构造/析构/赋值运算

条款05:了解c++默默编写并调用哪些函数(Know what functions c++ silently writes and calls)

我们写一个空class

class Empty{};    //空类

经过c++处理之后

class Empty{
public:
   Empty() {...}                             //default构造函数
   Empty(const Empty& rhs) {...}             //copy构造函数
   ~Empty() {...}                            //析构函数
   Empty& operator=(const Empty& rhs) {...}  //copy assignment操作符
};

编译器会自动为class创建出上述函数(当这些函数被调用时),所以我们写出的这两个class是等效的。

Empty e1;          //default构造函数
                   //析构函数
Empty e2(e1);      //copy构造函数
e2=e1;             //copy assignment操作符

但当我们声明了一个构造函数后,编译器就不再为class创建default构造函数

template<typename T>
class NameObject{
public:
   NameObject(const char* name,const T& value);
   NameObject(const std::string& name,const T& value);
   //由于没有声明copy构造函数和copy assignment操作符,编译器会创建这些函数
   ...
private:
   std::string nameValue;
   T objectValue;
};
NameObject<int> no1("Smallest Prime Number",2);
NameObject<int> no2(no1);            //调用copy构造函数->编译器生成的

nameValue的类型是string,而标准string有个copy构造函数,no2.nameValue就调用copy构造函数以no1.nameValue为实参,另一个NameObject::objectValue类型为int(内置类型),所以no2.objectValue会“拷贝no1.objectValue内的每一个bits”完成初始化。

编译器所生成的copy assignment操作符,其行为基本和copy构造函数类似
当base class把copy assignment操作符声明为private,编译器会拒绝为derived classes生成copy assignment操作符

请记住:
-编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符、以及析构函数

条款06:若不想使用编译器自动生成的函数,就该明确拒绝(Explicitly disallow the use of compiler-generated functions you do not want)

编译器产出的函数是public,当我们不需要copy构造函数、copy assignment操作符时,我们可以将它们声明为private,从而使得编译器不在声明这些函数,并且阻止别人调用它们。但是这对成员函数和友元函数而言却不还是安全!!
c++ Prime Plus: 类的友元函数是非成员函数,其访问权限和成员函数相同(P392),访问私有类成员的唯一方法是使用类方法,c++使用友元函数来避开这种限制。(P421)

class HomeForSale{
public:
   ...
private:
   ...
   HomeForSale(const HomeForSale&);         //只有声明
   HomeForSale& operator=(const HomeForSale&);
};

当member函数或friend函数调用private函数时,连接器会发出错误

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

class HomeForSale:private Uncopyable{
   ...
};

当member函数或friend函数拷贝HomeForSale对象,编译器会拒绝,因为调用的是base class 的拷贝函数为private。

请记住:
-为驳回编译器自动提供的机能,可将对应的成员函数声明为private并且不与实现。或者使用Uncopyable这样的base class做法

条款07:为多基态类声明virtual析构函数(Declare destructors virtual in polymorphic base classes)

class TimeKeeper{
public:
   TimeKeeper();
   ~TimeKeeper();       //析构函数
};
class AtomicClock:public TimeKeeper{...};   //derived class
class WaterClock: public TimeKeeper{...};
class WristWatch: public TimeKeeper{...};
TimeKeeper* ptk=getTimeKeeper();
...
delete ptk;

getTimeKeeper返回的指针指向一个derived class对象,而删除时却是被一个base class指针删除,当base class是一个non-virtual析构函数时,对象的derived成分没有被删除,且derived的析构函数也没有执行,但是其base class成分却被删除了,造成资源泄露!!!

class TimeKeeper{
public:
   TimeKeeper();
   virtual ~TimeKeeper(); //virtual 析构函数
};

base class带有virtual析构函数后,先调用derived对象的析构函数销毁derived对象,然后在调用base对象的析构函数。如果class不含virtual函数,最好不要把它用作一个base class,同时class不想成为base class,析构函数也最好不要为virtual。

当class内含至少一个virtual函数,才为它声明virtual析构函数

现在我们使用标准string

class SpeciaString:public std::string{     //继承标准string
   ...
};
SpeciaString* pss=new SpeciaString("Impending Doom");
std::string* ps;
...
ps=pss;
...
delete ps;    //*ps的SpeciaString资源会泄漏,SpeciaString析构函数没有被调用!

因为std::string是个non-virtual析构函数,同样所以STL容器(vector、list、set、trl::unordered_map等等)都是不带virtual析构函数的class,所以最好不要继承标准容器或者其他带non-virtaul析构函数的class

现在来看一看pure virtual析构函数:pure virtual函数会导致class成为一个abstract class也就是不能被实体化的class(c++ Prime Plus: 当类声明中包含纯虚函数时,则不能创建该类的对象,包含纯虚函数的类只用作基类(P509)),但是我们有时需要拥有abstract class,这时可以采用pure virtual析构函数

class AWOV{
publicvirtual ~AWOV = 0//声明pure virtual析构函数
};
AWOV::~AWOV() {}         //pure virtual析构函数的定义

必须为pure virtual析构函数提供一份定义!!当AWOV的derived class的析构函数被调用时,会创建一个对~AWOV的调用。

请记住:
-polymorphic(带多态性质的)base class 应该声明一个virtual 析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
-classes的设计目的如果不是作为base class使用,或不是为具备多态性,就不要声明virtual析构函数

条款08:别让异常逃离析构函数(Prevent exceptions from leaving destructors)

析构函数可能会发生异常

现在我们假设用一个class连接数据库

class DBConnection{
public:
   ...
   static DBConnection creat();  
   void close();                 //关闭联机
};
//管理DBConnection
class DBConn{
public:
   ...
   ~DBConn(){
      db.close();
   }
private:
   DBConnection db;
};

现在只要我们建立DBConnection对象交给DBConn管理,DBConn对象被销毁时自动调用close(),但是如果调用失败则会吐出异常,我们应该避免这个问题发生,这里有两个方法来避免。
1.如果close抛出异常就结束程序:

DBConn::~DBConn(){
  try { db.close(); }
  catch(...) {
     ...   //制作运转记录,记下对close的调用失败
     std::abort();
  }
}

2.吞下调用close而发生的异常:

DBConn::~DBConn(){
  try { db.close(); }
  catch(...) {
     ...   //制作运转记录,记下对close的调用失败
  }
}

这些方法都无法对"导致close抛出异常"的情况做出反应。我们可以改进DBConn接口,让客户对问题做出反应

class DBConn{
public:
   ...
   void close(){          //供客户使用的函数
      db.close();
      closed = true;
   }
   ~DBConn(){
      if(!closed){
          try { db.close(); }
          catch(...) {
              ...   //制作运转记录,记下对close的调用失败
          }
      }
   }
private:
   DBConnection db;
   bool closed;
};

把调用close的责任转移到客户手上,如果客户没有调用close(),当发生异常时客户就没有立场抱怨了,因为他们放弃第一时间处理问题。

请记住:
-析构函数绝对不要吐出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们或结束程序
-如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提出供一个普通函数(不在析构函数中)执行该操作

条款09:绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruction)

我们看一下这个代码

class Transaction{
public:
   Transaction();
   virtual void logTransaction() const =0;  //纯虚函数,用作基类(条款07)
   ...
};
Transaction::Transaction(){   //base class构造函数定义
   ...
   logTransaction();          //调用virtual函数
}
class BuyTransaction: public Transaction{  //derived class
public:
   virtual void logTransaction() cosnt;
   ...
};

现在执行这条语句

BuyTransaction b;

BuyTransaction构造函数被调用,但是base class Transaction构造函数先被调用,而Transaction构造函数内却调用了virtual函数,此时调用logTransaction是在Transaction内,即使目前建立的对象是BuyTransaction即**derived class对象在base class构造期间,对象的类型是base class而不是derived class**

同理也适用于对于析构函数
对于解决方法有如下:
第一个解决方法是错误的,这里看看背后的原因

class Transaction{
public:
   Transaction() { init(); }                //调用non-virtual函数
   virtual void logTransaction() const =0;  //纯虚函数,用作基类(条款07)
   ...
privatevoid init(){
      ...
      logTransaction();                     //调用virtual函数
   }
};

这个方法不会引发编译器和连接器的抱怨,但会建立一个derived class对象时调用错误版本的logTransaction。。因此,唯一的做法是构造函数和析构函数都没有调用virtual函数(对象被创建和销毁期间),而且它们调用的所有函数也都服从同一约束
另一种方法是将virtual改为non-virtual
parameters->形参)

class Transaction{
public:
   explicit Transaction(const std::string& logInfo);
   void logTransaction(const std::string& logInfo) const;  //non-virtual函数
   ...
};
Transaction::Transaction(){  
   ...
   logTransaction(logInfo);          //调用non-virtual函数
}
class BuyTransaction: public Transaction{  //derived class
public:
   BuyTransaction(parameters)
   :Transaction(creatLogString (parameters)){...}  //将信息传给base class构造函数
   ...
private:
   static std::string creatLogString (parameters);
};

我们无法使用virtual函数从base classes向下调用,但在构造期间,我们可以令derived classes将构造信息向上传递至base class构造函数

请记住:
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)

条款10:令operator=返回一个 reference to *this(Have assignment operators return a reference to *this)

这篇比较短简单看下
有如下赋值:

int x,y,z;
x=y=z=15;

赋值采用右结合

x=(y=(z=15));    //z=15,然后y=z,然后x=y

连锁赋值中赋值操作符必须返回一个reference指向操作符左侧实参

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

同样,这也适用于+=、-=、*=等等(最好遵守,这份协议被内置类型和标准程序库共同遵守
请记住:
-令赋值操作符返回一个reference to *this

条款11:在operator=中处理“自我赋值”(Handle assignment to self in operator=)

防止指向同一份资源的两个对象在被删除时掉入"在停止使用资源之前意外释放了它"

class Bitmap {...};
class Widget{
public:
   ...
private:
   Bitmap* pb;          //指向一个从heap分配而得到的对象
};

第一个写法:*this和rhs可能指向同一个对象

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

第二个写法:"new Bitmap"可能导致异常,Widget会持有一个指针指向一块被删除的Bitmap

Widget& Widget::operator=(const Widget& rhs){
   if(this=&rhs)              //证同测试
      return *this;     
   delete pb;
   pb=new Bitmap(*rhs.pb);    //"new Bitmap"可能导致异常
   return *this;
}

第三个写法:通过 “异常安全性” 获得 "自我赋值安全"

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

第四个写法:通过copy and swap技术

class Widget{
   ...
   void swap(Widget& rhs);
   ...
};
Widget& Widget::operator=(const Widget& rhs){   //pass by reference
   Widget temp(rhs);         //将rhs数据制作一份副本
   swap(temp);               //将*this数据和上述副本数据交换
   return *this;
}
/*
Widget& Widget::operator=(Widget rhs){ //rhs是被传对象的一个副本(pass by value)
   swap(temp);                         //将*this数据和上述副本数据交换
   return *this;
}
*/

请记住:
-确保当对象自我赋值时operator=有良好行为。其中技术包括“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
-确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确

条款12:复制对象时勿忘其每一个成分(Copy all parts of an object)

copy函数:copy构造函数和copy assignment操作符

void logCall(const std::string funcName);
class Customer{
public:
   ...
   Customer(const Customer& rhs);
   Customer& operator=(const Customer& rhs);
   ...
private:
   std::string name;
};
Customer::Customer(const Customer& rhs):name(rhs.name){
   logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs){
   logCall("Customer copy assignment operator");
   name=rhs.name;
   return *this;
}
class PriorityCustomer:public Customer{
public:
   PriorityCustomer(const PriorityCustomer& rhs);
   PriorityCustomer& operator=(const PriorityCustomer& rhs);
   ...
private:
   int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):priority(rhs.priority){
   logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer""operator=(const PriorityCustomer& rhs){
   logCall("PriorityCustomer copy assignment operator");
   priority=rhs.priority;
   return *this;
}

PriorityCustomer的copying函数只复制了PriorityCustomer声明的成员变量,而没有复制所继承的Customer成员变量

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):Customer(rhs),priority(rhs.priority){  //调用base class的copy构造函数
   logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer""operator=(const PriorityCustomer& rhs){
   logCall("PriorityCustomer copy assignment operator");
   Customer::operator=(rhs);      //调用base class成分进行赋值
   priority=rhs.priority;
   return *this;
}

让derived class的copying函数调用相应的base class函数

copy assignment操作符调用copy构造函数不合理,且copy构造函数调用copy assignment操作符也没有意义
可以创建一个名为init函数在private中给copying函数调用

请记住:
-Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
-不要尝试以某一个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值