Item 1: 将 C++ 视为 federation of languages(语言联邦)
不要将 C++ 视为一个由相关语言组成的联邦,而非单一的语言。在每一个特定的 sublanguage(子语言)中,它的特性趋向于直截了当,简单易记。但你从一个 sublanguage(子语言)转到另外一个,它的规则也许会发生变化。为了感受 C++,你必须将它的主要的 sublanguages(子语言)组织到一起。幸运的是,它只有 4 个:
· C ——归根结底,C++ 依然是基于 C 的。blocks(模块),statements(语句),preprocessor(预处理器),built-in data types(内建数据类型),arrays(数组),pointers(指针)等等,全都来自于 C。在很多方面。C++ 提出了比相应的 C 版本更高级的解决问题的方法(例如,参见 Item 2(选择 preprocessor(预处理器))和 13(使用 objects(对象)管理 resources(资源))),但是,当你发现你自己工作在 C++ 的 C 部分时,effective programming(高效编程)的规则表现了 C 的诸多限制范围:没有 templates(模板),没有 exceptions(异常),没有 overloading(重载)等等。
· Object-Oriented C++ —— C++ 的这部分就是 C with Classes 涉及到的全部:classes(类)(包括构造函数和析构函数),encapsulation(封装),inheritance(继承),polymorphism(多态),virtual functions (dynamic binding)(虚拟函数(动态绑定))等。C++ 的这一部分直接适用于 object-oriented design(面向对象设计)的经典规则。
· Template C++ ——这是 C++ 的 generic programming(泛型编程)部分,大多数程序员对此都缺乏经验。template(模板)的考虑已遍及 C++,而且好的编程规则中包含特殊的 template-only(模板专用)条款已不再不同寻常(参见 Item 46 通过调用 template functions(模板函数)简化 type conversions(类型转换))。实际上,templates(模板)极为强大,它提供了一种全新的 programming paradigm(编程范式)—— template metaprogramming (TMP) (模板元编程)。Item 48 提供了一个 TMP 的概述,但是,除非你是一个 hard-core template junkie(死心塌地的模板瘾君子),否则你不需在此费心,TMP 的规则对主流的 C++ 编程少有影响。
· STL —— STL 是一个 template library(模板库),但它一个非常特殊的 template library(模板库)。它将 containers(容器),iterators(迭代器),algorithms(算法)和 function objects(函数对象)非常优雅地整合在一起,但是。templates(模板)和 libraries(库)也可以围绕其它的想法建立起来。STL 有很多独特的处事方法,当你和 STL 一起工作,你需要遵循它的规则。
当你从一种 sublanguage(子语言)转到另一种时,为了高效编程你需要改变你的策略!!
Item 2: 尽量用consts, enums 和 inlines 取代 #defines
· 对于 simple constants(简单常量),用 const objects(const 对象)或 enums(枚举)取代 #defines。
· 对于 function-like macros(类似函数的宏),用 inline functions(内联函数)取代 #defines。
以下是一个用较大的宏参数调用函数 f 的 macro(宏):
// call f with the maximum of a and b
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
你可以通过一个 inline function(内联函数)的 template(模板)来获得 macro(宏)的效率,以及完全可预测的行为和常规函数的类型安全(参见 Item 30):
template<typename T> // because we don't
inline void callWithMax(const T& a, const T& b) // know what T is, we
{ // pass by reference-to-
f(a > b ? a : b); // const - see Item 20
}
Item 3: 尽可能使用 const
1.用于修饰指针
对于 pointers(指针),你可以指定这个 pointer(指针)本身是 const,或者它所指向的数据是 const,或者两者都是,或者都不是:
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
STL iterators(迭代器)以 pointers(指针)为原型,所以一个 iterator 在行为上非常类似于一个 T* pointer(指针)。声明一个 iterator 为 const 就类似于声明一个 pointer(指针)为 const(也就是说,声明一个 T* const pointer(指针)):不能将这个 iterator 指向另外一件不同的东西,但是它所指向的东西本身可以变化。如果你要一个 iterator 指向一个不能变化的东西(也就是一个 const T* pointer(指针)的 STL 对等物),你需要一个 const_iterator:
std::vector<int> vec;
...
const std::vector<int>::iterator iter = // iter acts like a T* const
vec.begin();
*iter = 10; // OK, changes what iter points to
++iter; // error! iter is const
std::vector<int>::const_iterator cIter = // cIter acts like a const T*
vec.begin();
*cIter = 10; // error! *cIter is const
++cIter; // fine, changes cIter
2. 用于修饰函数:在一个 function declaration(函数声明)中,const 既可以用在函数的 return value(返回值)上,也可以用在个别的 parameters(参数)上,对于 member functions(成员函数),还可以用于整个函数。
member functions(成员函数)被声明为 const 的目的是标明这个 member functions(成员函数)可能会被 const objects(对象)调用。因为两个原因,这样的 member functions(成员函数)非常重要。首先,它使一个 class(类)的 interface(接口)更容易被理解。知道哪个函数可以改变 object(对象)而哪个不可以是很重要的。第二,它们可以和 const objects(对象)一起工作。因为,书写高效代码有一个很重要的方面,就像 Item 20 所解释的,提升一个 C++ 程序的性能的基本方法就是 pass objects by reference-to-const(以传引用给 const 的方式传递一个对象)。这个技术只有在 const member functions(成员函数)和作为操作结果的 const-qualified objects(被 const 修饰的对象)存在时才是可行的。
两个成员函数如果只是常量性不同,可以被重载。
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 的 operator[]s 可能会这样使用:
TextBlock tb("Hello");
std::cout << tb[0]; // calls non-const
// TextBlock::operator[]
const TextBlock ctb("World");
std::cout << ctb[0]; // calls const TextBlock::operator[]
3.bitwise constness 和 logical constness
bitwise constness: 不改变对象的每一个bit。(问题在于如果你的const成员函数返回一个指向class内部数据成员的句柄,则其他代码可以通过该句柄在外部修改该数据)
Logical constness: 一个 const member function(成员函数)可能会改变调用它的 object(对象)中的一些 bits(二进制位),但是只能用客户无法察觉的方法。(使用mutable来修饰可在const成员函数内被修改的变量)
4.在const和non-const成员函数中避免重复
假设 TextBlock(包括 CTextBlock)中的 operator[] 不仅要返回一个适当的字符的 reference(引用),它还要进行 bounds checking(边界检查),logged access information(记录访问信息),甚至 data integrity validation(数据完整性确认),将这些功能都加入到 const 和 non-const 的 operator[] 函数中,使它们变成如下这样的庞然大物:
class TextBlock {
public:
...
const char& operator[](std::size_t position) const
{
... // do bounds checking
... // log access data
... // verify data integrity
return text[position];
}
char& operator[](std::size_t position)
{
... // do bounds checking
... // log access data
... // verify data integrity
return text[position];
}
private:
std::string text;
};
重复代码带来更长的编译时间,维护,代码膨胀等问题。解决办法一是将 bounds checking(边界检查)等全部代码转移到一个单独的 member function(成员函数)(自然是 private(私有)的)中,并让两个版本的 operator[] 来调用它,但是,你还是要重复写出调用那个函数和 return 语句的代码。
最简单的办法是只实现一次 operator[] 的功能,而使用两次。换句话说,你可以用一个版本的 operator[] 去调用另一个版本。并可以为我们 casting away(通过强制转型脱掉)constness(常量性)。
作为一个通用规则,casting(强制转型)是一个非常坏的主意,我会投入整个一个 Item 的篇幅来告诉你不要使用它(Item 27),但是 code duplication(重复代码)也不是什么好事。在当前情况下,const 版本的 operator[] 所做的事也正是 non-const 版本所做的,仅有的不同是它有一个 const-qualified return type(被 const 修饰的返回类型)。在这种情况下,casting away(通过强制转型脱掉)return value(返回类型)的 const 是安全的,因为,无论谁调用 non-const operator[],首先要有一个 non-const object(对象)。否则,它不能调用一个 non-const 函数。所以,即使需要一个 cast(强制转型),让 non-const operator[] 调用 const 版本也是避免重复代码的安全方法。代码如下,你读了后面的解释后对它的理解可能会更加清晰:
class TextBlock {
public:
...
const char& operator[](std::size_t position) const // same as before
{
...
...
...
return text[position];
}
char& operator[](std::size_t position) // now just calls const op[]
{
return
const_cast<char&>( // cast away const on
// op[]'s return type;
static_cast<const TextBlock&>(*this) // add const to *this's type;
[position] // call const version of op[]
);
}
...
};
正如你看到的,代码中有两处 casts(强制转型),而不是一处。我们让 non-const operator[] 调用 const 版本,但是,如果在 non-const operator[] 的内部,我们仅仅是调用 operator[],那我们将递归调用我们自己。它会进行一百万次甚至更多。为了避免 infinite recursion(无限递归),我们必须明确指出我们要调用 const operator[],但是没有直接的办法能做到这一点,于是我们将 *this 从 TextBlock& 的自然类型强制转型到 const TextBlock&。是的,我们使用 cast(强制转型)为它加上了 const!所以我们有两次 casts(强制转型):第一次是为 *this 加上 const(以便在我们调用 operator[] 时调用它的 const 版本),第二次是从 const operator[] 的 return value(返回值)之中去掉 const。
注意,去掉 const 只能经由 const_cast 来完成,所以在这里我们没有别的选择。(在技术上,我们有。一个 C-style cast(C 风格的强制转型)也能工作,但是,就像我在 Item 27 中解释的,这样的 casts(强制转型)很少是一个正确的选择。
另外,反向做法——通过用 const 版本调用 non-const 版本来避免重复——是你不能做的。记住,一个 const member function(成员函数)承诺绝不会改变它的 object(对象)的逻辑状态,但是一个 non-const member function(成员函数)不会做这样的承诺。如果你从一个 const member function(成员函数)调用一个 non-const member function(成员函数),你将面临你承诺不会变化的 object(对象)被改变的风险。
总结:
将某些东西声明为 const 有助于编译器发现使用错误。const 能被用于任何作用域中的 object(对象),用于 function parameters(函数参数)和 return types(返回类型),用于整个 member functions(成员函数)。
编译器坚持 bitwise constness(二进制位常量性),但是你应该用 conceptual constness(概念上的常量性)来编程。
当 const 和 non-const member functions(成员函数)具有本质上相同的实现的时候,使用 non-const 版本调用 const 版本可以避免 code duplication(代码重复)。
Item 4: 确保 objects(对象)在使用前被初始化
总是在使用之前初始化你的对象。
对于 built-in types(内建类型)的 non-member objects(非成员对象),需要你手动做这件事。
对于自定义对象,确保 all constructors(所有的构造函数)都初始化了 object(对象)中的每一样东西。
这个规则很容易遵守,但重要的是不要把 assignment(赋值)和 initialization(初始化)搞混。考虑下面这个表现一个通讯录条目的 class(类)的 constructor(构造函数):
class PhoneNumber { ... };
class ABEntry { // ABEntry = "Address Book Entry"
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 num TimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
theName = name; // these are all assignments,
theAddress = address; // not initializations
thePhones = phones;
numTimesConsulted = 0;
}
这样做虽然使得 ABEntry objects(对象)具有了你所期待的值,但还不是最好的做法。C++ 的规则规定一个 object(对象)的 data members(数据成员)在进入 constructor(构造函数)的函数体之前被初始化。在 ABEntry 的 constructor(构造函数)内,theName,theAddress 和 thePhones 不是 being initialized(被初始化),而是 being assigned(被赋值)。initialization(初始化)发生得更早——在进入 ABEntry 的 constructor(构造函数)的函数体之前,它们的 default constructors(缺省的构造函数)已经被自动调用。但不包括 numTimesConsulted,因为它是一个 built-in type(内建类型)。不能保证它在被赋值之前被初始化。
一个更好的写 ABEntry constructor(构造函数)的方法是用 member initialization list(成员初始化列表)来代替 assignments(赋值):
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
: theName(name),
theAddress(address), // these are now all initializations
thePhones(phones),
numTimesConsulted(0)
{} // the ctor body is now empty
这个 constructor(构造函数)的最终结果和前面那个相同,但是通常它有更高的效率。assignment-based(基于赋值)的版本会首先调用 default constructors(缺省构造函数)初始化 theName,theAddress 和 thePhones,然而很快又在 default-constructed(缺省构造)的值之上赋予新值。那些 default constructions(缺省构造函数)所做的工作被浪费了。而 member initialization list(成员初始化列表)的方法避免了这个问题,因为 initialization list(初始化列表)中的 arguments(参数)就可以作为各种 data members(数据成员)的 constructor(构造函数)所使用的 arguments(参数)。在这种情况下,theName 从 name 中 copy-constructed(拷贝构造),theAddress 从 address 中 copy-constructed(拷贝构造),thePhones 从 phones 中 copy-constructed(拷贝构造)。对于大多数类型来说,只调用一次 copy constructor(拷贝构造函数)的效率比先调用一次 default constructor(缺省构造函数)再调用一次 copy assignment operator(拷贝赋值运算符)的效率要高(有时会高很多)。
non-local static 的初始化顺序问题:
用local static对象替换non-local static 对象。
FileSystem& tfs() // this replaces the tfs object; it could be
{ // static in the FileSystem class
static FileSystem fs; // define and initialize a local static object
return fs; // return a reference to it
}
避免在初始化之前使用 objects(对象),你只需要做三件事。首先,手动初始化 built-in types(内建类型)的 non-member objects(非成员对象)。第二,使用 member initialization lists(成员初始化列表)初始化一个 object(对象)的所有部分。最后,为免除“跨编译单元之初始化次序”问题,以local static对象来代替non-local static对象。