条款 01 - 条款 10 | 《Effictive C++》笔记

Effictive C++ 条款 01 - 条款 25。

条款 01:视 C++ 为一个语言联邦

C++ 并不是一个带有一组守则的一体语言,而是从四个次语言组成的联邦政府,每个次语言都有自己的规约。四个次语言包括:

  • C
  • Object-Oriented C++
  • Template C++
  • STL

条款 02:尽量以 const, enum, inline 替换 #define(宁愿以编译器替换预处理器)

  • 对于单纯常量,最好以 const 对象或 enums 替换 #define,原因如下:
    • 编译错误信息不会提到宏的名字,而是会提示宏代表的东西,不利于排查错误
    • 对于浮点常量而言,使用常量可能比使用宏导致编译后的二进制体积更小
  • 对于形似函数的宏,最好改用 inline 函数替换 #defines

条款 03:尽可能使用 const

  • const 允许制定一个语义约束,比那一起会强制实施这项约束
  • 编译器强制实施 bitwise constness,但是编写程序时应该使用“逻辑上的常量性”
  • 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复

注:

bitwise constness 与 logical constness 代表两种对 const 成员函数的理解。

bitwise constness 是指成员函数不可更改对象的任何成员变量(static 除外),也就是说不更改对象内的任何一个 bit。这种理解的好处是很容易政策未返点:编译器只需寻找成员变量的复制动作即可。但是许多成员函数虽然不十足具备 const 性质却能够通过 bitwise 测试,比如一个成员函数更改了成员内的一个指针所指向的内容,但如果只有该指针(而非其所指物)隶属于该对象,那么此函数就没有违反bitwise const。这违反直观结果。

而 logical constness 指一个 const 成员函数可以修改它所处理的对象内的某些 bits,只要在逻辑上不违反该对象的常量性,但是只有在编译器政策不出的情况下才可以这样做。

条款 04:确定对象被使用前已先被初始化

  • 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们
  • 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。前者是直接调用成员的构造函数,后者是先调用默认构造函数然后赋新值。对大多数类型而言,比起先调用默认构造函数然后再调用拷贝赋值操作符,只调用一次构造函数时比较高效
  • 成员初值列中的成员按照顺序初始化,如果这个顺序和成员声明的次序不通,当一个成员你的初始化依赖另一个成员时,就有可能出现错误,但是编译器不会报错。所以初值列列出的成员变量,其排列次序应该和它们在类中的声明次序相同
  • 为避免“跨编译单元之初始化次序”问题,请以 local static 对象(静态成员函数)替换 non-local static 对象(全局静态对象),这样就可以在使用该静态成员的时候才初始化它而不是在程序运行的开始就初始化
1
2
3
4
static Singleton& getInstance() {
static Singleton instance;
return instance;
}

条款 05: 了解 C++ 默默编写并调用哪些函数

编译器会为类创建:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • (拷贝)赋值操作符

条款 06: 若不想使用编译器自动生成的函数,就该明确拒绝

当不想让一个对象被拷贝或者被赋值给其他对象时,可将拷贝构造函数和拷贝赋值操作符声明为 private 并且不予实现。这样在拷贝该对象时,编译器会报错;当在该对象的友元函数或成员函数中调用拷贝时,连接器会报错。

条款 07: 为多态基类声明 virtual 析构函数

  • 带多态性质的基类应该声明一个虚析构函数。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全
  • 虚函数的目的是允许派生类对虚函数进行定制化,如果一个类带有任何虚函数,就可以看做是带多态性质的基类,它就应该拥有一个虚析构函数
  • 类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明虚析构函数,否则虚函数表会增加类的体积。由于 std::string 没有虚拟析构函数,因此永远不应该将其作为基类从中派生

条款 08:别让异常逃离析构函数

不鼓励在析构函数中抛出异常。原因是异常抛出后程序会结束执行或者导致不明确行为,这就会剩下部分成员未析构,从而导致内存泄漏。

解决方案:

  • 析构函数中如果遇到异常就直接结束程序
  • 析构函数中如果遇到异常就吞下异常
  • 将析构函数中可能抛出异常的操作交给用户去做,但是在析构函数中保留兜底的检测(如果用户没有手动完成该操作,析构函数帮他完成,仍然可以选择直接结束或者吞下异常)

条款 09:绝不在构造和析构过程中调用 virtual 函数

如果基类的构造函数中有一个虚函数 v_func(),那么在构造派生类时会调用基类构造函数中 v_func() 而不是派生类的。当基类的构造函数执行时派生类的的成员尚未初始化,如果此期间调用的的虚函数下降至派生类这一等级,则有可能会使用到为初始化的派生类成员,C++ 是不会让你这么干的。相同道理也适用于析构函数。

非正式的说法比较传神:在基类的构造和析构期间,虚函数不是虚函数。

条款 10:令 operator= 返回一个 *this 的引用

为了实现“连锁赋值”(如 a = b = c = 1;),赋值操作符必须返回一个 reference 指向操作符的左侧实参。这是为自定义类实现赋值操作符时应该遵循的协议。

条款 01 - 条款 10 | 《Effictive C++》笔记

http://www.zh0ngtian.tech/posts/954cccd6.html

作者

zhongtian

发布于

2020-08-30

更新于

2023-12-16

许可协议

评论