改善程序与设计的55个具体做法

改善程序与设计的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
#define ABC 1

可能编译器在处理源码前ABC就被预处理器移走了,可以理解把程序中的ABC都替换为1,会导致ABC这个名称没有进入记号表(symbol table),编译一旦出错,错误信息不会显示ABC,只会是1.
解决方法

1
const int abc=1;//一般宏大写

使用常量不会像宏一样替换代码,所以“码量”较小。

1
2
const char* const authorName="Scott Meyers";
const std::string authorName("Scott Meyers");//这样定义比上面好

如果是类的专属常量,就要在类中声明一个常量成员;为了确保此常量至多有一份实体,必须让他成为一个static成员

1
2
3
4
5
6
class GamePlayer
{
private:
static const int NumTurns=5;//常量声明式,不是定义式
int scores[NumTurns];
};

旧的编译器不允许static成员在声明式获得初值,所以需要在定义式给初值。
class的专属常量如果是static 整数类型(int、char、bool),就不需要提供定义式。但若要取这个常量的地址或编译器(错误的)坚持看到一个定义式,则需要在实现文件中提供定义:

1
const int GamePlayer::NumTurns;//在声明时获得初值,这里不能再设置初值

#define相当于替换代码,所以不能用来定义class的专属常量,也不能提供封装性。

enum

外一编译器(错误的)不允许初值设定,这样编译器在编译期间也无法知道数组大小,可以用"the enum hack"解决

1
2
3
4
5
6
class GamePlayer
{
private:
enum {NumTurns=5;}
int scores[NumTurns];
};
  • 取const的地址合法,而enum不合法(和#define比较像),所以也不会导致非必要的内存分配。
  • 实用主义:许多代码这样使用enum,属于模板元编程基础技术。

inline

一般调用宏实现像函数的宏,需要为所有实参加上小括号。

1
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))

但即使是这样也会造成意想不到的结果

1
2
3
int a=5,b=0;
CALL_WITH_MAX(a,b);//a累加2次
CALL_WITH_MAX(a,b+10);//a累加一次

为了获得宏带来的效率并且类型安全性考虑。使用template inline替代宏

1
2
3
4
5
template<typename T>
inline void callWithMax(const T&a,constT& b)
{
f(a>b?a:b);
}

总结:

  • 对于单纯常量,最好以const对象或enum替代#define
  • 对于形似函数的宏(macros),最好改为inline函数替代#define

条款3:尽可能使用const

const出现在*号左边,表示被指物(data)是常量。*号右边表示指针(pointer)自身是常量。

1
2
3
4
5
char greeting[]="Hello";
char *p=greeting;
const char* p=greeting;
char* const p=greeting;
const char* const p=greeting;

在函数调用中:

1
2
void f1(const int *a);
void f2(int const *a);

两种写法等效,表示函数获得一个指针,指向常量的int对象。注意*的位置。

关于迭代器const:
STL迭代器是以指针为根据塑模出来,所以迭代器的作用就像个T*指针。

1
2
3
4
5
6
7
8
std::vector<int> vec;
...
const std::vector<int>::iterator iter=vec.begin();//T* const
*iter=10;//没问题,改变的是iter所指的物
++iter;//错误,iter是const
std::vector<int>::iterator const_iterator cIter=vec.begin();//const T*
*cIter=1; //错误,*cIter是const
++cIter; //正确,改变的是cIter
  • 令函数返回一个常量值,往往可以降低因客户错误而造成的意外。

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
2
3
4
5
6
7
8
9
10
11
12
13
class A
{
public:
A(const std::string& name);
private:
std::string theName;
int n;
}
A::A(const std::string& name)
{
theName=name;//这是赋值,非初始化
n=0;
}

初始化应该写成(效率较高):

1
2
3
4
A::A(const std::string& name)
:theName(name),
n(0)
{}//构造函数本体无任何动作

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
2
3
4
class Empty
{

}

当被调用时会创建出以下代码:

1
2
3
4
5
6
7
8
9
10
class Empty
{
public:
Empty(){...}
Empty (const Empty& rhs){...}
~Empty(){...}

Empty& operator=(const Rmpty& rhs){...}//copy assignment

}

当程序中这些函数被调用是,编译器自动创建出来。

1
2
3
4
Empty e1;  //default 构造函数
Empty e2=e1;//copy 构造函数
Empty e2(e1);//copy 构造函数
e2=e1;//copy assignment操作符

但时有时也无法自动创建,如类的成员变量中存在const或reference时:

1
2
3
4
5
...
private:
const a;
std::string& s;
...
  • 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的而且没有定义。
(注:C
11引入了新的特性 =delete,比上述private:更高效。private报错时可能很复杂,而=delete会提示这个方法已被删除)

1
2
3
public:
ios_base(const ios_base&) = delete;
ios_base& operator=(const ios_base&) = delete;

另外可以设计一个阻止copying的基类,其他子类继承他。

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