Effective C++学习笔记

阅读《Effective C++》时记录下的笔记

Accustoming Yourself to C++

Prefer consts, enums, and inlines to #defines

#define是preprocessor负责的,它可能导致报错时提示信息不便于问题定位,且#define定义的常量无法局限在一个类内。


const的优势

  • 对于一些更复杂的类型如浮点,它可以生成更少的代码,不像#define一样会导致复制多份。
  • 调试信息更便于追溯。

当想要把一个const限制在类的作用域中的时候,需要作为成员变量,并使用static确保最多只有一份该constant。

上图只有constant的declaration,类内的静态integral常量在没用到取地址操作时可以不提供definition。否则,应该在实现文件中进行definition,如果declaration已经给了初值,那么definition可以不给。

只有integral的类内静态常量可以在declaration的时候直接给初值。


对于类的编译直接需要被初始化的常量的情况,如声明一个指定大小的数组,可以用enum hack。

一个良好的编译器不会为integral的#define和enum分配内存空间。


即使用看样子实现没有问题的宏函数,也可能存在潜在的问题。

用inline则既可以没有函数的开销,又安全,又可以有作用域。


#ifdef / #ifndef等内容依然起着重要的不可替代的作用。


Use const whenever possible


把非built-in type的函数返回值设为const有利于避免下面的错误(built-in type本身就不允许返回值被修改)。

用mutable修饰变量使得即使在const的member function中也可以修改它们。

当const和non-const的member function功能完全一致但返回值类型不同时,可以用下面的方法减少代码重复。


Make sure that objects are initialized before they're used

ctor的member initialization list是initialization而不是assignment,所以效率往往会高于在ctor中进行赋值(因为赋值前先进行了一次default construct,对于built-in type并没有效率上的差别)。

const、reference必须initialize而不能assign。

初始化顺序为基类成员变量优先,按变量声明顺序初始化而不是在initialization list中的顺序,为保持一致性,initialization list中的顺序应和变量声明顺序保持一致。


对于non-local的static 变量,可以通过将它变成一个local的static变量,即把它的definition放入一个函数中,通过函数访问它。因为C++中确保第一次遇到local static物体的definition时会将其初始化。

对于多线程程序,这样的做法依然存在问题,可以在进入多线程之前手动调用所有返回static物体的函数来确保它们初始化完成。

Constructors, Destructors, and Assignment Operators

Know what functions C++ silently writes and calls

编译器会自动生成public、inline的ctor、dtor、copy ctor、copy operator如果代码中有用到对应的功能且没有声明。dtor在继承自的基类的destructor声明为virtual的时候也会是virtual。

编译器不支持生成含reference或const的类成员的copy assignment,因为它们必须在初始化的时候就确定,不支持二次赋值,应当自己实现copy assignment。

Explicitly disallow the use of compiler-generated functions you do not want

declare private(避免被client调用) and not defined(避免类内部调用,调用会发生link error)

为了使它在编译器就报错,可以使用以下技巧。

因为如果代码中使用到了copy,编译器自动生成的copy会尝试调用父类的copy,而父类的copy是private。


Declare destructors virtual in polymophic base classes

如果父类的dtor是virtual的,在用父类指针delete时,会先调用子类的dtor,再调用父类的dtor。

The rule for giving base classes virtual destructors applies only to polymorphic base classes — to base classes designed to allow the manipulation of derived class types through base class interfaces.

将不需要virtual的dtor声明为virtual会造成不必要的内存消耗(vptr)。


Prevent exceptions from leaving destructors

应当尽量避免destructor中抛出异常。

对于以下为确保数据库连接关闭而在dtor中进行可能抛出异常的操作

可以改为


Never call virtual functions during construction or destruction

不应该在constructor里调用或间接调用virtual function因为如果该类A是另一个类B的父类,那么B构造过程会先调用A的构造函数,A的构造函数调用的virtual function是A的而不是B的。destructor类似。


Have assignment operators return a reference to *this

为了能够连等。适用于各种assignment操作符,如+=。

Handle assignment to self in operator=

像上面这种代码如果不判断是否是自己则可能pb和rhs.pb是同一个,存在指针悬空的问题。

但这样是不exception-safe的,一旦pb = new Bitmap()抛出异常,pb将指向已经被free的地址。

下面的做法则既解决了assignment to self也确保了exception-safety。

如果构造非常消耗资源,可以加是否为自己的判断。如果assignment to self不经常发生,那么不判断会更好一些。


下面的方法兼顾了assignment to self和exception-safety。


Copy all parts of an object

对于一个子类,记得调用它父类的copy functions。

如果觉得这两个函数有较高的重复性,可以另写一个private的函数,而不是尝试在一个copy function里调用另一个copy function。


Resource Management

Use objects to manage resources

Resource Acquisition Is Initialization, RAII

不要将智能指针用于数组,因为它是delete而不是delete[]


Think carefully about copying behavior in resource-managing classes

Provide access to raw resources in resource-managing classes

Use the same form in corresponding uses of new and delete

对于array[]使用delete是undefined behavior

避免对数组使用typedef,那会使得new和delete的对应关系难以看出。

Store newed objects in smart pointers in standalone statements

对于下图的代码

它的执行顺序可能是,new Widget、priority()、std::tr1::shared_ptr<Widget>,如果priority抛出异常,那么会存在内存泄漏。所以应改成下面这样。


Designs and Declarations

对于下图这样参数顺序容易错的接口

可以写一个简单的类型封装

对于有小的范围限制的类型,可以直接用静态成员函数实现。


为了不让(a * b = c)这种错误出现,返回类型设为const。


Prefer pass-by-reference-to-const to pass-by-value

避免了不必要的copy,避免了传入派生类发生的slice off。

built-in、iterator、function object不适用该规则,pass-by-value更高效,因为reference实际上就是指针。

并不是小的object就更适合pass-by-value,因为有的编译器不会将user-defined object里的double放入寄存器但会将直接的double放入寄存器。而且小的object在维护过程中可能逐渐变大。


Prefer non-member non-friend functions to member functions

non-member non-friend functions有更好的封装性,因为它不能访问类的私有变量。

non-member non-friend functions有更好的拓展性,client可以写一个新的header,放在同一命名空间下,在需要用到时include进来,而member function对于client来说是不可拓展的。


Consider support for a non-throwing swap

Implementations

Inheritance and Object-Oriented Design

Templates and Generic Programming

Customizing new and delete

Miscellany


Effective C++学习笔记
https://jhex-git.github.io/posts/2255851676/
作者
JointHex
发布于
2022年10月6日
许可协议