Effective C++, 3/e
55 Specific Ways to Improve Your Program and Design§

作者:Scott Meyers
出版时间:2005
中文译者:侯捷
熟悉书中的C++编码规则,既可以提高代码性能、编码效率,同时有助于发现考虑不周、不合理的C++程序设计。
Chapter 0. Introduction§
对构造函数使用explicit
,阻止非预期的隐式类型转换。
使用=
操作符,如果是定义一个新对象,将会调用拷贝构造函数;如果对象已经定义,将会调用拷贝赋值操作符函数。
Chapter 1. Accustoming Yourself to C++§
Item 01 View C++ as a federation of language.§
C++是多范型(multiparadigm)编程语言。过程式,面向对象,函数式,泛型,元编程(procedural,object-oriented,functional,generic,metaprogramming)。
可以将C++看做由C,Object-Oriented C++,Template C++,STL四个部分组成,按需使用。
Item 02 Prefer consts, enums, and inlines to #defines.§
使用inline
函数而非由#define
实现的宏函数§
- 预编译器处理,对编译器不可见,记录在符号表中,难以追踪调试。
- 不受作用域影响,一旦定义了宏,在之后的程序中始终有效,除非使用
#undef
解除。 - 不具备封装特性,也不能设置
private
访问属性。 - 预编译器近似于完成字符串替换,无法保证类型安全。两个例子:
- 每个变量参数和宏函数整体需要使用圆括号
()
包裹。避免预编译器处理后,原地的运算优先级发生变化。 - 当宏函数的一个变量参数为
++i
这样带有运算的参数时,假定接收参数为a
,那么宏函数内部多次出现a
将导致出现非预期的运算。
- 每个变量参数和宏函数整体需要使用圆括号
in-class 的初值设定§
使用static const
变量作为class
专属常量。编译器可能不支持static
成员仅在声明式上初始化,需要额外的类外定义但不赋值。另外,in-class 的初值设定仅允许整数型常量。
enum hack§
当在class
编译期间需要一个class
常量值时,比如静态数组的大小。如果编译器不能支持 in-class 的初值设定,可以进行 enum hack。一个属于枚举类型的数值可以充当int
被使用。这样使用enum
很像在class
中使用#define
。而且,使用enum
可以避免整数型常量被用于指针或引用。
class Company {
private:
enum { Nums = 50 };
int peoples[Nums];
...
};
enum hack 也是模板元编程中的常用手段。
Item 03 Use const whenever possible.§
区分不同顺序的*
和const
同时修饰的类型§
const char*
等价于char const*
,char型常量的指针。数据是常量,不可修改,指针可以修改。char* const
,char型的常量指针。数据可以修改,指针是常量,不可修改。
区分STL中两种const相关的迭代器§
const ::std::vector<int>::iterator
,迭代器为常量,不可移动,可修改数据。类似于T* const
。::std::vector<int>::const_iterator
,迭代器指向数据为常量,不可修改,迭代器可移动。类似于const T*
。
bitwise/physical constness§
C++所使用的常量性定义,const
成员函数不能修改对象内任何non-static
成员变量。
常量指针是这种定义下的不足,修改指针指向的数据并不会被编译器发现。
logical/conceptual constness§
变量可以具有常量属性。作为类的成员,仍可以被const
函数修改。需要使用mutable
修饰类型。
尽管编译器采用 bitwise constness,但编写程序时应考虑这种概念上的常量性。
在non-const
成员函数中调用const
成员函数§
避免const
和non-const
成员函数的重复实现。
首先将non-const
对象转换为const
对象,然后移除返回类型的const
修饰。
class Company {
public:
...
const int& operator[] (::std::size_t pos) const {
...
return rank[pos];
}
int& operator[] (::std::size_t pos) {
return
const_cast<int&>(
static_cast<const Company&>(*this)[pos]
);
}
...
};
Item 04 Make sure that objects are initialized before they're used.§
static
对象的初始化顺序问题§
- 对于C++中的C部分,为了避免运行期成本,实现上并不保证进行初始化。而对于C++中的STL部分,总可以保证初始化。
- 编译单元(translation unit)是指产出单一目标文件的源码文件,指一个源码文件自身及其包含的所有头文件。
- 有别于基于堆栈分配的对象,
static
对象的生命周期从构造出来直到程序结束。 - 函数内的
static
对象称为local static对象,其它static
对象为non-local static对象。 - C++并没有定义不同编译单元的non-local static对象的初始化顺序。
如何初始化对象§
-
在使用对象前,首先要初始化,对于内置类型(built-in types),需要手动初始化。
-
区分赋值和初始化。对于用户自定义类型(user-defined types),如果未在初始化列表中指定初值,将会自动调用
default
构造函数,再完成构造函数中的赋值操作。因此,构造函数最好使用成员初始化列表,避免构造函数体中的赋值操作。 -
对于多个编译单元中的non-local static对象。可以将其封装在一个函数中,在函数中将该对象声明为
static
,函数返回相应的local static对象的引用。这种手法类似于Singleton模式的一种实现。C++能够保证,函数内的local static对象在函数被调用时完成初始化。由于
static
对象总是线程不安全的,对所有返回static
对象引用的函数,可以在程序的单线程阶段进行手动调用,以消除初始化时的race conditions。
Chapter 2. Constructors, Destructors, and Assignment Operators§
Item 05 Know what functions C++ silently writes and calls.§
-
编译器能够生成
default
构造函数,析构函数,拷贝构造函数,拷贝赋值操作符函数。均为public
且inline
的函数。 -
编译器生成的析构函数为non-virtual,虚属性(virtualness)取决于基类的析构函数是否为
virtual
。 -
类中只要声明了一个构造函数,编译器就不会再创建
default
构造函数。 -
当一个类中存在引用成员或
const
成员时,修改引用和const
的成员不合法,编译无法通过。 -
当基类中的拷贝赋值操作符访问属性为
private
时,编译器将无法为派生类生成拷贝赋值操作符,编译失败。
Item 06 Explicitly disallow the use of compiler-generated functions you do not want.§
禁用默认生成的函数§
- 对于拷贝构造函数以及拷贝赋值操作符,可以在
private
中进行声明(可不写参数名),并且不实现。 - 按以上描述实现一个基类
Uncopyable
,继承以阻止拷贝。Boost提供了一个名为noncopyable
的类可供使用。
Item 07 Declare destructors virtual in polymorphic base class.§
-
通过一个不带
virtual
析构函数的基类的指针删除派生类对象时,结果往往是派生类对象的派生成员未被销毁,内存泄漏。 -
由于STL中的
string, vector, list, set
等等均为不带virtual
析构函数的类。不应该继承这些类。 -
仅当类中至少有一个
virtual
函数时,才为其声明virtual
析构函数。 -
从基类接口的设计目的出发,如果不是为了通过基类接口处理派生类对象(多态特性),基类就不需要
virtual
析构函数。 -
并不是每一个派生类都需要
virtual
的析构函数。两个坏处:-
virtual
函数的实现通常为一个指向虚表vtbl的虚指针vptr。作为一个指针将会占用32/64bits空间。 -
引入
virtual
将导致类中结构与C产生差异,失去移植性。
-
-
需要抽象类时,可以考虑将析构函数声明为纯虚函数,同时为这个纯虚析构函数提供定义。
Item 08 Prevent exceptions from leaving destructors.§
-
析构函数不应该抛出异常,两种处理方式:
- 捕获异常,结束程序,
::std::abort()
。 - 捕获异常,记录并继续。
- 捕获异常,结束程序,
-
将析构函数中的操作封装成函数,可由用户自己调用。如用户未调用,在析构函数中再调用,如出现异常,再进行相应处理。
-
以上设计方式将可能出现异常的处理作为调用接口开放给用户,提供给用户处理,在用户未处理的情况下再考虑由析构函数进行处理。
Item 09 Never call virtual functions during construction or destruction.§
如果在基类的构造函数或析构函数中调用虚函数,对于派生类对象可能是非预期的。
- 在继承关系中,信息的可见性是单向的。对于基类来说,派生类是完全不可见的。
- 构造时,首先从基类开始,基类不知道派生类的存在;析构时,首先从派生类开始,当基类析构时,派生类成员已经不复存在。
- 确保一个类的构造函数和析构函数都没有调用
virtual
函数,并且它们调用的所有函数也服从这一条件。 - 派生类可以通过
static pricate
成员函数构造信息,在构造函数的初始化列表中传入到基类的构造函数。
Item 10 Have assignment operators return a reference to *this.§
操作符=, +=, -=, *=
等,返回一个对*this
的引用。
Item 11 Handle assignment to self in operator=.§
多个指针或引用变量可以表示同一个实际对象,对其中一个变量进行释放意味着其它变量失去有效性。
不具备自我赋值安全性的赋值运算符§
自我赋值
Bad& Bad::operator=(const Bad& rhs) {
delete pr;
pr = new Resource(rhs.pr);
return *this;
}
当Bad对象自己给自己赋值时,先将自身的成员释放了,最终得到错误的结果。
具备异常安全性的赋值运算符§
具备异常安全性通常保证具备自我赋值安全性。
Bad& Bad::operator=(const Bad& rhs) {
Resource* pOri = pr;
pr = new Resource(rhs.pr);
delete pOri;
return *this;
}
Item 12 Copy all parts of an object.§
-
拷贝函数:拷贝构造函数、拷贝赋值操作符函数。
-
拷贝函数应该确保复制对象内的所有成员变量以及所有基类的成员变量。注意:对应的调用基类的拷贝函数。
-
不应试图以一种拷贝函数调用另一种拷贝函数。对共同部分,应封装在单独的函数中,以共同调用。
Chapter 3. Resource Management§
资源:内存、文件描述符、互斥锁、数据库连接、sockets套接字等。