这是一个所有程序员都应该了解的小型的 C++ 词汇表。下面的条目都足够重要,值得我们对它们的含义务必取得完全一致。
声明(declaration)告诉编译器关于某物的名字和类型,但它省略了某些细节。以下这些都是声明:
extern int x; // object declaration
std::size_t numDigits(int number); // function declaration
class Widget; // class declaration
template<typename T> // template declaration
class GraphNode; // (see Item 42 for info on
// the use of "typename")
注意:即使是内建类型,我还是更喜欢将整数看作一个 "object",某些人将 "object" 这个名字保留给用户定义类型,但我不是他们中的一员。再有就是注意函数 numDigits 的返回类型是 std::size_t,也就是说,namespace std 中的 size_t 类型。这个 namespace 是 C++ 标准库中每一样东西实际所在的地方。但是,因为 C 标准库(严谨地说,来自于 C89)在 C++ 中也能使用,从 C 继承来的符号(诸如 size_t)可能存在于全局范围,std 内部,或两者都有,这依赖于哪一个头文件被 #included。在本书中,我假设 C++ 头文件被 #included,这也就是为什么我用 std::size_t 代替 size_t 的原因。当行文中涉及到标准库组件时,我一般不再提及 std,这依赖于你认可类似 size_t,vector,以及 cout 之类的东西都在 std 中,在示例代码中,我总是包含 std,因为真正的代码没有它将无法编译。
顺便说一下,size_t 仅仅是某些供 C++ 对某物计数时使用的 unsigned 类型的 typedef(例如,一个基于 char* 的 string 中字符的个数,一个 STL 容器中元素的个数,等等)。它也是 vector,deque,以及 string 的 operator[] 函数所持有的类型,这是一个在 Item 3 中定义我们自己的 operator[] 函数时将要遵守的惯例。
每一个函数的声明都表明了它的识别标志(signature),也就是它的参数和返回类型。一个函数的识别标志(signature)与它的类型相同。对于 numDigits 的情况,识别标志(signature)是 std::size_t (int),也就是说,“函数持有一个 int,并返回一个 std::size_t”。官方的“识别标志(signature)”的 C++ 定义排除了函数的返回类型,但是在本书中,将返回类型考虑为识别标志的一部分更加有用。
定义(definition)为编译器提供在声明时被省略的细节。对于一个对象,定义是编译器为对象留出内存的地方。对于一个函数或一个函数模板,定义提供代码本体。对于一个类或一个类模板,定义列出了类或者模板的成员:
int x; // object definition
std::size_t numDigits(int number) // function definition.
{
// (This function returns
std::size_t digitsSoFar = 1; // the number of digits
// in its parameter.)
while ((number /= 10) != 0) ++digitsSoFar;
return digitsSoFar;
}
class Widget {
// class definition
public:
Widget();
~Widget();
...
};
template<typename T> // template definition
class GraphNode {
public:
GraphNode();
~GraphNode();
...
};
初始化(Initialization)是设定一个对象的第一个值的过程。对于用户定义类型的对象,初始化通过构造函数完成任务。缺省构造函数(default constructor)就是不需要任何引数(arguments)就可以调用的构造函数。这样的一个构造函数既可以是没有参数(parameters),也可以是每一个参数都有缺省值:
class A {
public:
A(); // default constructor
};
class B {
public:
explicit B(int x = 0, bool b = true); // default constructor; see below
}; // for info on "explicit"
class C {
public:
explicit C(int x); // not a default constructor
};
这里 B 和 C 的构造函数都被声明为 explicit(显式的)。这是为了防止它们被用来执行隐式类型转换(implicit type conversions),虽然他们还可以被用于显示类型转换(explicit type conversions):
void doSomething(B bObject); // a function taking an object of
// type B
B bObj1; // an object of type B
doSomething(bObj1); // fine, passes a B to doSomething
B bObj2(28); // fine, creates a B from the int 28
// (the bool defaults to true)
doSomething(28); // error! doSomething takes a B,
// not an int, and there is no
// implicit conversion from int to B
doSomething(B(28)); // fine, uses the B constructor to
// explicitly convert (i.e., cast) the
// int to a B for this call. (See
// Item 27 for info on casting.)
构造函数被声明为 explicit(显式的)通常比 non-explicit(非显式)的更可取,因为它们可以防止编译器执行意外的(常常是无意识的)类型转换。除非我有一个好的理由允许一个构造函数被用于隐式类型转换(implicit type conversions),否则就将它声明为 explicit(显式的)。我希望你能遵循同样的方针。
构造函数被声明为 explicit(显式的)通常比 non-explicit(非显式)的更可取,因为它们可以防止编译器执行意外的(常常是无意识的)类型转换。除非我有一个好的理由允许一个构造函数被用于隐式类型转换(implicit type conversions),否则就将它声明为 explicit(显式的)。我希望你能遵循同样的方针。
请注意我是如何突出上面的示例代码中的强制转换(cast)的。贯穿本书,我用这样的突出引导你注意那些应该注意的材料。(我也突出章节号码,但那仅仅是因为我想让它好看一些。)
拷贝构造函数(copy constructor)被用来以一个对象来初始化同类型的另一个对象,拷贝赋值运算符(copy assignment operator)被用来将一个对象中的值拷贝到同类型的另一个对象中:
class Widget {
public:
Widget(); // default constructor
Widget(const Widget& rhs); // copy constructor
Widget& operator=(const Widget& rhs); // copy assignment operator
...
};
Widget w1; // invoke default constructor
Widget w2(w1); // invoke copy constructor
w1 = w2; // invoke copy
// assignment operator
当你看到什么东西看起来像一个赋值的话,要仔细阅读,因为 "=" 在语法上还可以被用来调用拷贝构造函数:
Widget w3 = w2; // invoke copy constructor!
幸运的是,拷贝构造函数很容易从拷贝赋值中区别出来。如果一个新的对象被定义(就象上面那行代码中的 w3),一个构造函数必须被调用;它不可能是一个赋值。如果没有新的对象被定义(就象上面那行 "w1 = w2" 代码中),没有构造函数能被调用,所以它就是一个赋值。
拷贝构造函数是一个特别重要的函数,因为它定义一个对象如何通过传值的方式被传递。例如,考虑这个:
bool hasAcceptableQuality(Widget w);
...
Widget aWidget;
if (hasAcceptableQuality(aWidget)) ...
参数 w 通过传值的方式被传递给 hasAcceptableQuality,所以在上面的调用中,aWidget 被拷贝给 w。拷贝动作通过 Widget 的拷贝构造函数被执行。通过传值方式传递意味着“调用拷贝构造函数”。(无论如何,通过传值方式传递用户定义类型通常是一个不好的想法,传引用给 const 通常是更好的选择。)
STL 是标准模板库(Standard Template Library),作为 C++ 的标准库的一部分,致力于容器(containers)(例如,vector,list,set,map,等等),迭代器(iterators)(例如,vector<int>::iterator,set<string>::iterator,等等),算法(algorithms)(例如,for_each,find,sort,等等),以及相关机能。相关机能中的很多都通过函数对象(function objects)——行为表现类似于函数的对象——提供。这样的对象来自于重载了 operator() ——函数调用运算符——的类,如果你不熟悉 STL,在读本书的时候,你应该有一本像样的参考手册备查,因为对于我来说 STL 太有用了,以至于不能不利用它。一但你用了一点点,你也会有同样的感觉。
从 Java 或 C# 那样的语言来到 C++ 的程序员可能会对未定义行为(undefined behavior)的概念感到吃惊。因为各种各样的原因,C++ 中的一些结构成分(constructs)的行为没有确切的定义:你不能可靠地预知运行时会发生什么。这里是两个带有未定义行为的代码的例子:
int *p = 0; // p is a null pointer
std::cout << *p; // dereferencing a null pointer
// yields undefined behavior
char name[] = "Darla"; // name is an array of size 6 (don’t
// forget the trailing null!)
char c = name[10]; // referring to an invalid array index
// yields undefined behavior
为了强调未定义行为的结果是不可预言而且可能是令人讨厌的,有经验的 C++ 程序员常常说带有未定义行为的程序能(can)删除你的硬盘。这是真的:一个带有未定义行为的程序可以(could)删除你的硬盘。只不过可能性不太大。更可能的是那个程序的表现反复无常,有时会运行正常,有时会彻底完蛋,还有时会产生错误的结果。有实力的 C++ 程序员能以最佳状态避开未定义行为。本书中,我会指出许多你必须要注意它的地方。
另一个可能把从其它语言转到 C++ 的程序员搞糊涂的条目是接口(interface)。Java 和 .NET 的语言都将接口作为一种语言要素,但是在 C++ 中没有这种事。当我使用条目“接口(interface)”时,一般情况下我说的是一个函数的识别标志,是一个类的可访问元素(例如,一个类的 "public interface","protected interface",或 "private interface"),或者是对一个模板的类型参数来说必须合法的表达式。也就是说,我是作为一个相当普遍的设计概念来谈论接口(interface)的。
客户(client)是使用你写的代码(一般是接口(interfaces))的某人或某物。例如,一个函数的客户就是它的用户:调用这个函数(或持有它的地址)的代码的片段以及写出和维护这样的代码的人。类或者模板的客户是使用这个类或模板的软件的部件,以及写出和维护那些代码的程序员。在讨论客户的时候,我一般指向程序员,因为程序员会被困扰和误导,或者因为不好的接口而烦恼。但他们写的代码却不会。
你也许不习惯于为客户着想,但是我会用大量的时间试图说服你:你应该尽你所能使他们的生活更轻松。记住,你也是一个其他人开发的软件的客户。难道你不希望那些人为你把事情弄得轻松些吗?除此之外,在某种程度上,你几乎肯定能发现你自己处在了你自己的客户的位置上(也就是说,使用你写的代码),而这个时候,你会为你在开发你的接口时在头脑中保持了对客户的关心而感到高兴。
我常常掩盖函数和函数模板之间以及类和类模板之间的区别。那是因为对其中一个确定的事对另一个常常也可以确定。如果不是这样,我会区别对待类,函数,以及由类和函数产生的模板。
在代码注释中提到构造函数和析构函数时,我有时使用缩写形式 ctor 和 dtor。
Naming Conventions 命名惯例
我试图为对象,类,函数,模板等选择意味深长的名字,但是在我的某些名字后面的含义可能不会立即显现出来。例如,我特别喜欢的两个参数名字是 lhs 和 rhs。它们分别代表 "left-hand side" 和 "right-hand side"。我经常用它们作为实现二元运算符的函数(例如,operator== 和 operator*)的参数名。例如,如果 a 和 b 是代表有理数的对象,而且如果 Rational 对象能通过一个非成员的 operator* 函数相乘(Item 24 中解释的很可能就是这种情况),表达式
a * b
与函数调用
operator*(a,b)
就是等价的。
我也这样声明 operator*过:
const Rational operator*(const Rational& lhs, const Rational& rhs);
你可以看到,左手操作数(left-hand operand)a 在函数内部以 lhs 的面目出现,而右手操作数(right-hand operand)b 以 rhs 的面目出现。
对于成员函数左手参数(left-hand argument)表现为 this 指针,所以有时候我单独使用参数名 rhs。你可能已经在第 5 页中某些 Widget 成员函数的声明(本文介绍拷贝构造函数的那一段中的例子——译者注)中注意到了这一点。这一点提醒了我。我经常在示例中使用 Widget 类。"Widget" 并不意味着什么东西。它仅仅是在我需要一个示例类的名字的时候不时地使用一下的名字。它和 GUI 工具包中的 widgets 没有任何关系。
我经常遵循这个规则为指针命名:一个指向类型 T 的对象的指针被称为 pt,"pointer to T"。以下是例子:
Widget *pw; // pw = ptr to Widget
class Airplane;
Airplane *pa; // pa = ptr to Airplane
class GameCharacter;
GameCharacter *pgc; // pgc = ptr to GameCharacter
我对引用使用类似的惯例:rw 可以认为是一个引向 Widget 的引用,而 ra 是一个引向 Airplane 的引用。
在我讨论成员函数的时候我偶尔会使用名字 mf。
Threading Considerations 对线程的考虑
作为一种语言,C++ 没有线程的概念——实际上,是没有任何一种并发的概念。对于 C++ 标准库也是同样如此。就 C++ 涉及的范围而言,多线程编程并不存在。
而且至今它们依然如此。我致力于让此书基于标准的,可移植的 C++,但我也不能对线程安全(thread safety)已成为很多程序员所面临的一个问题的事实视而不见。我对付这个标准 C++ 和现实之间的裂痕的方法就是指出某处的 C++ 结构成分(constructs)以我的分析很可能在多线程环境中引起问题的地方。这样不但不会使本书成为一本用 C++ 进行多线程编程的书。反而,它更会使本书在相当程度上成为这样一本 C++ 编程的书:将自己在很大程度上限制于单线程考虑,承认多线程的存在,并试图指出有线程意识的程序员需要特别当心评估我提供的建议的地方。
如果你不熟悉多线程编程或者不必为此担心,你可以忽略我关于线程的讨论。如果你正在编写一个多线程的应用或库,无论如何,请记住我的评注和并将它作为你使用 C++ 时需要致力去解决的问题的起点。
TR1 and Boost TR1 和 Boost
你会发现提及 TR1 和 Boost 的地方遍及整个系列。它们每一个都专门在某些细节上进行描述,但是,不幸的是,这些都在整个系列的最后。(他们在那里是因为那样更好一些,我确实试过很多其它的地方。)如果你愿意,但是如果你更喜欢从本书的起始处而不是结尾处开始,以下摘要会对你有所帮助:
·TR1 ("Technical Report 1") 是被加入 C++ 标准库的新机能的规格说明书。这些机能以新的类和函数模板的形式提供了诸如哈希表(hash tables),引用计数智能指针(reference-counting smart pointers),正则表达式(regular expressions),等等。所有的 TR1 组件都位于嵌套在 namespace std 内部的 namespace tr1 内。
·Boost 是一个组织和一个网站 (http://boost.org) 提供的可移植的,经过同行评审的,开源的 C++ 库。大多数 TR1 机能都基于 Boost 的工作,而且直到编译器厂商在他们的 C++ 库发行版中包含 TR1 之前,Boost 网站很可能会保持开发者寻找 TR1 实现的第一站的地位。Boost 提供的东西比用于 TR1 的更多,无论如何,在很多情况下,它还是值得去了解一下的。