2. 构造、析构、赋值运算

5. 了解c++默默编写并调用那些函数

  1. 在C++处理之后,empty class(空类)不再是个空类,如果没有声明,编译器就会为它声明一个copy构造函数,一个copy assignment 操作符和一个析构函数,所有这些函数都是public且inline的。
  2. 如果声明了一个构造函数,编译器不再为它创建default构造函数
  3. 在一个“内含reference成员”的class内支持赋值操作,必须自己定义copy assignment 操作符
  4. 在面对“内含const成员”,编译器处理与上相同
  5. 若某个base class将copy assignment操作符声明为private,编译器拒绝为其derived class生成一个copy assignment

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

  1. 将成员函数声明为private而且故意不实现他们,被用在c++ iostream中阻止copying行为,copy构造函数和copy assignment操作符都被声明为private且没有定义。

  1. 当客户企图拷贝对象时,连接器报错
  2. 为阻止对象被考不,唯一要做的时继承(私有继承) class不再声明拷贝构造函数和copy assignment运算符
1
class HomeForSale: private Uncopyable{}; 
  1. 为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。

7. 为多态基类声明virtual析构函数

  1. 当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,i结果未有定义,实际执行时通常发生的是对象的derived成分没有被销毁。造成一个诡异的局部销毁现象对象。
  2. 给base class一个virtual析构函数,此后删除derived class对象
1
2
3
4
5
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper();
};
  1. 其他的virtual函数,目的是允许derived class的实现得以客制化
  2. 如果class不含virtual函数,通常表示它并不意图被用作一个base class。若令其析构函数为virtual,是个馊主意
  3. virtual函数的实现细节不重要,重要的是class内含virtual函数,其对象体积会增加;在32位的计算机体系结构中将占用64位(为了存放两个ints)至96位(两个ints加上vptr)
  4. 心得:只有当class内含至少一个virtual函数,才为它声明virtual析构函数
  5. 即使class完全不带virtual函数,被非虚析构函数“咬伤”还是存在。
    1. 例如:标准string不含任何virtual函数,但有时候程序员会错误地把它当作base class
  6. 对任何不带virtual析构函数的class,包括所有STL容器,均禁止派生
  7. 有时class带一个pure virtual析构函数,可能颇为顺利、
    1. 纯虚函数导致抽象类,不能被实例化,不能创建对象
  8. 析构函数的运作方式是,最深层派生的class的析构函数最先被调用,然后其每一个base class的析构函数被调用
  9. 给base class一个virtual析构函数,只适用于带多态性质的base class 上,这种基类的设计目的是为了用来通过基类接口处理派生类对象。
  10. 并非所有的基类设计目的都是为了多态,例如标准string和STL容器都不能被设计作为基类使用,更别提多态了
  11. 某些类设计目的是作为基类使用,但不是为了多态用途,不需要virtual析构函数

8. 别让异常逃离析构函数

  1. C++并不禁止析构函数吐出异常,但它不鼓励你这样做,C++析构函数吐出异常,程序可能过早结束或者出现不明确行为
  2. 一个管理数据库管理的类

  1. 如果close抛出异常就结束程序,通常通过调用abort完成,如果程序遭遇一个”于析构期间发生的错误“后无法继续执行,”强迫结束程序“是个合理选项,它可以阻止异常从析构函数传播出去,调用abort可以抢先制”不明确行为“于死地

  1. 吞下因调用close而发生的异常
    1. 一般来说,将异常吞掉是个坏主意,因为它压制了”某些动作失败“的重要信息
    2. 然而有时吞下异常也比负担”草率结束程序“或”不明确行为带来的风险“好,是一个可行方案,程序能够继续可靠的执行
  2. 重新设计DEConn接口,使其客户有机会对可能出现的问题作出反应。
    1. 把调用close的责任从DEConn析构函数手上移到DBConn客户手上
  3. tips
    1. 析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或者结束程序。
    2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作

9. 绝不在构造和析构过程中调用virtual函数

  1. 在基类构造期间virtual函数绝对不会下降到派生类阶层,取而代之的是,对象的作为就像隶属基类型一样,非正式的说法:在基类构造期间,virtual函数不是virtual函数
  2. 基类构造函数的执行更早于派生类构造函数,当基类构造函数执行时,派生类的成员变量尚未初始化,如果此期间调用的virtual函数下降到派生类阶层,派生类的函数必然取到local成员变量,而那些成员变量尚未初始化。
  3. 在派生类对象的基类构造期间,对下那个的类型是基类而不是派生类。不只virtual函数会被编译器解析至基类,若使用运行期类型信息(如dynamic_cast 和typeid),也会把对象视为基类类型
  4. 相同道理也适用于析构函数,一旦派生类析构函数开始执行,对象内的派生类成员变量便呈现未定义值,c++视它们仿佛不在存在。进入基类析构函数后对象就成为一个基类对象,而c++的任何部分包括虚函数,dynamic_casts也这么看待它

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

  1. 连锁赋值,赋值操作符必须返回一个reference指向操作符的左侧实参
1
2
3
4
Widget & operator()=(const Widget& rhs)
{
return *this;//返回左侧对象
}
  1. 适用于所有赋值相关运算
  2. 这只是一个协议,并无强制性,但被所有内置类型和STL提供的类型所共同遵守

11. 令operator=中处理”自我赋值“

  1. 自我赋值发生在对象被赋值给自己时,看起来有点蠢,但合法
  2. operator=实现代码

  1. 潜在的自我赋值
1
2
a[i] = a[j]; // i==j
*px = *py; //px和py指向同一个东西
  1. 这是别名带来的结果,所谓别名就是有一个以上的方法指称某对象。一般而言如果某段代码操作pointers或者references而它们被用来“指向多个相同类型的对象”,就需考虑这些对象是否为同一个。实际上两个对象只要来自同一个继承体系,它们甚至不需声明为相同类型就可鞥造成别名,因为一个基类的引用或指针可以指向一个派生类对象
1
2
3
class Base{};
class Derived:public Base{};
void doSomething(const Base& rb, Derived* pd); //rb 和*pd可能其实是同一对象
  1. 传统做法是由operator=最前面的一个证同测试达到自我赋值的检验目的
1
2
3
4
5
6
7
8
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;

delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
  1. 前一版operator=不仅不具备“自我赋值安全性”,也不具备“异常安全性”,存在异常方面的麻烦。如果“new Bitmap”导致异常,Widget最终会持有一个指针指向一块被删除的bitmap,这样的指针有害,你无法安全的删除他们,以及无法安全的读取他们
  2. 让operator=具备“异常安全性”会获得“自我赋值安全”的回报,因此对自我赋值的处理态度是倾向不管它,焦点放在实现异常安全性上。本条款只要你注意“许多时候一群精心安排的语句就可以导出一场安全(以及自我赋值安全的代码,再复制pb所指东西之前别删除pb
1
2
3
4
5
6
7
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
  1. 这个主题的另一个变奏曲是利用以下事实:
    1. 某class的copy assignment操作符可能被声明为“以by value方式接受实参”
    2. 以by value方式传递东西会造成一份副本
1
2
3
4
5
Widget& Widget::operator=(Widget rhs)  	//rhs是被传对象的一份副本
{ //这里是pass by value
swap(rhs); //将*this的数据和副本的数据进行互换
return *this;
}
3. 将copying动作从函数本体内移到函数参数构造阶段可令编译器有时生成更高效的代码
  1. 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
  2. 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

  1. 面向对象系统会将对象的内部封装起来,只留两个函数负责对象拷贝(复制),那便是带着适切名称的copy构造函数和copy assignment操作符,成为copying函数
  2. 如果为class添加一个成员变量,必须同时修改copying函数
1
2
3
4
5
6
7
8
9
10
11
class PriorityCustomer: public Customer{};
PriorityCustomer::PriorityCustomoer(cosnt PriorityCustomer& rhs):priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.prioroty;
return *this;
}
  1. 在进行类继承时,copying函数复制了成员变量副本,但每个PriorityCustomer还内含它所继承的Customer成员变量副本,而那些变量却未被复制,PriorityCustomer的copy构造函数并没有指定实参传给其基类构造函数,(也就是说它在他的成员初值列中没有提到Customer。)因此PriorityCustomer对象的Customer成分会被不带实参之Customer狗仔函数(即default构造函数——必定有一个否则无法通过编译)初始化。default构造函数将针对name和lastTransaction执行缺省的初始化动作。
  2. 当编写一个copying函数
    1. 复制所有local成员变量
    2. 调用所有base classes内适当的copying函数
  3. 令copy assignment操作符调用copy构造函数是不合理的,因为这就像试图构造一个已经存在的对象。
  4. 令copy构造函数调用copy assignment操作符同样无意义。构造函数用来初始化新对象,而assignment操作符只施行于已初始化对象身上。
  5. 如果copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private而且命名为init。这个策略可以安全消除copy构造函数和copy assignment操作符之间的代码重复
  6. copying函数应该确保复制“对象内的所有成员变量”及“所有基类成分”
  7. 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用

2. 构造、析构、赋值运算
http://binbo-zappy.github.io/2024/11/27/effective-cpp/2-构造、析构、赋值运算/
作者
Binbo
发布于
2024年11月27日
许可协议