《Effective C++》笔记(一)

取自《Effective C++:改善程序与设计的55个具体做法》中文版第三版

一、让自己习惯C++

01:视C++为一个语言联邦

将C++视为一个由相关语言组成的联邦,其主要的次语言有四个:

  1. C:以C为基础。
  2. Object-Oriented C++:面向对象设计。
  3. Template C++:泛型编程。
  4. STL:template程序库。
  • C++高效编程守则视状况而变化,取决于使用C++的哪一部分。

02:尽量以const,enum,inline替换#define

  1. const
1
2
#define ASPECT_RATIO 1.653      //宏
const double AspectRatio 1.653 //常量

该宏定义,记号名称 ASPECT_PATIO 有可能没有进入记号表,运用该常量出现错误时可能不会直接指示该名称;此外,预编译器替换名称可能导致目标码出现多个1.653;采用常量定义没有这两个问题;

对于class专属常量,确保此常量只有一份实体,需要成为static成员;#define 无法创建class专属常量,实例如下:

1
2
3
4
5
6
7
class GamePlayer {
private:
static const int NumTurns = 5; //声明常量
int score[NumTurns]; //使用该常量
...
}
const int GamePlayer::NumTurns; //定义
  1. enum

有的旧编译器不支持声明时赋值,然而上述的 GamePlayer::score数组声明需要一个常量值,这时可采用enum替换:

1
2
3
4
5
6
class GamePlayer {   
private:
enum { NumTurns = 5 }
int score[NumTurns]; //使用该常量
...
}

注意,取一个 const 的地址合法,但是取一个 enum 或 #define 的地址则不合法。

  1. inline
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累加一次

对于该示例,宏不是函数,不会有函数调用带来的额外开销;宏中所有的实参需要加上小括号;对于使用宏带来的问题,可采用 template inline实现,如下:

1
2
3
4
5
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
  • 对于单纯常量,最好以 const 对象或 enunms 替换 #define。
  • 对于形似函数的宏,最好改用 inline 函数替换 #define。

03:尽可能使用const

  1. const 指针
1
2
3
4
char greeting[] = "Hello";
const char* p = greeting; //指针指向的值是常量
char* const p = greeting; //指针是常量
const char* const p = greeting;

const 允许指定一个语义约束,而编译器会强制实施该约束,使某值保持不变;

STL迭代器的作用类似 T* 指针,如果声明为const表示该指针的常量,如果使得指向的值为常量,需使用const_iterator,如下:

1
2
3
4
5
6
std::vector<int> vec;
...
const std::vector>int::iterator iter = vec.begin();
++iterz; //错误,iter是常量指针
std::vector>int::const_iterator cIter = vec.begin();
*vIter = 10; //错误,cIter指向的值是常量
  1. const 成员函数

将const作用于成员函数,可使class接口易于理,且使“操作const对象”成为可能;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CTextBlock {
public:
...
std::size_t length() const;
private:
char* pText;
std::size_t textLength; //最近一次计算的文本区块长度
bool lengthIsValid; //目前的长度是否有效
};

std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); //错误,const成员函数内
lengthIsValid = true; //不能赋值给 textLength、lengthIsValid
}
return textLength;
}

上诉问题的解决办法可添加 mutable,如下:

1
2
3
4
5
6
7
8
9
class CTextBlock {
public:
...
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength; //成员变量可能总被变更
mutable bool lengthIsValid;
};

对于是否带有 const 可实现重载,对于出现的代码重复,可通过转型避免。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TextBlock {
punlic:
...
const char& operator[] (std::size_t position) const
{
...
return text[Position];
}
char& operator[] (std::size_t position)
{
return const_cast<char&>( //移除const
static_cast<const TextBlock&>(*this) //加上const
[position]
);
}
...
};

该代码有两个转型动作,第一个,将原始类型 TextBlock& 转换为 const TextBlock&,从而得以调用 const operator[];第二次则从返回值中移除const。对于反向做法:令 const 版本调用 non-const 版本,则是一种错误行为,存在安全性风险。

  • 将某些东西声明为 const 可帮助编译器侦测出错误用法。
  • 编译器强制实施 bitwise constness,但编写程序应使用“概念上的常量性”。(实在无法理解原文中这些概念的描述)
  • 当 const 和 non-const 成员函数等价时,令 non-const 调用 const 版本可避免代码重复。

04:确定对象被使用前已被初始化

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

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

读取未初始化的值会导致不明确的行为,通常 C part of C++ 不保证发生初始化;

对于内置类型以外的东西,初始化通过构造函数实现,但需要分清赋值和初始化,如下实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PhoneNumber { ... };
class ABEntry {
public:
ABEntry(const std::string& name, const std::list<PhoneNumber>& Phones);
private:
std::string theNumber;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};

//此种方式为赋值操作
ABEntry::ABEntry(const std::string& name, const std::list<PhoneNumber>& Phones)
{
theNumber = name;
thePhones = Phones;
numTimesConsulted = 0;
}

//此种方式为初始化操作(使用成员初值列)
ABEntry::ABEntry(const std::string& name, const std::list<PhoneNumber>& Phones)
:theNumber(name),
thePhones(Phones),
numTimesConsulted(0)
{}

赋值的构造函数实际为先调用默认构造函数,再进行赋值操作,因此使用成员初值列来初始化的方式要高效的多;对于内置对象如 numTimesConsulted,则没有区别。
C++的成员初始化顺序往往是固定的,以其声明的顺序被初始化,与初值列的顺序无关;此外,对于如果一个成员变量的初始化需要依赖另外一个成员变量,如数组的长度,则那个成员变量需要先有初值。

函数内的 static 对象被称为 local static 对象,其他的 static 对象被称为 non-local static 对象,程序结束时 static 对象会自动销毁。编译单元是指产出单一目标文件的那些源码。
问题:某一个编译单元某个 non-local static 对象的初始化用到了另一个编译单元的 non-local static 对象,它所用到的这个对象可能尚未被初始化。
这里采用的设计是将每个 non-local static 对象搬到自己的专属函数内,该对象被声明为 static,函数返回一个指向该对象的指针,然后用户调用这些函数,而不是这些对象,即 non-local static 对象 被 local static 对象替换了。这类函数称为 reference-returning 函数。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//第一个编译单元
class FileSystem { ... };
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}

//第二个编译单元
class Directory { ... };
Directory::Directory(param)
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td;
return td;
}

然而,这种写法使得在多线程中带有不确定性,一种处理方法是在单线程启动阶段手工调用所有 reference-returning 函数;另外需要的则是加强设计以避免这类情况。

为内置对象进行手工初始化,因为 C++ 不保证初始化它们。
构造函数最好使用成员初值列,而不是在构造函数内使用赋值操作。
为避免“跨编译单元之初始化次序”问题,请以 local static 对象代替 non-local static 对象。