• 条款05:了解C++默默编写并调用了哪些函数
  • 条款06:若不想使用编译器自动生成的函数,就该明确拒绝
  • 条款07:为多态基类声明virtual析构函数
    • 1. 带多态性质的父类,应该声明一个virtual析构函数
    • 2. 类的设计目的不作为父类使用,不该声明virtual函数
    • 3. 不要继承一个没有声明虚析构函数的类
    • 4. 纯虚函数
  • 条款08:别让异常逃离析构函数
  • 条款09:绝不在构造和析构过程中调用virtual函数
  • 条款10:令operator= 返回一个reference to *this
  • 条款11:在operator= 中处理"自我赋值"
  • 条款12:复制对象时勿忘其每一个成分

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

  考虑如下一个类:

class Empty{};

  这个类其实等价于:

class Empty {public:Empty();Empty(const Empty& other);~Empty();Empty& operator=(const Empty& other);
}

  也就是说,当我们编写一个空类时,编译器会在我们调用这些函数时,自动为我们创建出来。那这些函数对应我们平时做的哪些操作呢:

{Empty a1;       //调用Empty(),默认构造函数Empty a2(a1);  //调用Empty(const Empty& other),拷贝构造函数a2 = a1;        //调用Empty& operator=(const Empty& other),赋值函数
}                   //作用域结束,调用~Empty(),析构函数

  所以在写一个类的时候,如果没有自定义这几个函数的话,编译器会自动为我们创建,这些默认的函数也有自己默认的操作内容。

  1. 当类中有const或引用的成员变量时,编译器拒绝自动生成赋值函数
  2. 如果父类中的赋值函数被声明为private,那么编译器拒绝为子类生成一个赋值函数。

总结

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

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

  在项目开发中,我们会经常编写一些复杂的类、或者单例,或者某些类从业务逻辑上是不允许被拷贝和赋值的。那么这个时候,如果不去明确拒绝,那么正如条款05所描述,编译器会自动为我们生成这些函数。所以如果不想这样做的话,我们需要明确拒绝。

  那么如何明确拒绝呢,一般有两种方法:

  1. 类中声明拷贝构造函数和赋值函数为private,且不实现它们.
class HomeForSale {public:...
private:HomeForSale(const HomeForSale&);HomeForSale& operator=(const HomeForSale&);
}

  如上在头文件中仅仅声明它即可,不用去实现。主要原因如下:

  1. 使用private修饰为了防止外部进行调用
  2. 不去实现,是为了防止在内部的成员函数或者friend函数内调用。
  1. 如果这种类过多,那么需要对每一个类做这样的操作,这样会比较麻烦。有一种方式是编写一个父类,让拥有这种属性的类继承父类,从而达到它们的实例不允许拷贝的效果。
class Uncopyable {protected:Uncopyable();         //不可实例化~Uncopyable();
private:Uncopyable(const Uncopyable&);Uncopyable& operator=(const Uncopyable&);
}

  这样我们只需要HomeForSale 类继承Uncopyable即可:

class HomeForSale : public Uncopyable {...
}

  这种方式比较常用,boost中有noncopyable提供了该功能:

#ifndef BOOST_NONCOPYABLE_HPP_INCLUDED
#define BOOST_NONCOPYABLE_HPP_INCLUDED  namespace boost {  //  Private copy constructor and copy assignment ensure classes derived from
//  class noncopyable cannot be copied.  //  Contributed by Dave Abrahams  namespace noncopyable_  // protection from unintended ADL
{  class noncopyable  {  protected:  noncopyable() {}  ~noncopyable() {}  private:  // emphasize the following members are private  noncopyable( const noncopyable& );  const noncopyable& operator=( const noncopyable& );  };
}  typedef noncopyable_::noncopyable noncopyable;  } // namespace boost  #endif  // BOOST_NONCOPYABLE_HPP_INCLUDED

总结

为驳回编译器自动提供的功能,可以将相应的成员函数声明为private并且不予以实现。使用像Uncopyable这样的父类也是一种做法。

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

1. 带多态性质的父类,应该声明一个virtual析构函数

   看如下的一个例子:

class A {A();~A();
};class B : public A {B();~B();
}A *a = new B();
delete a;

  如上类B是类A的派生类,在实例化的时候使用A类型的指针指向B生成的对象,那么在析构的时候会出现什么情况呢。

  答案是类B的析构函数没有被调用。这是为什么呢?这是因为C++明确指出,当派生类对象经由一个父类指针删除,而该父类带着一个非虚析构函数,其结果是未定义的。也就是派生类对象析构函数未被调用。

  从另外一个角度思考也是合理的,程序在调用析构函数的时候,此时该指针是A类的,但实际指向B的实例。那么它首先会调用覆盖析构函数的派生类B的析构函数,但是A的析构函数未声明为虚函数,那么就不存在覆盖它的析构函数了,所以派生类B的析构函数未能执行。

  如果B的析构函数未被执行,那就意味这,对象未全部销毁。如果B类中申请了一些其他类的实例,那么显然的,这会出现内存泄漏的问题。

2. 类的设计目的不作为父类使用,不该声明virtual函数

  如果一个类的设计目的不是用来作为基类的,那么我们最好不应该声明虚函数(包括虚析构函数)。

  这是因为当我们声明虚函数的时候,申请出来的对象中含有一个vptr,它指向了一个虚函数表(vpbl),它存储了类中每一个虚函数的函数指针地址。所以在我们每申请一次这个对象就会多出一个占4字节(32位)的vptr。这无疑是增加了内存的开销。

3. 不要继承一个没有声明虚析构函数的类

  在实际开发中,我们可能会写出这样的代码:

class MyString : public std::string {}

  这样的写法是危险的,因为std::string类并没有声明自己析构函数为virtual。如果发生1所描述的情况,那么就会出现问题。

4. 纯虚函数

  如果你的基类的虚函数本身什么都不做,可以将其声明为纯虚函数:

class AWOV {public:virtual ~AWOV() = 0;
}

  它的好处在于,我们知道先构造的后析构,后构造的先析构。那么编译器在先析构派生类的时候,如果发现派生类没有定义虚析构函数,那么链接器就会发出错误信息。这有助于我们提前知道自己所编写的代码的问题所在。

总结

  1. polymorphic(带多态性质的)base classes 应该声明一个virtual析构函数,如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
  2. Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。

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

  考虑如下代码:

class Widget {public:
~Widget() {    // 假定这个析构函数可能会吐出异常
};
void doSomething() {std::vector<Widget> v;
}

   这里如果在析构函数中对vector中的第一个元素析构时,抛出异常,那么就会导致程序结束执行或者出现不明确行为。
  对于这种情况,一般情况采用如下方式:

class DBConn {public:void close() {db.close();closed = true;}~DBConn() {if(!closed) {try {db.close();}catch {...} {}}}
private:DBConnection db;bool         closed;
};

  如上代码所示,它是一个在析构函数中关闭数据库链接的操作,为了防止db.close()函数在析构函数中发生异常,可以定义一个外部接口,供用户自己去关闭数据库连接,将异常的情况交给用户进行处理。

  但是如果用户忘记调用close,在析构函数中为了保险期间,需要try catch将异常吞掉,防止出现程序异常退出的情况。

  这种双保险时解决异常逃离析构函数的方法。
总结

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

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

  且看如下实例:

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;
};

  这是一个股票买进卖出的系统,不同的操作都会记录自己的日志信息。在Transaction父类的构造函数中调用了LogTransaction() 虚成员方法。这种情况会出现什么问题呢。

链接器会报错,无法找到LogTransaction定义的版本。

  我们知道当实例化BuyTransaction对象的时候,程序先调用父类Transaction的构造方法,此时BuyTransaction对象并没有被初始化,这个时候调用了LogTransaction虚方法并不是BuyTransaction的实现版本。就算是BuyTransaction的实现版本,那么BuyTransaction的构造方法没有执行,也就是类中的成员变量未初始化,这个时候BuyTransaction方法中如果使用了未初始化的成员变量,同样会使程序运行出现问题。

  同样的在析构函数中,如果在父类调用了虚函数,也会出现问题。因为父类的析构函数执行顺序在派生类之后。如果在父类的析构函数中调用了虚函数,此时,派生类中的成员变量已经变成未定义状态,这样同样会造成程序执行到不可知的方向。

  有时候我们也会这样做:

class Transaction {public:Transaction() {Init();}virtual void LogTransaction() const = 0;...
private:void Init() {LogTransaction();}
};

  这种情况和上面的一样会有问题,因为其本质上还是在构造函数中出现了对虚函数的调用。

  所以我们在coding的时候一定要注意构造函数中是否有对虚函数的调用,这样的做法使相当危险的。
  那么有什么办法解决这种问题呢。

class Transaction {public:explicit Transaction(const std::string& logInfo);void LogTransaction(const std::string& logInfo) const;...
};Transaction::Transaction(const std::string& logInfo) {...LogTransaction(logInfo);
}class BuyTransaction : public Transaction {public:BuyTransaction(parameters) : Transaction(createLogString(parameters)){}
private:static std::string createLogString(parameters);
};
  1. 声明LogTransaction为非虚函数。
  2. 将变化的部分作为LogTransaction的形参传递给父类。
  3. 使用static函数让初始化的实参是已经定义的。

  使用用static方法,同样是因为在传递参数的时候,该实参如果作为派生类的成员变量传递的话,此成员变量并未被初始化,同样会出问题。所以使用createLogString静态方法,确保传递的实参是已经被定义的。

  当然以上只是举个例子,在实际开发中我们可能不会这样去写。

总结

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

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

  该条款说明的是一种大家都遵循的协议,即我们在class中实现operator=、+=、-=、*=等操作符的时候,最好使它的返回类型是一个reference to *this。

class widget {public:widget& operator= (const widget& other) {...return *this;}
}

总结

令赋值(assignment)操作符返回一个reference to *this。

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

  该条款使用最经典的String拷贝面试题来解释会更好一些。这个在《剑指Offer》中也提到过的面试题。

  关于这个面试题,基本的解法如下:

EMyString& EMyString::operator = (const EMyString& str) {if (this == &str) {return *this;}delete[] m_pData;m_pData = nullptr;m_nLen = str.Len();m_pData = new char[m_nLen];memcpy(m_pData, str.m_pData, m_nLen);return *this;
}

  那么我们看到的:

 if (this == &str) {return *this;}

  就是条款中所说的,在operator =中处理自我赋值。因为如果不处理的话,很可能在后面delete[] m_pData,delete的是自己,这会出现很严重的问题。

  所以这个条款在我们的实际开发中要谨记。

  条款中也提到了另一个风险:

 delete[] m_pData;m_pData = nullptr;m_nLen = str.Len();m_pData = new char[m_nLen];

  m_pData = new char[m_nLen];这句可能出现的风险是,当内存不足时,可能申请不到这块内存。但是此时我们已经将this对象中的数据释放掉了,此时等于破坏掉了原始的this对象,这就会出现异常安全。所以有更好的实现方式如下:

const EMyString& EMyString::operator=(const EMyString& str) {if (this != &str) {EMyString strTmp(str);char* tempData = strTmp.m_pData;strTmp.m_pData = m_pData;              //作用域之后调用析构释放m_pData = tempData;                       //tempData是在EMyString构造函数中申请的内存}return *this;
}

  这里使用交换的方式,先申请临时的strTmp实例,然后通过str拷贝构造出来的对象内容与this对象内容进行交换。当出了if作用域后,它会释放原本this对象中的m_pData。这样当EMyString拷贝构造函数中如果申请不到内存的话,也不会破坏原来this对象的内容。

总结

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

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

  该条款表明的是,在实现拷贝构造函数和赋值函数时,我们需要考虑到类中的每一个成员是否被拷贝了,根据业务逻辑,我们需要使用的时浅拷贝还是深拷贝。

  同时,当该类时子类的时候,需要考虑到父类的成员变量是否被拷贝到。

总结

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

Effective C++ 之《构造/析构/赋值运算》相关推荐

  1. 读书笔记 Effective C++: 02 构造析构赋值运算

    条款05:了解C++默认编写并调用的哪些函数 编译器会为class创建: 1. default构造函数(前提是:没有定义任何构造函数): 如果已经声明了一个构造函数,编译器就不会再创建default构 ...

  2. Effective C++ --2 构造/析构/赋值运算

    上一部分Effective C++ --1 让自己习惯C++ 5. 了解C++默认编写并调用哪些函数 (1)   编译器暗自为类创建默认构造函数.拷贝构造函数.拷贝赋值函数和析构函数. (2)   拷 ...

  3. 构造/析构/赋值运算--龙之介《Effective C++》实验室

    条款5:了解C++默默编写并调用哪些函数 编译器可以暗自为class创建default构造函数,copy构造函数,copy assignment操作符 但是c++拒绝编译那一行赋值动作.你不会自动co ...

  4. Effective C++ -- 构造析构赋值运算

    05.了解C++默默编写并调用哪些函数 编译产生的析构函数时non-virtual,除非这个类的基类析构函数为virtual 成员变量中有引用和const成员时,无法自己主动生成copy assign ...

  5. Effective C++学习笔记——构造/析构/拷贝运算

    条款9:决不再构造和析构过程中调用virtual函数,包括通过函数间接调用virtual函数. 应用:想在一个继承体系中,一个derived class被创建时,某个调用(例如生成相应的日志log)会 ...

  6. 声明及赋值_重述《Effective C++》二——构造、析构、赋值运算

    关于本专栏,请看为什么写这个专栏.如果你想阅读带有条款目录的文章,欢迎访问我的主页. 构造和析构一方面是对象的诞生和终结:另一方面,它们也意味着资源的开辟和归还.这些操作犯错误会导致深远的后果--你需 ...

  7. 《深度探索C++对象模型》读书笔记第五章:构造析构拷贝语意学

    <深度探索C++对象模型>读书笔记第五章:构造析构拷贝语意学 对于abstract base class(抽象基类),class中的data member应该被初始化,并且只在constr ...

  8. 【从零学习OpenCV 4】Mat类构造与赋值

    本文首发于"小白学视觉"微信公众号,欢迎关注公众号 本文作者为小白,版权归人民邮电出版社所有,禁止转载,侵权必究! 经过几个月的努力,小白终于完成了市面上第一本OpenCV 4入门 ...

  9. C++学习笔记-----在重载的赋值运算函数中调用拷贝构造函数

    类的拷贝构造函数与赋值运算不同,拷贝构造函数是对这个类进行初始化的过程,而赋值是删除原有的东西,赋予它新的东西. 但是二者在实现上是互通的. template<class T> graph ...

最新文章

  1. IT规划中的技术体系架构
  2. AMD发布“全球单核性能最快”CPU,参数碾压英特尔,性能提升47%
  3. leetcode 743. Network Delay Time | 743. 网络延迟时间(邻接矩阵,Dijkstra 算法)
  4. 基于 FPGA 的数字抢答器设计
  5. MFC界面库BCGControlBar v25.3新版亮点:Dialogs和Forms
  6. BZOJ - 2186 欧拉函数
  7. mcldownload文件夹_《我的世界》中国版游戏空间精简教程 多余文件删除方法
  8. Selenium 显示等待和隐式等待
  9. PID控制(三)(位置式和增量式PID)
  10. 配置ouster雷达过程
  11. 居民身份证号码的编码规则
  12. 最全最新的的Java核心知识点整理!!! 【推荐】
  13. 项目进度控制的主要任务是什么?
  14. js中出现错误:Uncaught TypeError: date.getDay is not a function
  15. 使用adb从手机拉取apk包
  16. 计算机慢怎么解决6,解决电脑运行慢卡顿问题的六种方法
  17. 基于Spark实现电影点评系统用户行为分析—RDD篇(一)
  18. python打包总出错,解决Pyinstaller打包软件失败的一个坑
  19. 使用python爬取猫眼电影、房王、股吧论坛、百度翻译、有道翻译、高德天气、华夏基金、扇贝单词、糗事百科(糗事百科)
  20. CSS字体、文本属性、CSS 盒模型

热门文章

  1. 推荐一款免费的Markdown编辑器,GitHub斩获22.8k Star
  2. Vue中读取md文件
  3. 第16届JOLT大奖获奖书籍名单最新揭晓
  4. 诚之和:《鱿鱼游戏》普通人与恶的距离,究竟有多远
  5. 神策军丨那个在神策跨城转岗的小伙子,现在怎么样了?
  6. mysql idataparameter_[转]另一个SqlParameterCollection 中已包含 SqlParameter[解决方案]
  7. 物流企业的类型有哪些?物流企业分类
  8. python课程介绍-少儿Python编程课程的具体介绍
  9. 剩余电流继电器ASJ20-LD1A自恢复式过欠压保护器
  10. Vue3使用element-plusUI解决菜单高度自动自适应的问题,使用CSS3的vh单位