本系列是《Effective C++》一书的阅读摘记,分章整理各个条款。
01 将C++视作语言联邦
C++在发明之初,只是一个带类的C。现在的C++已经变成了同时支持面向过程,面向对象,支持泛型和模板元编程的巨兽。这部分内容可以参见“C++的设计与演化”一书。
本条款中,将C++概括为四个次语言组成的联邦:
- 传统C:面向过程,也规定了C++基本的语法。
- OOP:面向对象,带类的C,加入了继承,虚函数等概念。
- Template:很多针对模板需要特殊注意的条款,甚至催生了模板元编程。
- STL:标准模板库。使用STL要遵守它的约定。
想要高效地使用C++,必须根据不同的情况遵守不同的编程规范。
02 尽量使用const
, enum
, inline
替换 #define
(以编译器替换预处理器)
#define
是C时代遗留下来的预编译指令。
当#define
用来定义某个常量时,通常const
是一个更好的选择。
1 |
|
当此常量为整数类型(int
, char
, bool
)等时,也可以使用enum
定义常量。这种做法常常用在模板元编程中。
1 | enum {K = 1}; |
对于const
常量,你可以获取变量的地址,但是对于enum
来说,无法获取变量的地址。对于这一点来说,enum
和#define
相类似。
另一种可能使用#define
的场景是宏定义。这种情形可以使用inline
声明内联函数解决。
总之,尽可能相信编译器的力量。使用#define
将遮蔽编译器的视野,带来奇怪的问题。
03 尽可能使用const
const
不止是给程序员看的,而且为编译器指定了一个语义约束,即这个对象是不该被改变的。所以任何试图修改这个对象的操作,都会被编译器检查出来,并给出error。
所以,如果某一变量满足const
的要求,那么请加上const
,和编译器签订一份契约,保护你的行为。
这里不再讨论const
的寻常用法。提示一下:当修饰指针变量时,const
在星号左边,是指指针所指物是常量;当const
在星号右边,是指指针本身是常量。如下所示:
1 | const int* p = &a; |
STL中,如果声明某个迭代器为const
,是指该迭代器本身是常量;如果你的意思是迭代器指向的元素为常量,那么使用const_iterator
。
const
更丰富的用法是用于函数声明中,
- 当修饰返回值时,意思是返回值不能修改。这可以让你避免无意义的赋值,尤其是以下的错误:
1 | if (fun(a, b) = c) // 这里错把 == 打成了 = |
- 当修饰参数时,常常用做 pass-by-const-reference 的形式,不再多说了。
- 当修饰函数本身时,常常用在类中的成员函数上,意思是这个函数将不改变对象的成员。
这种情况下,可能会有const
重载现象。
1 | class my_string{ |
实际调用时,根据调用该函数的对象是否是const
的来决定究竟调用哪个版本。
上面的实现未免过于复杂,我们还可以改成下面的形式:
1 | class my_string{ |
注意上面的代码进行了两次类型转换。由non-const reference
转为const reference
是类型安全的,使用static_cast
进行。最后我们要脱掉const char&
的const
属性,使用了const_cast
。
对于const
成员函数,有时不得不修改类中的某些成员变量,可以将这些变量声明为mutable
。
04 确保对象在使用前已经被初始化
使用未被初始化的变量有可能导致未定义的行为,导致奇怪的bug。所以推荐为所有变量进行初始化。
对于内建类型,需要手动初始化。
对于用户自定义类型,一般需要调用构造函数初始化。推荐在构造函数中使用初始化列表进行初始化,这样可以避免不必要的性能损失。原因见下:
1 | public A(name, age) { |
如果在类A
的构造函数中使用初始化列表,就可以避免上面的赋值,而是使用copy-construct
实现。
需要注意,成员初始化的顺序与其在类中声明的顺序相同,与初始化列表中的顺序无关。所以推荐将两者统一。
讨论完上述情况,再来看一种特殊变量:不同编译单元non-local static
变量,是指不在某个函数scope下的static
变量。这种变量的初始化顺序是未定义的,所以作者推荐使用单例模式,将它们移动到某个函数中去,明确初始化顺序。这里不再多说了。