0%

Item 4: Make sure that objects are initialized before they’re used.

手工初始化内置对象

为内置对象进行手工初始化,因为C++不保证初始化他们。

1
2
3
4
5
int x = 0;                                  //对 int 进行手工初始化
const char *text = "A C-style string"; //对指针进行手工初始化

double d;
std::cin >> d; //以读取 input stream 的方式完成初始化

构造函数最好使用成员初值列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PhoneNumber { ... }

class ABEntry {
public:
ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
}

ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones) {
theName = name; //这些都是赋值
theAddress = address; //而非初始化
thePhones = phones;
numTimesConsulted = 0;
}

构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同。

1
ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones) : theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {}

local static 对象替换 non-local static 对象。

为免除”跨单元之初始化次序“问题,请以 local static 对象替换 non-local static 对象。

1
2
3
4
5
6
7
class FileSystem {
public:
...
std::size_t numDisks() const;
...
}
extern FileSystem tfs;
1
2
3
4
5
6
7
8
9
10
11
12
class Directory {
public:
Directory( params );
...
}

Directory::Directory( params)
{
...
std::size_t disks = tfs.numDisks();
...
}

客户使用使用:

1
Directory tempDir( params );

现在初始化次序的重要性体现出来了,除非 tfstempDir 之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfstempDir是不同的人在不同的时间于不同的源文件建立起来的,它们是定义于不同编译单元内的 non-local static 对象。它们初始化相对次序并无明确定义。但我们可以将 local static 对象替换non-local static 对象来解决。这也是Singleton模式的常见实现手法。

这个手法的基础在于:C++保证,函数内的 local static 对象会在调用该函数时首次遇上该对象的定义式时被初始化。

1
2
3
4
5
6
7
class FileSystem { ... }

FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Directory { ... }

Directory::Directory( params)
{
...
std::size_t disks = tfs().numDisks();
...
}

Directory& tempDir()
{
static Directory td;
return td;
}

Item3: Use const whenever possible.

常量的声明

指针的常量声明:

1
2
3
4
5
char greeting[] = "Hello";
char* p = greeting; //non-const pointer, non-const data
const char* p = greeting; //non-const pointer, const data
char* const p = greeting; //const pointer, non-const data
const char* const p = greeting; //const pointer, const data

如果 const 出现在*左边,表示被指物为常量; 如果出现在*右边,表示指针自身为常量;如果出现在*两边,表示被指物和指针两者都是常量。

如果被指物是常量,const 放在类型之前和放在类型之后*之前表示的意义一样:

1
2
void f1(const Widget* p); //f1 获得一个指针,指向一个常量Widget对象
void f2(widget const *p); //f2 也是

STL的iterator 系以指针塑模出来,所以iterator的作用像个T*指针。如果希望指针是常量,可以声明为 const iterator,如果希望被指物为常量,需使用 const_iterator

1
2
3
4
5
6
7
8
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin(); //iter的作用像个T* const
*iter = 10; //没问题,改变iter所指物
++iter;                            //错误,iter是const
std::vector<int>::const_iterator cIter = vec.begin(); //cIter的作用像个const T*
*cIter = 10; //错误,*cIter是const
++cIter; //没问题, 改变cIter

返回值声明为常量,可以降低代码被错误使用:

1
2
class Rational {...};
const Rational operator*{const Rational& lhs, const Rational& rhs};

当我们本来想做个比较,错误地输入=

1
if (a * b = c) ...

编译器就会报错误:不可给常量赋值。

const 成员函数

声明const 成员函数,是为了确认该成员函数可以作用与const对象,也使class接口比较容易理解,可以得知哪些函数可以改动对象内容,哪些不可以。

成员函数只是常量性不同是可以被重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TextBlock {
public:
...
const char& operator[](std::size_t position) const // operator[] for
{ return text[position]; } // const objects

char& operator[](std::size_t position) // operator[] for
{ return text[position]; } // non-const objects

private:
std::string text;
};

TextBlock tb("Hello");
const TextBlock ctb("World");
tb[0] = 'x'; // fine — writing a non-const TextBlock
ctb[0] = 'x'; // error! — writing a const TextBlock

bitsise constness 和 logical constness

bitsise constness: 成员函数只有在不改变对象的任何非静态成员变量时才可以被称为常量函数。也是C++对常量性的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
class TextBlock{

public:
char& operator[](std::size_t position) const{
return pText[position];
}
private:
char* pText;
};

const TextBlock tb;
char *p = &tb[1];
*p = 'a';

在const和non-const成员函数中避免重复

constnon-const成员函数有着实质等价的实现时,令non-const函数调用const函数可以避免代码重复。不可以反着来。

1
2
3
4
5
6
7
8
9
10
const char& operator[](std::size_t position) const {
...
return text[position]
}
char& operator[](std::size_t position) {
return const_cast<char&>(
static_cast<const TextBlock&>(*this)
[position]
)
}
  1. *this 的类型是 TextBlock,先把它强制隐式转换为 const TextBlock,这样我们才能调用那个常量方法。
  2. 调用 operator[](std::size_t) const,得到的返回值类型为 const char&
  3. 把返回值去掉 const 属性,得到类型为 char& 的返回值。

Item 2: Prefer consts, enums, and inlines to #defines

我们先看看#deifne 有哪些的问题:

不利于调试

1
#define ASPECT_RATION 1.653

在预处理时候 ASPECT_RATION 可能就被移走了,ASPECT_RATION 没有进入 符号表, 运行此常量获得编译错误信息时, 可能会疑惑。因为这个错误信息总是提到 1.653,而不是ASPECT_RATION , 如果 ASPECT_RATION 定义不是自己写的头文件中,可能对 1.653 的来源毫无概念,将因追踪它浪费时间,解决之道是以一个常量替换上述宏 。

1
2
const double AspectRatio = 1.653 //大写名称通常用于宏
//因此这里改变名称写法

作为一个语言常量,ASPECT_RATION 肯定会被编译器看到,当然会进入记号表内。此外对于浮点常量(floating point constant)而言,使用常量可能比使用#define 导致较少量的码。

不重视scope

无法利用 #define 创建class专属常量。一旦宏定义,它就在其后的编译过程中有效(除非在某处 #undef )。而 const 可以。

1
2
3
4
5
6
class GamePlayer {
private:
static const int NumTurns; //常量声明式
int scores[NumTurns]; //使用该常量

}

enum 比 const 更好用

旧式编译器也许不支持上述语法, 它们不允许static在声明式上获得初值,此外所谓的“in-classs 初值设定”也只运行对整数常量进行, 如果编译器不支持上述语法,可以将初值放在定义式

1
2
3
4
5
class CostEstimate {
public:
static const double FudgeFactor; //staitc class 常量声明位于头文件内
}
const double CostEstimate::FudgeFactor = 1.35; //staitc class 常量定义位于实现文件内

如果使用emnu就很简单:

1
2
3
4
5
6
class GamePlayer {
private:
enum { NumTurns = 5 };

int scores[NumTurns]; //the enum hack
}

 

不易理解

1
2
3
4
5
#define CALL_WITH_MAX(a, b)  f((a) > (b) ? (a) : (b))

int a = 5, b =0;
CALL_WITH_MAX(++a, b);  //a被累加二次
CALL_WITH_MAX(++a, b + 10); //a被累加一次
  • 必须记住为宏的所有实参加上小括号
  • 在这里调用f之前,a的递增次取决与“它被拿来与谁比较”

更好的做法是使用 template inline 函数。

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

Item 1: View C++ as a federation of languages

一开始,C++ 只是 加上一些面向对象特性,C++ 最初的名称 C with Classes 也反映了这个血缘关系。现在这个语言逐渐成熟,已经是一个多重泛型编程语言(multiparadigm programming language)。同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)

C++ 视为一个由相关语言组成的联邦而非单一的语言。

C++ 主要4个子语言:

  • C。说到底C++仍是以C为基础。许多时候C++对问题的解法其实不过就是较高级的C的解法如item2item13。当只使用C++C的那部分语法, 会发现C语言的缺陷:没有模板、没有异常、没有重载。
  • Object-Oriented。面向对象程序设计也是C++的设计初衷:构造与析构、封装与继承、多态、动态绑定的虚函数。
  • Template C++。这是C++的泛型编程部分,大多数程序员经验最少的部分。TMP模板元编程template metaprogramming)也是一个新兴的程序设计范式。
  • STLSTL是一个特殊的模板库,它将容器、迭代器和算法优雅地结合在一起。

C++ 程序设计的惯例并非一成不变,而是取决于你使用 C++ 语言的哪一部分。例如, 在基于C语言的程序设计中,基本类型传参时传值比传引用更有效率。 然而当你接触 Object-Oriented C++ 时会发现,传常量指针是更好的选择。运用Template C++时尤其如此,因为彼时你甚至不知道所处理的对象的类型。 但是你如果又碰到了STL,其中的迭代器和函数对象都是基于C语言的指针而设计的, 这时又回到了原来的规则:传值比传引用更好。