前言:如何有效運(yùn)用C++,包括一般性的設(shè)計(jì)策略,以及帶有具體細(xì)節(jié)的特定語(yǔ)言特性。知道細(xì)節(jié)很重要,否則如果疏忽幾乎總是導(dǎo)致不可預(yù)期的程序行為(undefined behavior)。本文總結(jié)對(duì)于如何使用C++的一些建議,從而讓你成為一個(gè)有戰(zhàn)斗力的C++程序員。
Effective C - Accustoming Yourself to C構(gòu)造函數(shù)的explicit對(duì)象的復(fù)制命名習(xí)慣TR1和Boost視C為一個(gè)語(yǔ)言聯(lián)邦盡量以const enum inline替換define盡量使用const確定對(duì)象被使用前已先被初始化
被聲明為explicit
的構(gòu)造函數(shù)通常比non-explicit
更受歡迎,因?yàn)樗鼈兘咕幾g器執(zhí)行非預(yù)期的類型轉(zhuǎn)換。除非有一個(gè)好理由允許構(gòu)造函數(shù)被用于隱式類型轉(zhuǎn)換,否則把它聲明為explicit
。
copy
構(gòu)造函數(shù)被用來“以同型對(duì)象初始化自我對(duì)象”,copy assignment
操作符被用來“從另一個(gè)同型對(duì)象中拷貝其值到自我對(duì)象”。
copy構(gòu)造和copy賦值的區(qū)別:如果一個(gè)新對(duì)象被定義,一定會(huì)有一個(gè)構(gòu)造函數(shù)被調(diào)用,不可能調(diào)用賦值操作。如果沒有新對(duì)象被定義,就不會(huì)有構(gòu)造函數(shù)被調(diào)用,那么就是賦值操作被調(diào)用。
構(gòu)造函數(shù)和析構(gòu)函數(shù)分別使用縮寫ctor
和dtor
代替。 使用lhs
(left-hand side)和rhs
(right-hand side)表示參數(shù)名稱。
TR1
(Technical Report 1)是一份規(guī)范,描述加入C++標(biāo)準(zhǔn)程序庫(kù)的諸多新機(jī)能。這些機(jī)能以新的class templates
和function templates
形式體現(xiàn)。所有TR1
組件都被置于命名空間tr1
內(nèi)。 Boost
是個(gè)組織,亦是一個(gè)網(wǎng)站,提供可移植,源代碼開放的C++程序庫(kù)。大多數(shù)TR1
機(jī)能是以Boost
的工作為基礎(chǔ)。
今天的C++已經(jīng)是個(gè)多重范型編程語(yǔ)言(multiparadigm PRogramming language),一個(gè)同時(shí)支持以下特性的語(yǔ)言: * 過程形式(procedural) * 面向?qū)ο笮问剑╫bject-oriented) * 函數(shù)形式(functional) * 泛型形式(generic) * 元編程形式(metaprogramming)
為了理解C++,你必須認(rèn)識(shí)其主要的次語(yǔ)言(sublanguage):
C 說到底C++仍是以C為基礎(chǔ)。blocks, statements, preprocessor, built-in data types, arrays, pointers等統(tǒng)統(tǒng)來自C。許多時(shí)候C++對(duì)問題的解法其實(shí)不過就是較高級(jí)的C解法,但是C++提供了C沒有的templates, exceptions, overloading(重載)等功能。C語(yǔ)言可以重載嗎
// 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è)試C語(yǔ)言是否支持overload重載 fun(); // C語(yǔ)言可以通過不同的函數(shù)指針來模擬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)
部分,也是大多數(shù)程序員經(jīng)驗(yàn)最少的部分。
STL STL
是個(gè)template程序庫(kù),它對(duì)containers
, iterators
, algorithms
以及function objects
的規(guī)約有極佳的緊密配合與協(xié)調(diào)。
寧可以編譯器
替換預(yù)處理器
。當(dāng)你做出這樣的事情:
記號(hào)名稱ASPECT_RATIO也許從未被編譯器看見,也許在編譯器開始處理源碼之前就被預(yù)處理器替換了,于是記號(hào)名稱有可能沒有進(jìn)入記號(hào)表(symbol table)
內(nèi),當(dāng)你運(yùn)用此常量但獲得一個(gè)編譯錯(cuò)誤時(shí)可能會(huì)帶來困惑,因?yàn)檫@個(gè)錯(cuò)誤信息提到的是1.653而不是ASPECT_RATIO。尤其是如果ASPECT_RATIO被定義在一個(gè)非你所寫的頭文件內(nèi),你肯定對(duì)1.653來自何處毫無概念。解決的方法是:以一個(gè)常量替換上述的宏(#define)
。
好處是:
作為一個(gè)語(yǔ)言常量,AspectRatio肯定會(huì)被編譯器看到,當(dāng)然就會(huì)進(jìn)入記號(hào)表內(nèi)。使用常量可能比使用#define導(dǎo)致較小量的目標(biāo)代碼,因?yàn)轭A(yù)處理器盲目地將宏名稱進(jìn)行替換會(huì)導(dǎo)致目標(biāo)代碼出現(xiàn)多份1.653,而若改用常量則不會(huì)出現(xiàn)。字符串常量,string
對(duì)象通常比char*-based
合適。const char* const authorName = "gerry";const std::string authorName("gerry");class專屬常量。為了將常量的作用域(scope)限制在class內(nèi),你必須讓它成為class的一個(gè)成員(member)
,另外為了保證此常量至多只有一份實(shí)體,必須讓它成為一個(gè)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++要求所使用的任何東西提供一個(gè)定義式,但如果它是class專屬常量且又是static整數(shù)類型,只要不取它們的地址,你可以聲明并使用它們而無須提供定義式。
但是,如果你需要取某個(gè)class專屬常量的地址,或者編譯器要求(比如,老編譯器)需要看到一個(gè)定義式,那么需要另外提供定義式。
#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的定義式中沒有賦值是因?yàn)椋琧lass常量已在聲明時(shí)獲得了初值,因此定義時(shí)不可以再設(shè)置初值。 我們無法利用#define
創(chuàng)建一個(gè)class專屬常量,因?yàn)?define并不能限制作用域(scope),一旦宏被定義,它就在其后的編譯過程中有效,除非在某處被#undef
。因此,#define
不僅不能用來定義class專屬常量,也不能提供任何封裝性。 如果想具備作用域,但又不想取地址,可以使用enum
來實(shí)現(xiàn)這個(gè)約束。
預(yù)處理器和宏的陷阱:
宏看起來像函數(shù),但是不會(huì)招致函數(shù)調(diào)用(function call)
帶來的額外開銷。 糟糕的做法:(有效率,但不安全)
好的做法:(效率和安全同時(shí)得到保證)
template<typename T>inline void callWithMax(const T& a, const T& b){ f(a > b ? a : b);}這個(gè)template
根據(jù)實(shí)例化可以產(chǎn)出一整群函數(shù),每個(gè)函數(shù)都接受兩個(gè)同類型對(duì)象,并以其中較大的調(diào)用f。這里不需要在函數(shù)本體中為參數(shù)加上括號(hào),也不需要操心參數(shù)被計(jì)算的次數(shù),同時(shí),由于callWithMax是個(gè)真正的函數(shù),它遵守作用域和訪問規(guī)則,因此可以寫出一個(gè)class內(nèi)的private inline函數(shù),而對(duì)于宏是無法完成的。
請(qǐng)記住:
對(duì)于單純常量,最好以const
對(duì)象或enum
替換#define
對(duì)于形似函數(shù)的宏,最好改用inline函數(shù)
替換#define
const
允許你指定一個(gè)語(yǔ)義約束,也就是指定一個(gè)“不該被改動(dòng)”的對(duì)象,而編譯器會(huì)強(qiáng)制實(shí)施該項(xiàng)約束。
如果關(guān)鍵字const
出現(xiàn)在星號(hào)左邊,表示被指物是常量;如果出現(xiàn)在星號(hào)右邊,表示指針自身是常量;如果出現(xiàn)在星號(hào)兩邊,表示被指物和指針兩者都是常量。
注意:如果被指物是常量,將關(guān)鍵字const
寫在類型之前,和寫在類型之后星號(hào)之前,這兩種寫法的意義相同。
STL迭代器系以指針為根據(jù)塑模出來,所以迭代器的作用就像個(gè)T*
指針。如果你希望迭代器所指的東西不可被改變,則需要使用const_iterator
。
const成員函數(shù)
將const
實(shí)施于成員函數(shù)的目的,是為了確認(rèn)該成員函數(shù)可作用于const
對(duì)象身上。這一類成員函數(shù)之所以重要,是因?yàn)椋?/p>它們使class接口比較容易被理解,可以得知哪個(gè)函數(shù)可以改動(dòng)對(duì)象內(nèi)容,而哪個(gè)函數(shù)不行。它們使“操作const
對(duì)象”成為可能,這對(duì)編寫高效代碼是個(gè)關(guān)鍵,比如,改善程序效率的一個(gè)根本方法是以pass by reference-to-const
方式傳遞對(duì)象,而此技術(shù)可行的前提是,我們有const成員函數(shù)可用來處理取得的const對(duì)象。
注意:兩個(gè)成員函數(shù)如果只是常量性不同,可以被重載(overload)。只有返回值類型不同的兩個(gè)函數(shù)不能重載(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; // 調(diào)用non-const TextBlock::operator[] const TextBlock ctb("yang"); // 調(diào)用const TextBlock::operator[] std::cout << ctb[0] << std::endl; return 0;}成員函數(shù)如果是const
意味著什么?—— bitwise constness或者physical constness
VS logical constness
bitwise const
指的是,成員函數(shù)只有在不更改對(duì)象之任何成員變量(static除外)時(shí)才可以說是const
,即,const成員函數(shù)不可以更改對(duì)象內(nèi)任何non-static成員變量。
注意:許多成員函數(shù)雖然不完全具備const
性質(zhì),卻能通過bitwise
測(cè)試。比如,一個(gè)更改了”指針?biāo)肝铩钡某蓡T函數(shù),如果只有指針隸屬于對(duì)象,那么此函數(shù)為bitwise const
不會(huì)引發(fā)編譯器異議,但是實(shí)際不能算是const
。
下面這段代碼,可以通過bitwise
測(cè)試,但是實(shí)際上改變了對(duì)象的值。
logical constness
主張,一個(gè)const
成員函數(shù)可以修改它所處理的對(duì)象的某些bits
,但只有在客戶端偵測(cè)不出的情況才可以(即,對(duì)客戶端是透明的,但是實(shí)際上對(duì)象的某些值允許改變)。正常情況下,由于bitwise const
的約束,const
成員函數(shù)內(nèi)是不允許修改non-static成員變量的,但是通過將一些變量聲明為mutable
則可以躲過編譯器的bitwise const
約束。
在const
和non-const
成員函數(shù)中避免重復(fù)
方法是:運(yùn)用const
成員函數(shù)實(shí)現(xiàn)出其non-const
孿生兄弟。
不好的做法(因?yàn)橛兄貜?fù)代碼):
// 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]; }好的做法(實(shí)現(xiàn)operator[]
的機(jī)能一次并使用它兩次,令其中一個(gè)調(diào)用另一個(gè)):
請(qǐng)記住:
將某些東西聲明為const
可幫助編譯器偵測(cè)出錯(cuò)誤用法。const
可被施加于任何作用域內(nèi)的對(duì)象、函數(shù)參數(shù)、函數(shù)返回類型、成員函數(shù)本體。 編譯器強(qiáng)制實(shí)施bitwise constness
,但你編寫程序時(shí)應(yīng)該使用“概念上的常量性”。 當(dāng)const
和non-const
成員函數(shù)有著實(shí)質(zhì)等價(jià)的實(shí)現(xiàn)時(shí),令non-const
版本調(diào)用const
版本可避免代碼重復(fù)。 關(guān)于“將對(duì)象初始化”這事,C++似乎反復(fù)無常(對(duì)象的初始化動(dòng)作何時(shí)一定發(fā)生,何時(shí)不一定發(fā)生)。針對(duì)這種復(fù)雜的規(guī)則,最佳的處理方法是:永遠(yuǎn)在使用對(duì)象之前先將它初始化。
對(duì)于內(nèi)置類型,必須手工完成初始化;對(duì)于內(nèi)置類型以外的其他類型,初始化責(zé)任落在構(gòu)造函數(shù)(constructors)身上,即,確保每一個(gè)構(gòu)造函數(shù)都將對(duì)象的每一個(gè)成員初始化。
構(gòu)造函數(shù)初始化的正確方法是:使用member initialization list(成員初值列)
,而不是在構(gòu)造函數(shù)中的賦值。因?yàn)榈谝环N方法的執(zhí)行效率通常較高(對(duì)于大多數(shù)類型而言,比起先調(diào)用default
構(gòu)造函數(shù),然后再調(diào)用copy assignment
操作符,單只調(diào)用一次copy
構(gòu)造函數(shù)是比較高效的。對(duì)于內(nèi)置類型,其初始化和賦值的成本相同,但為了一致性最好也通過成員初值列來初始化)。
C++有著十分固定的”成員初始化次序”:總是base classes
更早于其derived classes
被初始化。而class的成員變量總是以其聲明次序被初始化,而和它們?cè)诔蓡T初始值列中的出現(xiàn)次序無關(guān)。建議,當(dāng)你在成員初值列中初始化各個(gè)成員時(shí),最好總是和其聲明的次序一致。
最后一個(gè)問題:不同編譯單元內(nèi)定義的non-local static
對(duì)象的初始化順序是怎么樣的?
函數(shù)內(nèi)的static
對(duì)象稱為local static
對(duì)象,其他static對(duì)象稱為non-local static
對(duì)象。
C++對(duì)定義于不同編譯單元內(nèi)的non-local static
對(duì)象的初始化次序并無明確定義。因此,如果某編譯單元內(nèi)的某個(gè)non-local static
對(duì)象的初始化動(dòng)作依賴另一編譯單元內(nèi)的某個(gè)non-local static
對(duì)象,那么它所用到的這個(gè)對(duì)象可能尚未被初始化。
針對(duì)上面這個(gè)問題的解決方法是: 將每個(gè)non-local static
對(duì)象搬到自己的專屬函數(shù)內(nèi),這些函數(shù)返回一個(gè)reference指向它所含的對(duì)象。即,non-local static
對(duì)象被local static
對(duì)象替換了。
注意:這些函數(shù)內(nèi)含static對(duì)象的事實(shí)使它們?cè)诙嗑€程系統(tǒng)中帶有不確定性。處理這種麻煩的方法是,在程序的單線程啟動(dòng)階段,手工調(diào)用所有reference-returning函數(shù),這可消除與初始化有關(guān)的race conditions(競(jìng)速形勢(shì))
。
請(qǐng)記住
為內(nèi)置類型對(duì)象進(jìn)行手工初始化,因?yàn)镃++不保證初始化它們。 構(gòu)造函數(shù)最好使用成員初值列(member initialization list
),而不要在構(gòu)造函數(shù)本體內(nèi)使用賦值操作(assignment
)。初值列列出的成員變量,其排列次序應(yīng)該和它們?cè)赾lass中的聲明次序相同。 為免除跨編譯單元的初始化次序問題,請(qǐng)以local static
對(duì)象替換non-local static
對(duì)象。 下一篇: Effective C++ - Constructors, Destructors, and Assignment Operators
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注