Effective C++ 读书笔记

记录一些几条茅塞顿开的条款。

02 尽量以const,enum,inline替换#define

(1)对于单纯常量,最好以const对象或enums替换#defines

为什么?因为默认状态下,const对象仅在文件内有效(当多个文件中出现同名的const对象时,等同于在不同文件中分别定义了独立的变量)

好处:const限制了作用域

(2)class的专属常量

1
2
3
4
5
6
class GamePlayer{
private:
// static 使得类共享一个
// const 声明为常量
static const int numTruns = 5;
}

如果要对类专属常量取地址,你必须提供一个定义式,并将它放到实现文件中,如下:

1
const int GamePlayer::NumTruns;

(3)enum hack 技巧

理论基础:一个属于枚举类型的数值可以充当int被使用

1
2
3
4
class GamePlayer{
private:
enum { NumTurns = 5;}
}

03 尽可能用const

(1)const的可能用法

  • const 变量

  • const 指针(两种:常量指针和指向常量的指针)

  • 函数参数用cosnt,返回类型const

  • const类(包括const成员变量、const成员函数、const this)

    • const对象、const对象的指针或引用只能调用cosnt成员函数
    • cosnt成员函数在参数后、函数体前加const,表示this指针是cosnt的 //todo 添加代码块示意

(2)const与普通函数

1
2
3
classs Rational {}

cosnt Rational operator* (cosnt Rational& lhs, const Rationa& rhs);

返回一个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)继承中构造函数的执行顺序

  1. 基类的构造函数
  2. 成员列表的拷贝构造函数
  3. 构造函数体

构造函数推荐写法:成员初始值列(member initialization list)语法

  • 而不是在构造函数体内使用赋值操作(为什么?)
  • 初值列列出的成员变量,其排列顺序应和它们在class中的声明次序相同

(2)区别赋值与初始化

1
2
3
4
5
6
7
8
9
ABEntry::ABEntry(cosnt std::string& name, ...)
{
theName = name; // assignments, not initializations
}

ABEntry::ABEntry(cosnt std::string& name, ...):
theName(name), ... // initializations
{
}

一个好的准则是:每次都在初始化列表中列出所有的成员变量,默认构造函数使用括号即可(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)析构函数的运算顺序:

  1. 最外层派生类的析构函数被调用
  2. 基类的析构函数被调用

(4)基类带纯析构函数可能非常便利,但记得带上一份默认实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

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

class B : A{
public:
B(int m):mem(m){}
~B(){}
private:
int mem;
};

int main(){
B b(1);
cout<<"hello world\n";
return 0;
}

执行结果: B的析构函数会调用A的析构函数,但A的析构函数未定义。

1
2
3
4
5
Undefined symbols for architecture x86_64:
"A::~A()", referenced from:
B::~B() in main-3fde59.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

08 别让异常逃离析构函数

C++并不禁止析构函数吐出异常,但析构函数最好不要抛出异常。

(1)如果析构函数可能抛出异常,该如何处理?

考虑一个vector对象,它包含若干个可能在析构期间出现异常的对象,如果第一个元素就发生异常了,该怎么办?

那还是要确保剩下几个元素的资源释放,又得调用析构函数。如果又发生异常,太多异常了,程序将被迫中止。boom~

(2)可以考虑的解决方法:析构函数catch异常,吞下它们活着结束程序abort

09 别在构造和析构过程中调用virtual函数

在构造函数中调用virtual函数,调用的是基类的还是派生类的虚函数?

答案是:基类的虚函数。

为什么?当基类的构造函数执行时,派生类的构造函数还未执行(派生类的成员变量还没有初始化),而派生类的函数往往使用派生类的成员变量,因此如果当基类的构造函数执行时调用派生类的虚函数,使用未初始化的成员变量将导致未定义行为。

10 operator=返回reference to *this

这是为了能够应对以下连续赋值

1
2
3
x = y = z = 1;
// 相当于
x = (y = (z = 1));

11 operator=处理自我赋值

(1)简单判断

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

问题出在处理new Bitmap(*rhs.pb)时抛出异常,那么pb就是个空悬指针。

(2)不那么高效率的解决办法:先别着急删除pb,复制一份,申请内存成功后再删除。

1
2
3
4
5
6
7
8
9
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this;

Widget* tmp = bp; //复制一份
pb = new Bitmap(*rhs.pb);
delte tmp;
return *this;
}

(3)Swap技巧

1
2
3
4
5
6
7
8
9
10
11
12
Widget& Widget::operator=(const Widget& rhs)
{
Widget tmp(rhs); // 拷贝rhs
swap(tmp); // 将tmp与*this交换
return *this;
}
// more tricky
Widget& Widget::operator=(const Widget& rhs)
{
swap(ths);
return *this;
}

17 以独立语句将newed置入智能指针

(1)现象

1
processWidget(std::shared_ptr<Widget>(new Widget), priority());

编译器做这三件事:

  • 调用 priority 函数
  • 执行 new Widget 内存申请
  • 调用 shared_ptr 构造函数

但是编译器却不是以一个固定顺序执行,可以肯定内存申请在调用shared_ptr构造函数之前,如果按以下顺序执行:

  1. 执行 new Widget 内存申请
  2. 调用 priority 函数
  3. 调用 shared_ptr 构造函数

并且在第二步发生异常,那么申请的内存将泄漏。

(2)解决方法

1
2
3
std::shared_ptr<Widget> pw(new Widget);

processWidget(pw, priority());

以独立语句将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手法),尝试以下事情:

  1. 提供一个public swap成员函数,它高效实现置换pimpl
  2. 在你的class的命令空间内提供一个non-member swap,另它调用上述swap函数

27 少做转型

使用新式转型的好处:

  • 容易被识别
  • 转型动作窄化,编译器可以识别错误使用并优化

28 避免返回handle指向对象内部成分

即便是返回一个const reference handle,你就暴露在“handle 比其所指对象更长寿”的风险下。

代码例子:

1
2
3
4
5
class GUIObject {...}
const Rectangle boundingBox(consgt GUIObject& obj);
// 客户使用
GUIObject* po;
const Point* p = &( boudingBox(*po).upperLeft() );

问题在于boundingBox返回的是一个临时对象,马上被析构。

作者

Desirer

发布于

2025-06-15

更新于

2025-07-06

许可协议