這是一個所有程式員都應該瞭解的小型的 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),Regex(regular expressions),等等。所有的 TR1 組件都位於嵌套在 namespace std 內部的 namespace tr1 內。
·Boost 是一個組織和一個網站 (http://boost.org) 提供的可移植的,經過同行評審的,開源的 C++ 庫。大多數 TR1 機能都基於 Boost 的工作,而且直到編譯器廠商在他們的 C++ 庫發行版中包含 TR1 之前,Boost 網站很可能會保持開發人員尋找 TR1 實現的第一站的地位。Boost 提供的東西比用於 TR1 的更多,無論如何,在很多情況下,它還是值得去瞭解一下的。