Effective C++笔记(2)
本文最后更新于 716 天前,其中的信息可能已经有所发展或是发生改变。

构造/析构/赋值运算

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

C++会为类生成默认的关键函数,但是这些函数只有在被需要调用的时候才会生成,因此这4种功能不是保证都存在的。

class Empty {};             
​
//假定以下功能都会被调用,此定义等价于:
class Empty{
 public:
   Empty(){....}                                //构造函数
   Empty(const Empty& empty){.....}             //拷贝构造函数
   ~Empty(){....}                               //析构函数
   Empty& operator=(const Empty& empty){....}   //拷贝赋值运算符
};

对于赋值运算符,只有当代码合法而且有意义时,编译器才会自动生成,否则拒绝编译。解决方法是自己定义赋值运算符。

条款06: 明确告诉编译器你不需要的某些自动生成的函数

所有编译器生成的函数都是public。通过将不需要的函数声明为private可以阻止编译器自动生成。

class House{
 public:
  ....
 private:
   House(const House&);             //只有声明,不要定义
   House& operator=(const House&);  //因为不会被用到,就不必再给参数想一个名字了
};

更好的解决方案:当一个父类将拷贝函数声明为私有时,编译器会拒绝为它的子类生成拷贝函数。因此我们可以专门使用一个父类,在其中声明拷贝操作为私有,并让我们的类继承自它。

class Uncopyable{
 protected:             //允许derived对象构造和解析
   Uncopyable();
   ~Uncopyable();
 private:
   Uncopyable(const Uncopyable&);             //将拷贝函数转移至此
   Uncopyable& operator=(const Uncopyable&);
};
​
class House : private Uncopyable{...};         //私有继承

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

析构函数(destructor)用来释放对象所占用的资源。当对象的使用周期结束后,例如当某对象的范围(scope)结束时,或者是动态分配的对象被delete关键字解除资源时,对象的析构函数会被自动调用,对象所占用的资源就会被释放。以及像第五章讲过的,假如在你的类中不声明析构函数,编译器也会为你自动生成一个。

多态(polymorphism)则是C++面向对象的基本思想之一,即抽象(abstraction),封装(encapsulation),继承(inheritance),多态(polymorphism)。如果我们希望仅仅通过基类指针就能操作它所有的子类对象,那这就是多态。

当你通过基类指针使用子类,使用完毕后却只从基类删除。同时这个基类的析构函数并不是虚函数(virtual),也就是不允许子类有自己版本的析构函数,这样就只能删除子类中基类的部分,而子类衍生出来的变量和函数所占用的资源并没有被释放,这就造成了这个对象只被释放了一部分资源的现象,依然会导致内存泄漏。

class TimeKeeper{                              //计时器类,用来当做基类
  public:
    TimeKeeper();                              //这是构造函数
    ~TimeKeeper();                             //这是析构函数
  ......
};

class AtomicClock : public TimeKeeper{...};   //原子钟是一种计时器
class WaterClock : public TimeKeeper{...};    //水钟也是一种计时器

TimeKeeper* getTimeKeeper(){...}              //用来返回一个动态分配的基类对象
TimeKeeper* ptk = getTimeKeeper();            
.....                                         //使用这个指针操作它的子类
delete ptk;                                   //(不建议这么写)使用完毕,释放资源

解决方法:给基类一个虚的析构函数,这样子类就允许拥有自己的析构函数,就能保证被占用的所有资源都会被释放。

class TimeKeeper{
  public:
    virtual ~TimeKeeper();
      // 此时删除derived class对象就会销毁整个对象
  ....
};

TimeKeeper* ptk = getTiemKeeper();
......
delete ptk;  // 现在行为正确

虚函数是用来在运行时(runtime),自动把编译时未知的对象,比如用户输入的对象,和它所对应的函数绑定起来并调用。当一个类包含虚函数时,编译器会给这个类添加一个隐藏变量,即虚函数表指针(virtual table pointer),用来指向一个包含函数指针的数组,即虚函数表(virtual table)。当一个虚函数被调用时,具体调用哪个函数就可以从这个表里找了。

如果class不含virtual函数,通常表示它并不意图被用作一个base class,此时令其析构函数为virtual往往是个馊主意(会增加其对象大小)。因此只有当class内含至少一个virtual函数,才为它生命virtual析构函数。

对于抽象类(abstract class),抽象类是包含至少一个纯虚函数的类(pure virtual function),而且它们不能被实例化,只能通过指针来操作,是纯粹被用来当做多态的基类的。

相比具体类(concrete class),虽然它们都可以通过父类指针来操作子类,但抽象类有更高一层的抽象,从设计的角度上能更好概括某些类的共同特性。

多态的基类需要有虚析构函数,抽象类又需要有纯虚函数,那么在抽象类中就要把析构函数声明为纯虚函数:

class A{
  public:
    virtual ~A() = 0;  //声明纯虚函数
};

同时注意,当在继承层级中某一类的析构函数被调用时,它下一级类的析构函数会被随后调用,最后一直到基类的析构函数,因此作为析构函数调用的终点,要保证有一个定义,否则链接器会报错。

A::~A(){}         //基类的析构函数要有一个空的定义

析构函数的运作方式是:最深层派生(most derived)的那个class起析构函数最先被调用,然后是其每一个base class的析构函数被调用。

*某些类不是被用来当做基类的,比如std::string和STL,或者某些不是用来实现多态的基类,比如上一章的Uncopyable,就不需要虚的析构函数。

条款08: 阻止异常离开析构函数

一个例子:

class DBConnection{                   //某用来建立数据库连接的类
  public:
    ...
    static DBConnection create();     //建立一个连接
    void close();                     //关闭一个连接,假设可以抛出异常
};

为确保客户不忘在DBConnection对象上调用close(),可以创建一个用来管理DBConnection资源的class,并在其析构函数中调用close

class DBConn{                         //创建一个资源管理类来提供更好的用户接口
  public:
    ....
    ~DBConn{ db.close(); ]            //终止时自动调用关闭连接的方法
  private:
    DBConnection db;
};

// 用户可这样写
  {
    DBConn dbc(DBConnection::create());//创建一个DBConn类的对象
    ...                                                                 //使用该对象
  }                                                                     //对象dbc被释放资源

但是上述场景有一个问题,若对象dbc的析构函数调用close()时抛出异常该怎么办?

  1. 主动关闭
    使用std::abort()主动关闭程序,而非任由程序崩溃。
   DBConn::~DBConn(){
     try{ 
       db.close();
     }catch(...){
       //记录访问历史
       std::abort();
     }
   }
  1. 重新设计借口
    把调用close的责任从DBConn析构函数移到DBConn客户手上。但在DBConn中仍有“双保险”调用。
   class DBConn{
     public:
       ...
       ~DBConn();
       void close();        //[1]当要关闭连接时,手动调用此函数
     private:
       DBConnection db;
       bool closed = false; //显示连接是否被手动关闭
   };

   void DBConn::close(){    //[1]
     db.close();
     closed = true;
   }

   DBConn::~DBcon(){
     if(!closed)
       try{
         db.close();
       }catch(...){
         //记录访问历史
         //消化异常或者主动关闭
       }
   }

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

假设有一个class继承体系,用来记录股票交易:

class Transaction{
 public:
  Transaction();
  virtual void logTransaction() const = 0;
  ...
};

Transaction::Transaction(){
  ...
  logTransaction();
}

class BuyTransaction : public Transaction {
public:
  virtual void logTransaction() const;
  ...
};

class SellTransaction : public Transaction {
public:
  virtual void logTransaction() const;
  ...
};

此时定义一个对象:

BuyTransaction b;

此时bass class构造函数先被调用,但base class构造期间virtual函数不会下降到derived class阶层。derived class 对象的base class构造期间,对象的类型是base class而不是derived class(可以看作在base class构造期间,virtual函数不是virtual函数)

如果一定想要对象在初始化的时候完成某些任务。那就需要在父类Transaction中,把虚函数logTransaction去掉virtual关键字,变成普通的函数,然后在子类构造过程中,把某些信息传递到父类的构造函数中。

class Transaction{
  public:
    explicit Transaction(const std::string& info);
      //explicit关键字用来防止隐式转换
    void logTransaction(const std::string& info) const; 
    //增加一个传递参数,就可以从子类获得信息了
    ...
};

Transaction::Transaction(const std::string& info){
  ...
  logTransaction(info);  
}

现在就可以在子类对象中如下定义构造函数了,这样就能把子类的信息传递到父类中,让父类构造函数去完成子类构造函数想做的事:

class BuyTransaction : public Transaction{
  public:
    BuyTransaction(...) : Transaction(createLog(...)) { ...}
    ...
  private:
    static std::string createLog(...);
};

这里createLog()就是一个辅助函数(helper function),用来将某函数的一部分功能封装成另一个小函数,减少代码的复杂性,使代码更加可读。此外,因为这是一个子类的私有成员,父类构造函数被调用时不能保证它被初始化,所以使用static关键字可以避免意外使用了未初始化的成员数据。

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

C++的赋值符号=(assignment operator)具有链式赋值(Chained assignment)特性:

int x, y, z;
x = y = z = 15; // x = (y = (z = 15)));

对于自定义类,应该遵守如下两个规则:

  1. 返回类型是reference;
  2. 返回*this给左变量。
class Widget{
public:
  ...
  Widget& operator=(const Widget& rhs){ // 返回reference并指向当前对象
    ...
    return *this;                       // 返回*this给左变量
  }
  ...
}

以上适用于所有赋值相关运算,如:

class Widget{
public:
  ...
  Widget& operator+=(const Widget& rhs){// -=, *=, etc.
    ...
    return *this;                       // 返回*this给左变量
  }
  ...
  Widget* operator=(int rhs){           // 适用。即使参数类型不符合规定
    ...
    return *this;
  }
}

条款11: 在operator=中处理「自我赋值」

自我赋值(self-assignment):

class Widget{};
Widget w;
w = w;          // self-assignment

//other situations (aliasing)
a[i] = a[j];       // when i == j
*px = *py;         // both are pointers to the same object

在继承体系中,即使两个对象被声明为不同类型,但仍可能发生该情况:

class Base{...};
class Derived : public Base{...};               //同一个继承层级中

void doSomething(const Base& rb, Derived* pd);  //rb与pd就可能指向同一个对象

如果使用条款13条款14运用对象来管理资源则或许是“自我赋值安全的”(self-assignment-safe)。

但如果手动管理资源则可能出现问题。假设用一个class保存一个指针指向一块动态分配的位图(bitmap):

class Bitmap{...}
class Widget{
  ...
  private:
    Bitmap *bp;
};

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

如若传入的参数rhs为自身,会导致delete语句把*this自己的资源释放掉,同时也释放掉了rhs的资源,最后返回的*this包含了一个损坏的数据,你不能访问不能修改,甚至不能通过delete来为其释放资源,等于说这段空间就凭空消失了,所以这段代码不是自赋值安全的。

方案1:检查传入的参数rhs是否为*this
但仍不是异常安全的(exception-safe)

Widget& Widget::operator=(const Widget& rhs){
  if(this == &rhs)             //先做一个身份检测
    return *this;

  delete pb;                   //如果不是自己,再执行如下操作
  pb = new Bitmap(*this.pb);   //存在的问题:若new抛出异常(如内存不足)
  return *this;                //则返回的*this为损坏的数据
}

方案2:重新排列语句
既对自赋值安全,对异常也是安全的。如果现在new的这行抛出了异常,指针pb也不会被提前删除。

Widget& Widget::operator=(const Widget& rhs){
  Bitmap *pOrigin = pb;            //先保存一个原pb的备份
  pb = new Bitmap(*this.pb);       //拷贝过来rhs的pb
  delete pOrigin;                  //只删除备份
  return *this;
}

方案3:先拷贝再调换(copy and swap技术)

class Widget{
   ...
  void swap(Widget& rhs);   //把rhs和*this的数据成员互相调换
  ...
};

Widget& widget::operator=(const Widget& rhs){
  Widget temp(rhs);         //拷贝rhs
  swap(temp);               //将*this的数据与这个拷贝的数据调换
  return *this;
}

另一种形式,巧妙利用了C++传值(pass-by-value)会自动生成一份本地拷贝的特性:

Widget& Widget::operator=(Widget rhs){ // 此时rhs是被传对象的副本
    swap(rhs);
    return *this;
}

条款12: 对复制对象的所有成员变量都拷贝

C++有两种拷贝函数(copying function):拷贝构造函数(copy constructor)和拷贝赋值操作符(copy assignment operator)。如果在自己定义的类中不声明这些拷贝函数,编译器会自动生成。如果我们声明了自己的拷贝函数,程序将会执行我们自己的拷贝函数。

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;       //返回*this,见条款10
}

当上述代码新增了一个数据成员时,如果没改变拷贝函数,则只能得到一个部分拷贝(partial copy)的对象。同样的情况也发生在继承体系中:

class PriorityCustomer : public Customer{
  public:
    ...
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& rhs);
  private;
    int priority;
};

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
  :priority(rhs.prority){           //使用初始化列表来构造该类的数据成员
  logCall("PriorityCustomer copy constructor");  
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
  logCall("PriorityCustomer copy assignment operator");
  priority = rhs.priority;          //拷贝该类的数据成员
  return *this;
}

上面的代码并没有拷贝基类部分的数据成员。基类的数据成员被设定为默认值。

⚠️base class的数据复制过程必须小心(数据往往是private,无法直接访问)。
所以应该直接让derived class的拷贝函数调用相应的base class函数。

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
  : priority(rhs.prority), Customer(rhs){     //要把基类部分也添加进初始化列表
  logCall("PriorityCustomer copy constructor"); 
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
  logCall("PriorityCustomer copy assignment operator");
  Customer::operator=(rhs);         //要使用基类的拷贝操作符
  priority = rhs.priority; 
  return *this;
}

C++的这两种拷贝函数有相似的功能和代码,那么我们能不能避免代码重复,让其中一个拷贝函数调用另一个呢?答案是不能

使用拷贝赋值操作符调用拷贝构造函数,或者使用拷贝构造函数调用拷贝赋值操作符,都是没有意义的。拷贝赋值操作符适用于已经构造好的对象,而拷贝构造函数适用于还没有构造好的对象,所以这种做法在语义上是错误的。

消除代码重复需要做的是:建立一个新的成员函数给两者调用

这样的函数往往是private且常被命名为init

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 。转载请注明出处!
您可以通过 RSS 订阅本站文章更新。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇