前言:如何有效運用C++,包括一般性的設計策略,以及帶有具體細節的特定語言特性。知道細節很重要,否則如果疏忽幾乎總是導致不可預期的程序行為(undefined behavior)。本文總結對于如何使用C++的一些建議,從而讓你成為一個有戰斗力的C++程序員。
Effective C - Accustoming Yourself to C構造函數的explicit對象的復制命名習慣TR1和Boost視C為一個語言聯邦盡量以const enum inline替換define盡量使用const確定對象被使用前已先被初始化
被聲明為explicit
的構造函數通常比non-explicit
更受歡迎,因為它們禁止編譯器執行非預期的類型轉換。除非有一個好理由允許構造函數被用于隱式類型轉換,否則把它聲明為explicit
。
copy
構造函數被用來“以同型對象初始化自我對象”,copy assignment
操作符被用來“從另一個同型對象中拷貝其值到自我對象”。
copy構造和copy賦值的區別:如果一個新對象被定義,一定會有一個構造函數被調用,不可能調用賦值操作。如果沒有新對象被定義,就不會有構造函數被調用,那么就是賦值操作被調用。
構造函數和析構函數分別使用縮寫ctor
和dtor
代替。 使用lhs
(left-hand side)和rhs
(right-hand side)表示參數名稱。
TR1
(Technical Report 1)是一份規范,描述加入C++標準程序庫的諸多新機能。這些機能以新的class templates
和function templates
形式體現。所有TR1
組件都被置于命名空間tr1
內。 Boost
是個組織,亦是一個網站,提供可移植,源代碼開放的C++程序庫。大多數TR1
機能是以Boost
的工作為基礎。
今天的C++已經是個多重范型編程語言(multiparadigm PRogramming language),一個同時支持以下特性的語言: * 過程形式(procedural) * 面向對象形式(object-oriented) * 函數形式(functional) * 泛型形式(generic) * 元編程形式(metaprogramming)
為了理解C++,你必須認識其主要的次語言(sublanguage):
C 說到底C++仍是以C為基礎。blocks, statements, preprocessor, built-in data types, arrays, pointers等統統來自C。許多時候C++對問題的解法其實不過就是較高級的C解法,但是C++提供了C沒有的templates, exceptions, overloading(重載)等功能。C語言可以重載嗎
// http://www.cplusplus.com/reference/cstdlib/qsort//* qsort example */#include <stdio.h> /* printf */#include <stdlib.h> /* qsort */int values[] = { 40, 10, 100, 90, 20, 25 };int compare (const void * a, const void * b){ return ( *(int*)a - *(int*)b );}void fun(){ printf("fun()/n");}/*$gcc -o overload_test overload_test.c overload_test.c:18:6: error: redefinition of 'fun'void fun(int a) ^overload_test.c:13:6: note: previous definition is herevoid fun() ^1 error generated. */#if 0void fun(int a){ printf("fun(int a)/n");}#endifint main (){ // 測試C語言是否支持overload重載 fun(); // C語言可以通過不同的函數指針來模擬overload重載 int n; qsort (values, 6, sizeof(int), compare); for (n=0; n<6; n++) printf ("%d ",values[n]); return 0;}Object-Oriented C++ 這部分就是C with Classes
所訴求的:
Template C++ 這是C++的泛型編程(generic programming)
部分,也是大多數程序員經驗最少的部分。
STL STL
是個template程序庫,它對containers
, iterators
, algorithms
以及function objects
的規約有極佳的緊密配合與協調。
寧可以編譯器
替換預處理器
。當你做出這樣的事情:
記號名稱ASPECT_RATIO也許從未被編譯器看見,也許在編譯器開始處理源碼之前就被預處理器替換了,于是記號名稱有可能沒有進入記號表(symbol table)
內,當你運用此常量但獲得一個編譯錯誤時可能會帶來困惑,因為這個錯誤信息提到的是1.653而不是ASPECT_RATIO。尤其是如果ASPECT_RATIO被定義在一個非你所寫的頭文件內,你肯定對1.653來自何處毫無概念。解決的方法是:以一個常量替換上述的宏(#define)
。
好處是:
作為一個語言常量,AspectRatio肯定會被編譯器看到,當然就會進入記號表內。使用常量可能比使用#define導致較小量的目標代碼,因為預處理器盲目地將宏名稱進行替換會導致目標代碼出現多份1.653,而若改用常量則不會出現。字符串常量,string
對象通常比char*-based
合適。const char* const authorName = "gerry";const std::string authorName("gerry");class專屬常量。為了將常量的作用域(scope)限制在class內,你必須讓它成為class的一個成員(member)
,另外為了保證此常量至多只有一份實體,必須讓它成為一個static成員
。#include<stdio.h>class GamePlayer {public: void set_scores() { for (int i = 0; i != NumTurns; ++i) { scores[i] = i; } } void get_scores() { for (int i = 0; i != NumTurns; ++i) { printf("%d ", scores[i]); } printf("/n"); } static int get_numturns() { //printf("addr GamePlayer::NumTurns[%p]/n", &GamePlayer::NumTurns); return GamePlayer::NumTurns; }private: static const int NumTurns = 5; // 常量聲明 int scores[NumTurns]; // 使用該常量};int main(){ printf("GamePlayer::NumTurns[%d]/n", GamePlayer::get_numturns()); GamePlayer player; player.set_scores(); player.get_scores(); GamePlayer player2; printf("player.NumTurns[%d] player2.NumTurns[%d]/n", player.get_numturns(), player2.get_numturns()); return 0;}/*GamePlayer::NumTurns[5]0 1 2 3 4 player.NumTurns[5] player2.NumTurns[5] */然而,上面你所看到的是NumTurns的聲明式
,而非定義式
。通常C++要求所使用的任何東西提供一個定義式,但如果它是class專屬常量且又是static整數類型,只要不取它們的地址,你可以聲明并使用它們而無須提供定義式。
但是,如果你需要取某個class專屬常量的地址,或者編譯器要求(比如,老編譯器)需要看到一個定義式,那么需要另外提供定義式。
#include<stdio.h>class GamePlayer {public: void set_scores() { for (int i = 0; i != NumTurns; ++i) { scores[i] = i; } } void get_scores() { for (int i = 0; i != NumTurns; ++i) { printf("%d ", scores[i]); } printf("/n"); } static int get_numturns() { printf("addr GamePlayer::NumTurns[%p]/n", &GamePlayer::NumTurns); return GamePlayer::NumTurns; }private: static const int NumTurns = 5; // 常量聲明 int scores[NumTurns]; // 使用該常量};const int GamePlayer::NumTurns; // NumTurns的定義int main(){ printf("GamePlayer::NumTurns[%d]/n", GamePlayer::get_numturns()); GamePlayer player; player.set_scores(); player.get_scores(); GamePlayer player2; printf("player.NumTurns[%d] player2.NumTurns[%d]/n", player.get_numturns(), player2.get_numturns()); return 0;}/*addr GamePlayer::NumTurns[0x102092f30]GamePlayer::NumTurns[5]0 1 2 3 4 addr GamePlayer::NumTurns[0x102092f30]addr GamePlayer::NumTurns[0x102092f30]player.NumTurns[5] player2.NumTurns[5]*/通過提供定義式,我們就可以獲取class專屬常量的地址。
注意:
NumTurns的定義式中沒有賦值是因為,class常量已在聲明時獲得了初值,因此定義時不可以再設置初值。 我們無法利用#define
創建一個class專屬常量,因為#define并不能限制作用域(scope),一旦宏被定義,它就在其后的編譯過程中有效,除非在某處被#undef
。因此,#define
不僅不能用來定義class專屬常量,也不能提供任何封裝性。 如果想具備作用域,但又不想取地址,可以使用enum
來實現這個約束。
預處理器和宏的陷阱:
宏看起來像函數,但是不會招致函數調用(function call)
帶來的額外開銷。 糟糕的做法:(有效率,但不安全)
好的做法:(效率和安全同時得到保證)
template<typename T>inline void callWithMax(const T& a, const T& b){ f(a > b ? a : b);}這個template
根據實例化可以產出一整群函數,每個函數都接受兩個同類型對象,并以其中較大的調用f。這里不需要在函數本體中為參數加上括號,也不需要操心參數被計算的次數,同時,由于callWithMax是個真正的函數,它遵守作用域和訪問規則,因此可以寫出一個class內的private inline函數,而對于宏是無法完成的。
請記住:
對于單純常量,最好以const
對象或enum
替換#define
對于形似函數的宏,最好改用inline函數
替換#define
const
允許你指定一個語義約束,也就是指定一個“不該被改動”的對象,而編譯器會強制實施該項約束。
如果關鍵字const
出現在星號左邊,表示被指物是常量;如果出現在星號右邊,表示指針自身是常量;如果出現在星號兩邊,表示被指物和指針兩者都是常量。
注意:如果被指物是常量,將關鍵字const
寫在類型之前,和寫在類型之后星號之前,這兩種寫法的意義相同。
STL迭代器系以指針為根據塑模出來,所以迭代器的作用就像個T*
指針。如果你希望迭代器所指的東西不可被改變,則需要使用const_iterator
。
const成員函數
將const
實施于成員函數的目的,是為了確認該成員函數可作用于const
對象身上。這一類成員函數之所以重要,是因為:
const
對象”成為可能,這對編寫高效代碼是個關鍵,比如,改善程序效率的一個根本方法是以pass by reference-to-const
方式傳遞對象,而此技術可行的前提是,我們有const成員函數可用來處理取得的const對象。 注意:兩個成員函數如果只是常量性不同,可以被重載(overload)。只有返回值類型不同的兩個函數不能重載(functions that differ only in their return type cannot be overloaded)。
#include<stdio.h>#include<iostream>#include<string>class TextBlock {public: TextBlock() { } TextBlock(const char* lhs) { text = lhs; }public: // operator[] for const object const char& operator[] (std::size_t position) const { return text[position]; } // operator[] for non-const object char& operator[] (std::size_t position) { return text[position]; }private: std::string text;};int main(){ TextBlock tb("gerry"); std::cout << tb[0] << std::endl; // 調用non-const TextBlock::operator[] const TextBlock ctb("yang"); // 調用const TextBlock::operator[] std::cout << ctb[0] << std::endl; return 0;}成員函數如果是const
意味著什么?—— bitwise constness或者physical constness
VS logical constness
bitwise const
指的是,成員函數只有在不更改對象之任何成員變量(static除外)時才可以說是const
,即,const成員函數不可以更改對象內任何non-static成員變量。
注意:許多成員函數雖然不完全具備const
性質,卻能通過bitwise
測試。比如,一個更改了”指針所指物”的成員函數,如果只有指針隸屬于對象,那么此函數為bitwise const
不會引發編譯器異議,但是實際不能算是const
。
下面這段代碼,可以通過bitwise
測試,但是實際上改變了對象的值。
logical constness
主張,一個const
成員函數可以修改它所處理的對象的某些bits
,但只有在客戶端偵測不出的情況才可以(即,對客戶端是透明的,但是實際上對象的某些值允許改變)。正常情況下,由于bitwise const
的約束,const
成員函數內是不允許修改non-static成員變量的,但是通過將一些變量聲明為mutable
則可以躲過編譯器的bitwise const
約束。
在const
和non-const
成員函數中避免重復
方法是:運用const
成員函數實現出其non-const
孿生兄弟。
不好的做法(因為有重復代碼):
// operator[] for const object const char& operator[] (std::size_t position) const { // bounds checking // log access data // verify data integrity // ... return text[position]; } // operator[] for non-const object char& operator[] (std::size_t position) { // bounds checking // log access data // verify data integrity // ... return text[position]; }好的做法(實現operator[]
的機能一次并使用它兩次,令其中一個調用另一個):
請記住:
將某些東西聲明為const
可幫助編譯器偵測出錯誤用法。const
可被施加于任何作用域內的對象、函數參數、函數返回類型、成員函數本體。 編譯器強制實施bitwise constness
,但你編寫程序時應該使用“概念上的常量性”。 當const
和non-const
成員函數有著實質等價的實現時,令non-const
版本調用const
版本可避免代碼重復。 關于“將對象初始化”這事,C++似乎反復無常(對象的初始化動作何時一定發生,何時不一定發生)。針對這種復雜的規則,最佳的處理方法是:永遠在使用對象之前先將它初始化。
對于內置類型,必須手工完成初始化;對于內置類型以外的其他類型,初始化責任落在構造函數(constructors)身上,即,確保每一個構造函數都將對象的每一個成員初始化。
構造函數初始化的正確方法是:使用member initialization list(成員初值列)
,而不是在構造函數中的賦值。因為第一種方法的執行效率通常較高(對于大多數類型而言,比起先調用default
構造函數,然后再調用copy assignment
操作符,單只調用一次copy
構造函數是比較高效的。對于內置類型,其初始化和賦值的成本相同,但為了一致性最好也通過成員初值列來初始化)。
C++有著十分固定的”成員初始化次序”:總是base classes
更早于其derived classes
被初始化。而class的成員變量總是以其聲明次序被初始化,而和它們在成員初始值列中的出現次序無關。建議,當你在成員初值列中初始化各個成員時,最好總是和其聲明的次序一致。
最后一個問題:不同編譯單元內定義的non-local static
對象的初始化順序是怎么樣的?
函數內的static
對象稱為local static
對象,其他static對象稱為non-local static
對象。
C++對定義于不同編譯單元內的non-local static
對象的初始化次序并無明確定義。因此,如果某編譯單元內的某個non-local static
對象的初始化動作依賴另一編譯單元內的某個non-local static
對象,那么它所用到的這個對象可能尚未被初始化。
針對上面這個問題的解決方法是: 將每個non-local static
對象搬到自己的專屬函數內,這些函數返回一個reference指向它所含的對象。即,non-local static
對象被local static
對象替換了。
注意:這些函數內含static對象的事實使它們在多線程系統中帶有不確定性。處理這種麻煩的方法是,在程序的單線程啟動階段,手工調用所有reference-returning函數,這可消除與初始化有關的race conditions(競速形勢)
。
請記住
為內置類型對象進行手工初始化,因為C++不保證初始化它們。 構造函數最好使用成員初值列(member initialization list
),而不要在構造函數本體內使用賦值操作(assignment
)。初值列列出的成員變量,其排列次序應該和它們在class中的聲明次序相同。 為免除跨編譯單元的初始化次序問題,請以local static
對象替換non-local static
對象。 下一篇: Effective C++ - Constructors, Destructors, and Assignment Operators
新聞熱點
疑難解答