Effective C++ 读书笔记
记录一些几条茅塞顿开的条款。
02 尽量以const,enum,inline替换#define
(1)对于单纯常量,最好以const对象或enums替换#defines
为什么?因为默认状态下,const对象仅在文件内有效(当多个文件中出现同名的const对象时,等同于在不同文件中分别定义了独立的变量)
好处:const限制了作用域
(2)class的专属常量
1 | class GamePlayer{ |
如果要对类专属常量取地址,你必须提供一个定义式,并将它放到实现文件中,如下:
1 | const int GamePlayer::NumTruns; |
(3)enum hack 技巧
理论基础:一个属于枚举类型的数值可以充当int被使用
1 | class GamePlayer{ |
03 尽可能用const
(1)const的可能用法
const 变量
const 指针(两种:常量指针和指向常量的指针)
函数参数用cosnt,返回类型const
const类(包括const成员变量、const成员函数、const this)
- const对象、const对象的指针或引用只能调用cosnt成员函数
- cosnt成员函数在参数后、函数体前加const,表示this指针是cosnt的 //todo 添加代码块示意
(2)const与普通函数
1 | classs Rational {} |
返回一个const对象,可避免如下错误
1 | if (a * b = c) |
(3)const与成员函数
const作用于成员函数,是仅让该成员函数作用于const对象上。
- 两个成员函数只是常量性不同,可以被重载。
const作用于成员函数到底意味着什么?
(3)bitwise constness || logical constness
- bitwise constness const成员函数不能改变对象内任何一个bit
- logical constness const成员函数可以修改对象内的某些bit,只需要逻辑成立(客户端不感知)
- 引入关键字mutable,可以释放non-static成员变量的bitwise constness约束
(4)当const和non-const成员函数有实质等价的实现时,令non-const版本调用const版本
04 确定对象使用前已经被初始化
Q : 在使用 = 运算符的情况下,如何确定是调用拷贝构造函数还是拷贝赋值函数?
A:通过左侧对象是否已经构造来判断。如果左侧对象已经构造,那么调用的是右侧对象的拷贝赋值函数;否则调用的左侧对象的拷贝构造函数。
(1)继承中构造函数的执行顺序
- 基类的构造函数
- 成员列表的拷贝构造函数
- 构造函数体
构造函数推荐写法:成员初始值列(member initialization list)语法
- 而不是在构造函数体内使用赋值操作(为什么?)
- 初值列列出的成员变量,其排列顺序应和它们在class中的声明次序相同
(2)区别赋值与初始化
1 | ABEntry::ABEntry(cosnt std::string& name, ...) |
一个好的准则是:每次都在初始化列表中列出所有的成员变量,默认构造函数使用括号即可(theName())
但是列出所有成员变量未免显得太臃肿,对于“赋值与初始化表现一样好的”变量,可以移动到private的成员函数(init),然后让构造函数调用。
(3)小心跨编译单元的初始化次序问题
用local static对象(单例模式)替换non-local static对象,避免依赖使用时未初始化。
确保对象在使用之前以及被初始化,请明确这一点。
06 不想使用编译器自动生成的函数,就明确拒绝
方法一:令成员函数为private
- 缺点:类的member函数和friend函数还是可以访问它
方法二:令成员函数为private且不去实现它们
- 推荐!
方法二:设置一个基类,阻止拷贝和拷贝赋值
方法三:=delete关键字
07 为多态基类声明virtual析构函数
(1)c++ 明白指出,当基类带有non-virtual析构函数时,由基类指针释放派生类对象,将导致未定义行为(通常是对象的派生成分没有被销毁)
(2)任何class只要带有virtual函数都几乎确定有一个virtual 析构函数
(3)析构函数的运算顺序:
- 最外层派生类的析构函数被调用
- 基类的析构函数被调用
(4)基类带纯析构函数可能非常便利,但记得带上一份默认实现
1 |
|
执行结果: B的析构函数会调用A的析构函数,但A的析构函数未定义。
1 | Undefined symbols for architecture x86_64: |
08 别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但析构函数最好不要抛出异常。
(1)如果析构函数可能抛出异常,该如何处理?
考虑一个vector对象,它包含若干个可能在析构期间出现异常的对象,如果第一个元素就发生异常了,该怎么办?
那还是要确保剩下几个元素的资源释放,又得调用析构函数。如果又发生异常,太多异常了,程序将被迫中止。boom~
(2)可以考虑的解决方法:析构函数catch异常,吞下它们活着结束程序abort
09 别在构造和析构过程中调用virtual函数
在构造函数中调用virtual函数,调用的是基类的还是派生类的虚函数?
答案是:基类的虚函数。
为什么?当基类的构造函数执行时,派生类的构造函数还未执行(派生类的成员变量还没有初始化),而派生类的函数往往使用派生类的成员变量,因此如果当基类的构造函数执行时调用派生类的虚函数,使用未初始化的成员变量将导致未定义行为。
10 operator=返回reference to *this
这是为了能够应对以下连续赋值
1 | x = y = z = 1; |
11 operator=处理自我赋值
(1)简单判断
1 | Widget& Widget::operator=(const Widget& rhs) |
问题出在处理new Bitmap(*rhs.pb)
时抛出异常,那么pb就是个空悬指针。
(2)不那么高效率的解决办法:先别着急删除pb,复制一份,申请内存成功后再删除。
1 | Widget& Widget::operator=(const Widget& rhs) |
(3)Swap技巧
1 | Widget& Widget::operator=(const Widget& rhs) |
17 以独立语句将newed置入智能指针
(1)现象
1 | processWidget(std::shared_ptr<Widget>(new Widget), priority()); |
编译器做这三件事:
- 调用 priority 函数
- 执行 new Widget 内存申请
- 调用 shared_ptr 构造函数
但是编译器却不是以一个固定顺序执行,可以肯定内存申请在调用shared_ptr构造函数之前,如果按以下顺序执行:
- 执行 new Widget 内存申请
- 调用 priority 函数
- 调用 shared_ptr 构造函数
并且在第二步发生异常,那么申请的内存将泄漏。
(2)解决方法
1 | std::shared_ptr<Widget> pw(new Widget); |
以独立语句将newed置入智能指针。
22 将成员变量声明为private
一个惊人的事实:protected并不比pulbic更具有封装性
某些东西的封装性与“当其内容改变时可能造成的代码破坏量”成反比。
- 如果我们有一个public变量,而我们最终取消了它,有多少代码会被破坏?结果是所有使用它的客户代码
- 如果我们有一个protected变量,而我们最终取消了它,有多少代码被破坏?所有使用它的派生类。
23 以non-member、non-freied替换member函数
从封装开始讨论。
如果某些东西被封装,那么它就不再可见。
愈多东西被封装,愈少人可以看到它,我们就有愈多的弹性去改变它。
我们推崇封装的原因:它使我们能够改变事物只影响有限客户。
如何量化某个数据的封装性?越多函数能够访问它,封装性越低。
所以,如果要提供较好的封装性,non-member non-friend时个不错的选择,因为它并不增加能够访问类private数据的函数数量。
25 考虑写出一个不抛异常的swap函数
(1)ADL 法则
Argument Dependent Lookup法则,依赖于实参命名空间的查找法则。
(2)实现自己的高效swap
如果swap缺省版实现的效率不足(几乎意味着你的class 或 template使用了某种pimpl手法),尝试以下事情:
- 提供一个public swap成员函数,它高效实现置换pimpl
- 在你的class的命令空间内提供一个non-member swap,另它调用上述swap函数
27 少做转型
使用新式转型的好处:
- 容易被识别
- 转型动作窄化,编译器可以识别错误使用并优化
28 避免返回handle指向对象内部成分
即便是返回一个const reference handle,你就暴露在“handle 比其所指对象更长寿”的风险下。
代码例子:
1 | class GUIObject {...} |
问题在于boundingBox返回的是一个临时对象,马上被析构。
Effective C++ 读书笔记
https://xyz.desirer233.fun/2025/06/15/C++/effective C++读书笔记/