今天是周六早上,但很不幸待会儿还是要去公司,本月kpi还剩一些工作要做,这个月计划的Effective C++学习,也基本完成了,最后一章节模板相关那部分还看不太懂,就大概过了一遍。现在是收尾总结阶段了。这本书的准则在这里我想尽量精简化,本篇主要是第二章节的内容:构造、析构和赋值。
5. 了解C++默默编写和调用了哪些函数
创建class时,构造函数和析构函数是非常重要的。构造函数是在创建对象时调用的,涉及到相关成员变量的初始化工作。析构函数则是在销毁对象是调用的,不完备的析构函数很容易造成堆内存泄漏。我们先看构造函数,如果不显式定义构造函数,编译器会默认自动创建构造函数,但一旦我们定义了某个构造函数,编译器将不会为你生成其他构造函数(编译器这里认为你只需要你声明的构造函数),若有需要,使用=default
可以告诉编译器生成对应的默认函数。
同样,拷贝构造函数和拷贝赋值运算符如果没有声明,编译器也会自动创建一个默认版本。且这些函数都是public和inline的。注意,默认生成的拷贝构造和拷贝赋值往往会执行浅拷贝,这在有些场景下是危险的。
此外,如若没有声明析构函数,编译器也会生成析构函数,这个析构函数中会依次调用每个成员变量的析构函数。PS:一个对象析构两次会导致行为未定义的错误。
6. 若不想使用编译器生成的函数,就要明确拒绝
上一条准则说明了编译器会自动生成一些成员函数,但当我们需要禁用一些自动生成的函数接口时,我们可以将其声明为private的,例如私有的operator=可以禁止拷贝赋值。在C++11以上版本,我们还可以使用=delete
来直接禁止对应方法的调用。
7. 为多态基类声明virtual析构函数
这个准则是和多态调用一致的,当我们将一个子类对象的指针赋给一个父类指针对象时,我们可以通过虚函数表调用到真正的子类方法,而在析构的时候也是一样,只有当基类的析构函数时虚函数的时候,才能先正确调用到子类的析构函数,然后子类的析构函数会自动调用基类的析构函数,从而完成对象资源的完全释放,不会导致内存泄漏。PS:如果一个class带有任何virtual函数,那么就是希望该class在未来会被继承,就有可能将一个子类对象指针赋值给父类指针,对应的virtual函数会执行多态调用,析构函数亦同。反之,不期望作为基类的class,不应该声明virtual析构函数。
8. 别让异常逃离析构函数
如果在析构过程中出现了异常,将其往上层、即调用方传播是不明智的,因为只会导致析构没有正常结束,很有可能导致内存泄漏。我们应当尽量将可能抛出异常的操作放到成员函数中,并对异常进行处理
9. 绝对不在构造和析构过程中调用virtual函数
因为虚函数是为了让子类选择是否重写的成员函数,同时为该函数提供一个缺省实现;(纯虚函数是强制其继承体系中可实例化的子类必须实现)。所以,在构造函数中调用一个虚函数,这个虚函数只能映射到当前层级的class中,所以可能无法按照预期调用到子类实现的虚函数上去。(另一个角度,构造还未完成,所以还找不到虚函数表,所以无法完成多态调用)。
析构函数中调用虚函数也是十分危险的,假如你在子类析构函数开始调用,子类的成员便会变得未定义,执行完子类析构后,开始进入父类析构函数,这个析构函数中如果调用virtual函数,只会映射到父类的对应virtual函数上,并不会映射到你最初调用的那个子类的成员函数上,这种时候很容易发生纯虚函数被调用的报错,导致程序直接crash掉。
10. 令operator=返回一个reference to *this
我们写的每行代码,如果有很多操作符,按照优先级通过右结合律去匹配操作数。比如连续赋值,所以赋值操作也需要返回一个值去和次右操作数匹配。为实现连续赋值,赋值操作必须返回一个reference指向赋值操作符左侧实参。所以,对于class中的操作符重载函数,也需要返回一个reference to *this。这非必须,但很必要,也符合预期。
11. 在operator=中处理“自我赋值”
这个也不是很重要的准则,只是为了确保客户代码异常调用引发不可预料的结果。目的主要还是要保证对象拷贝时的异常安全性,所以在实现每个成员拷贝前,通常加一个“证同测试”是明智之举。此外,利用copy-and-swap技术,可以保证异常安全性,先声明拷贝赋值传参方式为by value,那么可以构造一个右操作数的副本,然后将其与左操作数交换内容,最后返回左操作数的引用,右操作数是临时对象会自动销毁。
12. 复制对象时勿忘其每一部分
OOP中我们封装的类一般要实现两个copy函数,一个是拷贝构造,一个是拷贝赋值。首先,要明确拷贝操作的深浅,尤其是指针的拷贝,还有父类的成员拷贝也需要显式调用。最后,不能用拷贝赋值函数调用拷贝构造函数,反之也不行,行为无意义。这两个函数执行的目的基本一致,所以可能会存在重复代码,所以可将重复代码拎出到一个单独的成员函数来调用,以降低代码复杂度。
小结:以上。