《Effective C++》阅读总结(二):类的构造、析构和赋值

人工智能74

第二章 类的构造、析构和赋值

今天是周六早上,但很不幸待会儿还是要去公司,本月kpi还剩一些工作要做,这个月计划的Effective C++学习,也基本完成了,最后一章节模板相关那部分还看不太懂,就大概过了一遍。现在是收尾总结阶段了。这本书的准则在这里我想尽量精简化,本篇主要是第二章节的内容:构造、析构和赋值。

创建class时,构造函数和析构函数是非常重要的。构造函数是在创建对象时调用的,涉及到相关成员变量的初始化工作。析构函数则是在销毁对象是调用的,不完备的析构函数很容易造成堆内存泄漏。我们先看构造函数,如果不显式定义构造函数,编译器会默认自动创建构造函数,但一旦我们定义了某个构造函数,编译器将不会为你生成其他构造函数(编译器这里认为你只需要你声明的构造函数),若有需要,使用 =default可以告诉编译器生成对应的默认函数。
同样,拷贝构造函数和拷贝赋值运算符如果没有声明,编译器也会自动创建一个默认版本。且这些函数都是public和inline的。注意,默认生成的拷贝构造和拷贝赋值往往会执行浅拷贝,这在有些场景下是危险的。
此外,如若没有声明析构函数,编译器也会生成析构函数,这个析构函数中会依次调用每个成员变量的析构函数。PS:一个对象析构两次会导致行为未定义的错误。

上一条准则说明了编译器会自动生成一些成员函数,但当我们需要禁用一些自动生成的函数接口时,我们可以将其声明为private的,例如私有的operator=可以禁止拷贝赋值。在C++11以上版本,我们还可以使用 =delete来直接禁止对应方法的调用。

这个准则是和多态调用一致的,当我们将一个子类对象的指针赋给一个父类指针对象时,我们可以通过虚函数表调用到真正的子类方法,而在析构的时候也是一样,只有当基类的析构函数时虚函数的时候,才能先正确调用到子类的析构函数,然后子类的析构函数会自动调用基类的析构函数,从而完成对象资源的完全释放,不会导致内存泄漏。PS:如果一个class带有任何virtual函数,那么就是希望该class在未来会被继承,就有可能将一个子类对象指针赋值给父类指针,对应的virtual函数会执行多态调用,析构函数亦同。反之,不期望作为基类的class,不应该声明virtual析构函数。

如果在析构过程中发生异常,将其传播到更高级别,即调用方是不明智的,因为这只会导致销毁无法正常结束,这很可能导致内存泄漏。我们应该尽量把可能抛出异常的操作放到成员函数中,并处理异常。

[En]

If an exception occurs during destructing, it is unwise to propagate it to the upper level, that is, the caller, because it will only cause the destruction not to end properly, which is likely to lead to a memory leak. We should try our best to put the operations that may throw exceptions into the member functions and handle the exceptions.

因为虚函数是为了让子类选择是否重写的成员函数,同时为该函数提供一个缺省实现;(纯虚函数是强制其继承体系中可实例化的子类必须实现)。所以,在构造函数中调用一个虚函数,这个虚函数只能映射到当前层级的class中,所以可能无法按照预期调用到子类实现的虚函数上去。(另一个角度,构造还未完成,所以还找不到虚函数表,所以无法完成多态调用)。
析构函数中调用虚函数也是十分危险的,假如你在子类析构函数开始调用,子类的成员便会变得未定义,执行完子类析构后,开始进入父类析构函数,这个析构函数中如果调用virtual函数,只会映射到父类的对应virtual函数上,并不会映射到你最初调用的那个子类的成员函数上,这种时候很容易发生纯虚函数被调用的报错,导致程序直接crash掉。

我们写的每行代码,如果有很多操作符,按照优先级通过右结合律去匹配操作数。比如连续赋值,所以赋值操作也需要返回一个值去和次右操作数匹配。为实现连续赋值,赋值操作必须返回一个reference指向赋值操作符左侧实参。所以,对于class中的操作符重载函数,也需要返回一个reference to *this。这非必须,但很必要,也符合预期。

这个也不是很重要的准则,只是为了确保客户代码异常调用引发不可预料的结果。目的主要还是要保证对象拷贝时的异常安全性,所以在实现每个成员拷贝前,通常加一个"证同测试"是明智之举。此外,利用copy-and-swap技术,可以保证异常安全性,先声明拷贝赋值传参方式为by value,那么可以构造一个右操作数的副本,然后将其与左操作数交换内容,最后返回左操作数的引用,右操作数是临时对象会自动销毁。

OOP中我们封装的类一般要实现两个copy函数,一个是拷贝构造,一个是拷贝赋值。首先,要明确拷贝操作的深浅,尤其是指针的拷贝,还有父类的成员拷贝也需要显式调用。最后,不能用拷贝赋值函数调用拷贝构造函数,反之也不行,行为无意义。这两个函数执行的目的基本一致,所以可能会存在重复代码,所以可将重复代码拎出到一个单独的成员函数来调用,以降低代码复杂度。

小结:以上。

Original: https://www.cnblogs.com/lee-zq/p/16320174.html
Author: Lee-zq
Title: 《Effective C++》阅读总结(二):类的构造、析构和赋值