改善程序与设计的55个具体做法
让自己习惯C++
条款1:视C++为一个语言联邦
今天的C已经是一个多重编程范型语言,一个同时支持过程形式,面向对象形式,泛型形式,元编程形式的语言。
可以把C视为一个有相关语言组成的联邦而非单一语言,C++的次语言一共有四种:
- C
- Object-Oriented C++: 类,封装,继承,多态,虚函数…
- Template C++ :泛型编程
- STL:STL是一个Template程序库。
pass-by-value,pass-by-reference-to-const
C高效编程守则取决于你使用C的那一部分。
条款2:尽量以const,enum,inline替换 #define
const
宁可以编译器替换预处理器。
1 |
可能编译器在处理源码前ABC就被预处理器移走了,可以理解把程序中的ABC都替换为1,会导致ABC这个名称没有进入记号表(symbol table),编译一旦出错,错误信息不会显示ABC,只会是1.
解决方法
1 | const int abc=1;//一般宏大写 |
使用常量不会像宏一样替换代码,所以“码量”较小。
1 | const char* const authorName="Scott Meyers"; |
如果是类的专属常量,就要在类中声明
一个常量成员;为了确保此常量至多有一份实体,必须让他成为一个static
成员
1 | class GamePlayer |
旧的编译器不允许static成员在声明式获得初值,所以需要在定义式给初值。
class的专属常量如果是static 整数类型(int、char、bool),就不需要提供定义式。但若要取这个常量的地址或编译器(错误的)坚持看到一个定义式,则需要在实现文件
中提供定义:
1 | const int GamePlayer::NumTurns;//在声明时获得初值,这里不能再设置初值 |
#define
相当于替换代码,所以不能用来定义class的专属常量,也不能提供封装性。
enum
外一编译器(错误的)不允许初值设定,这样编译器在编译期间也无法知道数组大小,可以用"the enum hack"
解决
1 | class GamePlayer |
- 取const的地址合法,而enum不合法(和
#define
比较像),所以也不会导致非必要的内存分配。 - 实用主义:许多代码这样使用enum,属于模板元编程基础技术。
inline
一般调用宏实现像函数的宏,需要为所有实参加上小括号。
1 |
但即使是这样也会造成意想不到的结果
1 | int a=5,b=0; |
为了获得宏带来的效率并且类型安全性考虑。使用template inline
替代宏
1 | template<typename T> |
总结:
- 对于单纯常量,最好以const对象或enum替代
#define
- 对于形似函数的宏(macros),最好改为inline函数替代
#define
条款3:尽可能使用const
const出现在*
号左边,表示被指物(data)是常量。*
号右边表示指针(pointer)自身是常量。
1 | char greeting[]="Hello"; |
在函数调用中:
1 | void f1(const int *a); |
两种写法等效,表示函数获得一个指针,指向常量的int
对象。注意*
的位置。
关于迭代器const:
STL迭代器是以指针为根据塑模出来,所以迭代器的作用就像个T*
指针。
1 | std::vector<int> vec; |
- 令函数返回一个常量值,往往可以降低因客户错误而造成的意外。
const成员函数
- 使接口易于理解,知道哪个函数可以改动,哪些不可以改
- “操作const对象”,高效编程->
pass by reference-to-const
(有const成员函数才能这么传递对象)
语法:在成员函数的参数列表后面加上const关键字。
- bitwise constness(physical constness)阵营:成员函数只有在不更改对像任何成员变量时才可以是const成员函数,否则编译出错。
但是有时成员函数不具备const性质却也可以通过bitwise测试,如更改指针所指物
- logical constness:const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端检测不出的情况下才得如此。
使用mutable
(可变的)关键字 修饰成员变量,使其在const成员函数内也可以被修改。
const和non-const成员函数中避免重复
两个相同的函数,但一个是const,一个是non-const,这也是重载。但这通常只为了不同的调用方式,const的调用const的,non-const调用non-const的。这会导致相同的代码有两份几乎一摸一样的,很难维护。
解决办法:在const中实现,non-const的函数调用const的。为了避免non-
const函数无穷递归自己。需要利用转型。static_cast
将*this
从non-const转成const,之后再用const_cast
将const移除。
总结
- 将默写东西声明为const可帮助编译器侦测出错误用法,const可被施加于任何作用域内的对象、函数参数、返回类型、成员函数本体。
- 编译器强制实施
bitwise constness
,但你编写程序时应该使用“概念性的常量性” - 当const和non-const成员函数有实质性等价的实现时,令non-const版本调用const可避免代码重复
条款4:确定对象被使用前已先被初始化
永远在使用对象之前先将他初始化。
- 对于无任何成员的内置类型,需要手工完成此事。
- 对于内置类型之外的任何其他东西,初始化责任落在构造函数身上。要确保每个构造函数都将对象的每一个成员初始化。
注意赋值和初始化的区别。即使在构造函数中赋值,也不算初始化。
1 | class A |
初始化应该写成(效率较高):
1 | A::A(const std::string& name) |
C++规定,对象的成员变量的初始化动作发生在构造函数本体
之前,发生于这些成员的default构造函数被自动调用之时。但对内置类型int不为真,不保证你所看到的那个赋值动作的时间点之前获得初值。
基于赋值的那个版本首先调用default构造函数为theName设初值,然后立刻再对他们赋予新值。default构造函数的一切作为因此浪费了
。避免浪费的做法是第二种,成员初值列。theName以name为初值进行copy构造。
对于内置烈类型,其初始化和赋值成本相同,但为了一致性,一般也通过成员初值列来初始化。
初始化顺序
C++有十分固定的成员初始化顺序。
- 先基类后子类。
- 声明顺序而非成员初值列顺序。
不同编译单元内定义之non-local static 对象 的初始化顺序。
-
编译单元:当一个c或cpp文件在编译时,预处理器首先递归包含头文件,形成一个含有所有必要信息的单个源文件,这个源文件就是一个编译单元。
-
static对象:寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。
-
local-static对象:函数内的static对象称为local static对象,因为他们对函数而言是local的。
-
non-local static对象:非local 的static对象都是non-local对象
问题:C++对“定义于不同编译单元的non-local对象”的初始化并无明确定义。
如果两个不同编译单元的non-local static对象其中一个需要另一个先出现,但是编译器无法做到。
解决办法:将每个non-local static 对象搬到自己的专属函数内,该对象在此函数内被声明为static,这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接涉及这些对象。也就是,non-local static被local static 对象替换了。
这是Singleton模式的一个常见实现手法。
结论
- 为内置型对象手工初始化,因为C++不保证初始化他们。
- 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作,初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题请以local static对象替换non-local static对象。
构造/析构/赋值运算
条款05 了解C++默默编写并调用了哪些函数
如果没有声明构造函数,编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
1 | class Empty |
当被调用时会创建出以下代码:
1 | class Empty |
当程序中这些函数被调用是,编译器自动创建出来。
1 | Empty e1; //default 构造函数 |
但时有时也无法自动创建,如类的成员变量中存在const或reference时:
1 | ... |
- C++不允许绕让reference改变所指向的对象
- 更改const成员是不合法的。
所以编译器不能够创建这种copy assignment。需要自己定义copy assignment。
另外,父类的copy assignment的若是private的,子类不会自动生成copy assignment。
条款06 若不想使用编译器自动生成的函数,就应该明确拒绝
有时候我们不希望类有拷贝构造功能,但编译器通常会自己生成这些函数。
解决思路:编译器产出的函数都是public
的,所以为阻止编译器创建,可以将copy构造函数或copy assignment操作符声明为private
。
但这样并不绝对安全,因为成员函数和友元函数
仍可以调用它,这时我们不能定义它们,当有人调用时,会获得一个连接错误(linkage error)。
C标准库中如ios_base
的copy构造函数和copy assignment操作符都是private的而且没有定义。
(注:C11引入了新的特性 =delete
,比上述private:更高效。private报错时可能很复杂,而=delete
会提示这个方法已被删除)
1 | public: |
另外可以设计一个阻止copying的基类,其他子类继承他。