C++ Style and Technique FAQ (中文版)-------
C++ Style and Technique FAQ (中文版)
Q: 這個簡單的程序……我如何把它搞定?
#include<iostream> #include<vector> #include<algorithm> using namespace std; int main() { vector<double> v; double d; while(cin>>d) v.push_back(d); // read elements if (!cin.eof()) { // check if input failed cerr << "format error\n"; return 1; // error return } cout << "read " << v.size() << " elements\n"; reverse(v.begin(),v.end()); cout << "elements in reverse order:\n"; for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n'; return 0; // success return }
- 這是一個用標準C++寫的程序,使用了標準庫[譯注:標準庫主要是將原來的C運行支持庫(Standard C Library)、iostream庫、STL(Standard Template Library,標準模板庫)等標準化而得的] 。標準庫提供的功能都位于namespace std之中,使用標準庫所需包含的頭文件是不含.h擴展名的。[譯注:有些編譯器廠商為了兼容性也提供了含.h擴展名的頭文件。]
- 如果你在Windows下編譯,你需要把編譯選項設為“console application”。記住,你的源代碼文件的擴展名必須為.cpp,否則編譯器可能會把它當作C代碼來處理。
- 主函數(shù)main()要返回一個整數(shù)。[譯注:有些編譯器也支持void main()的定義,但這是非標準做法]
- 將輸入讀入標準庫提供的vector容器可以保證你不會犯“緩沖區(qū)溢出”之類錯誤——對于初學者來說,硬是要求“把輸入讀到一個數(shù)組之中,不許犯任何‘愚蠢的錯誤’”似乎有點過份了——如果你真能達到這樣的要求,那你也不能算完全的初學者了。如果你不相信我的這個論斷,那么請看看我寫的《Learning Standard C++ as a New Language》一文。 [譯注:CSDN文檔區(qū)有該文中譯。]
- 代碼中“ !cin.eof() ”是用來測試輸入流的格式的。具體而言,它測試讀輸入流的循環(huán)是否因遇到EOF而終止。如果不是,那說明輸入格式不對(不全是數(shù)字)。還有細節(jié)地方不清楚,可以參看你使用的教材中關(guān)于“流狀態(tài)”的章節(jié)。
- Vector是知道它自己的大小的,所以不必自己清點輸入了多少元素。
- 這個程序不含任何顯式內(nèi)存管理代碼,也不會產(chǎn)生內(nèi)存泄漏。Vector會自動配置內(nèi)存,所以用戶不必為此煩心。
- 關(guān)于如何讀入字符串,請參閱后面的“我如何從標準輸入中讀取string”條目。
- 這個程序以EOF為輸入終止的標志。如果你在UNIX上運行這個程序,可以用Ctrl-D輸入EOF。但你用的Windows版本可能會含有一個bug(http://support.microsoft.com/support/kb/articles/Q156/2/58.asp?LN=EN-US&SD=gn&FR=0&qry=End of File&rnk=11&src=DHCS_MSPSS_gn_SRCH&SPR=NTW40),導致系統(tǒng)無法識別EOF字符。如果是這樣,那么也許下面這個有稍許改動的程序更適合你:這個程序以單詞“end”作為輸入終結(jié)的標志。
#include<iostream> #include<vector> #include<algorithm> #include<string> using namespace std; int main() { vector<double> v; double d; while(cin>>d) v.push_back(d); // read elements if (!cin.eof()) { // check if input failed cin.clear(); // clear error state string s; cin >> s; // look for terminator string if (s != "end") { cerr << "format error\n"; return 1; // error return } } cout << "read " << v.size() << " elements\n"; reverse(v.begin(),v.end()); cout << "elements in reverse order:\n"; for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n'; return 0; // success return }
Q: 為何我編譯一個程序要花那么多時間?
不過,也有可能原因在于你的程序——看看你的程序設計還能不能改進?編譯器是不是為了順利產(chǎn)出正確的二進制碼而不得不吃進成百個頭文件、幾萬行的源代碼?原則上,只要對源碼適當優(yōu)化一下,編譯緩慢的問題應該可以解決。如果癥結(jié)在于你的類庫供應商,那么你大概除了“換一家類庫供應商”外確實沒什么可做的了;但如果問題在于你自己的代碼,那么完全可以通過重構(gòu)(refactoring)來讓你的代碼更為結(jié)構(gòu)化,從而使源碼一旦有更改時需重編譯的代碼量最小。這樣的代碼往往是更好的設計:因為它的藕合程度較低,可維護性較佳。
我們來看一個OOP的經(jīng)典例子:
class Shape { public: // interface to users of Shapes virtual void draw() const; virtual void rotate(int degrees); // ... protected: // common data (for implementers of Shapes) Point center; Color col; // ... }; class Circle : public Shape { public: void draw() const; void rotate(int) { } // ... protected: int radius; // ... }; class Triangle : public Shape { public: void draw() const; void rotate(int); // ... protected: Point a, b, c; // ... };
- 要確認“哪些功能會被所有的繼承類用到,而應在基類中實作”可不是件簡單的事。所以,基類的保護成員或許會隨著要求的變化而變化,其頻度遠高于公共界面之可能變化。例如,盡管我們把“center”作為所有形狀的一個屬性(從而在基類中聲明)似乎是天經(jīng)地義的,但因此而要在基類中時時維護三角形的中心坐標是很麻煩的,還不如只在需要時才計算——這樣可以減少開銷。
- 和抽象的公共界面不同,保護成員可能會依賴實作細節(jié),而這是Shape類的使用者所不愿見到的。例如,絕大部分使用Shape的代碼應該邏輯上和color無關(guān);但只要color的聲明在Shape類中出現(xiàn)了,就往往會導致編譯器將定義了“該操作系統(tǒng)中顏色表示”的頭文件讀入、展開、編譯。這都需要時間!
- 當基類中保護成員(比如前面說的center,color)的實作有所變化,那么所有使用了Shape類的代碼都需要重新編譯——哪怕這些代碼中只有很少是真正要用到基類中的那個“語義變化了的保護成員”。
所以,在基類中放一些“對于繼承類之實作有幫助”的功能或許是出于好意,但實則是麻煩的源泉。用戶的要求是多變的,所以實作代碼也是多變的。將多變的代碼放在許多繼承類都要用到的基類之中,那么變化可就不是局部的了,這會造成全局影響的!具體而言就是:基類所倚賴的一個頭文件變動了,那么所有繼承類所在的文件都需重新編譯。
這樣分析過后,解決之道就顯而易見了:僅僅把基類用作為抽象的公共界面,而將“對繼承類有用”的實作功能移出。
class Shape { public: // interface to users of Shapes virtual void draw() const = 0; virtual void rotate(int degrees) = 0; virtual Point center() const = 0; // ... // no data }; class Circle : public Shape { public: void draw() const; void rotate(int) { } Point center() const { return center; } // ... protected: Point cent; Color col; int radius; // ... }; class Triangle : public Shape { public: void draw() const; void rotate(int); Point center() const; // ... protected: Color col; Point a, b, c; // ... };
但是,如果確實有一些功能是要被所有繼承類(或者僅僅幾個繼承類)共享的,又不想在每個繼承類中重復這些代碼,那怎么辦?也好辦:把這些功能封裝成一個類,如果繼承類要用到這些功能,就讓它再繼承這個類:
class Shape { public: // interface to users of Shapes virtual void draw() const = 0; virtual void rotate(int degrees) = 0; virtual Point center() const = 0; // ... // no data }; struct Common { Color col; // ... }; class Circle : public Shape, protected Common { public: void draw() const; void rotate(int) { } Point center() const { return center; } // ... protected: Point cent; int radius; }; class Triangle : public Shape, protected Common { public: void draw() const; void rotate(int); Point center() const; // ... protected: Point a, b, c; };
Q: 為何空類的大小不是零?
class Empty { }; void f() { Empty a, b; if (&a == &b) cout << "impossible: report error to compiler supplier"; Empty* p1 = new Empty; Empty* p2 = new Empty; if (p1 == p2) cout << "impossible: report error to compiler supplier"; }
struct X : Empty { int a; // ... }; void f(X* p) { void* p1 = p; void* p2 = &p->a; if (p1 == p2) cout << "nice: good optimizer"; }
Q: 為什么我必須把數(shù)據(jù)放到類的聲明之中?
template<class Scalar> class complex { public: complex() : re(0), im(0) { } complex(Scalar r) : re(r), im(0) { } complex(Scalar r, Scalar i) : re(r), im(i) { } // ... complex& operator+=(const complex& a) { re+=a.re; im+=a.im; return *this; } // ... private: Scalar re, im; };
class Implementer; // forward declaration
class Interface { public: // interface
private: Implementer impl; };
class Implementer {
public:
// implementation details, including data members
上述代碼中的注釋處可以存放提問者所說的“數(shù)據(jù)”,而Implementer的聲明代碼不需暴露給用戶。不過,Proxy模式也不是十全十美的——Interface通過impl指針間接調(diào)用實作代碼帶來了額外的開銷。或許讀者會說,C++不是有內(nèi)聯(lián)機制嗎?這個開銷能通過內(nèi)聯(lián)定義而彌補吧。但別忘了,此處運用Proxy模式的目的就是把“實作”部分隱藏起來,這“隱藏”往往就意味著“實作代碼”以鏈接庫中的二進制代碼形式存在。目前的C++編譯器和鏈接器能做到既“代碼內(nèi)聯(lián)”又“二進制隱藏”嗎?或許可以。那么Proxy模式又能否和C++的模板機制“合作愉快”呢?(換句話說,如果前面代碼中Interface和Implementer的聲明均不是class,而是template,又如何呢?)關(guān)鍵在于,編譯器對內(nèi)聯(lián)和模板的支持之實作是否需要進行源碼拷貝,還是可以進行二進制碼拷貝。目前而言,C#的泛型支持之實作是在Intermediate Language層面上的,而C++則是源碼層面上的。Bjarne給出的復數(shù)類聲明代碼稱“數(shù)據(jù)必須出現(xiàn)在類聲明中”也是部分出于這種考慮。呵呵,扯遠了……畢竟,這段文字只是FAQ的“譯注”而已,此處不作更多探討,有興趣的讀者可以自己去尋找答案 :O) ]
Q: 為何成員函數(shù)不是默認為虛?
另外,有虛函數(shù)的類有虛機制的開銷[譯注:指存放vtable帶來的空間開銷和通過vtable中的指針間接調(diào)用帶來的時間開銷],通常而言每個對象增加的空間開銷是一個字長。這個開銷可不小,而且會造成和其他語言(比如C,F(xiàn)ortran)的不兼容性——有虛函數(shù)的類的內(nèi)存數(shù)據(jù)布局和普通的類是很不一樣的。[譯注:這種內(nèi)存數(shù)據(jù)布局的兼容性問題會給多語言混合編程帶來麻煩。]
《The Design and Evolution of C++》 中有更多關(guān)于設計理念的細節(jié)。
Q: 為何析構(gòu)函數(shù)不是默認為虛?
那么,何時我該讓析構(gòu)函數(shù)為虛呢?哦,答案是——當類有其它虛函數(shù)的時候,你就應該讓析構(gòu)函數(shù)為虛。有其它虛函數(shù),就意味著這個類要被繼承,就意味著它有點“interface”的味道了。這樣一來,程序員就可能會以基類指針來指向由它的繼承類所實例化而來的對象,而能否通過基類指針來正常釋放這樣的對象就要看析構(gòu)函數(shù)是否為虛了。 例如:
class Base { // ... virtual ~Base(); }; class Derived : public Base { // ... ~Derived(); }; void f() { Base* p = new Derived; delete p; // virtual destructor used to ensure that ~Derived is called }
Q: C++中為何沒有虛擬構(gòu)造函數(shù)?
struct F { // interface to object creation functions virtual A* make_an_A() const = 0; virtual B* make_a_B() const = 0; }; void user(const F& fac) { A* p = fac.make_an_A(); // make an A of the appropriate type B* q = fac.make_a_B(); // make a B of the appropriate type // ... } struct FX : F { A* make_an_A() const { return new AX(); } // AX is derived from A B* make_a_B() const { return new BX(); } // BX is derived from B }; struct FY : F { A* make_an_A() const { return new AY(); } // AY is derived from A B* make_a_B() const { return new BY(); } // BY is derived from B }; int main() { user(FX()); // this user makes AXs and BXs user(FY()); // this user makes AYs and BYs // ... }
Q: 為何無法在派生類中重載?
#include<iostream> using namespace std; class B { public: int f(int i) { cout << "f(int): "; return i+1; } // ... }; class D : public B { public: double f(double d) { cout << "f(double): "; return d+1.3; } // ... }; int main() { D* pd = new D; cout << pd->f(2) << '\n'; cout << pd->f(2.3) << '\n'; }
f(double): 3.3
f(double): 3.6
f(int): 3
f(double): 3.6
換句話說,在D和B之間沒有重載發(fā)生。你調(diào)用了pd->f(),編譯器就在D的名字域里找啊找,找到double f(double)后就調(diào)用它了。編譯器懶得再到B的名字域里去看看有沒有哪個函數(shù)更符合要求。記住,在C++中,沒有跨域重載——繼承類和基類雖然關(guān)系很親密,但也不能壞了這條規(guī)矩。詳見《The Design and Evolution of C++》或者《The C++ Programming Language》第三版。
不過,如果你非得要跨域重載,也不是沒有變通的方法——你就把那些函數(shù)弄到同一個域里來好了。使用一個using聲明就可以搞定。
class D : public B { public: using B::f; // make every f from B available double f(double d) { cout << "f(double): "; return d+1.3; } // ... };
f(int): 3 f(double): 3.6
Q: 我能從構(gòu)造函數(shù)調(diào)用虛函數(shù)嗎?
#include<string> #include<iostream> using namespace std; class B { public: B(const string& ss) { cout << "B constructor\n"; f(ss); } virtual void f(const string&) { cout << "B::f\n";} }; class D : public B { public: D(const string & ss) :B(ss) { cout << "D constructor\n";} void f(const string& ss) { cout << "D::f\n"; s = ss; } private: string s; }; int main() { D d("Hello"); }
B constructor
B::f
D constructor
注意,輸出不是D::f 。 究竟發(fā)生了什么?f()是在B::B()中調(diào)用的。如果構(gòu)造函數(shù)中調(diào)用虛函數(shù)的規(guī)則不是如前文所述那樣,
而是如一些人希望的那樣去調(diào)用D::f()。那么因為構(gòu)造函數(shù)D::D()尚未運行,字符串s還未初始化,所以當D::f()試圖將參數(shù)
賦給s時,結(jié)果多半是——立馬當機。
析構(gòu)則正相反,遵循從繼承類到基類的順序(拆房子總得從上往下拆吧?),所以其調(diào)用虛函數(shù)的行為和在構(gòu)造函數(shù)中一樣:虛函數(shù)此時此刻被綁定到哪里(當然應該是基類啦——因為繼承類已經(jīng)被“拆”了——析構(gòu)了!),調(diào)用的就是哪個函數(shù)。
更多細節(jié)請見《The Design and Evolution of C++》,13.2.4.2 或者《The C++ Programming Language》第三版,15.4.3 。
有時,這條規(guī)則被解釋為是由于編譯器的實作造成的。[譯注:從實作角度可以這樣解釋:在許多編譯器中,直到構(gòu)造函數(shù)調(diào)用完畢,vtable才被建立,此時虛函數(shù)才被動態(tài)綁定至繼承類的同名函數(shù)。] 但事實上不是這么一回事——讓編譯器實作成“構(gòu)造函數(shù)中調(diào)用虛函數(shù)也和從其他函數(shù)中調(diào)用一樣”是很簡單的[譯注:只要把vtable的建立移至構(gòu)造函數(shù)調(diào)用之前即可]。關(guān)鍵還在于語言設計時的考量——讓虛函數(shù)可以求助于基類提供的通用代碼。[譯注:先有雞還是先有蛋?Bjarne實際上是在告訴你,不是“先有實作再有規(guī)則”,而是“如此實作,因為規(guī)則如此”。]
Q: 有"placement delete"嗎?
class Arena {
public:
void* allocate(size_t);
void deallocate(void*);
// ...
};
void* operator new(size_t sz, Arena& a)
{
return a.allocate(sz);
}
Arena a1(some arguments);
Arena a2(some arguments);
X* p1 = new(a1) X; Y* p2 = new(a1) Y; Z* p3 = new(a2) Z; // ...
template<class T> void destroy(T* p, Arena& a) { if (p) { p->~T(); // explicit destructor call a.deallocate(p); } }
destroy(p2,a2);
destroy(p3,a3);
如何在類繼承體系中定義配對的operator new() 和 operator delete() 可以參看 《The C++ Programming Language》,Special Edition,15.6節(jié) ,《The Design and Evolution of C++》,10.4節(jié),以及《The C++ Programming Language》,Special Edition,19.4.5節(jié)。[譯注:此處按原文照譯。前面有提到“參見《The C++ Programming Language》第三版”的,實際上特別版(Special Edition)和較近重印的第三版沒什么區(qū)別。]
A: 可以的,但何必呢?好吧,也許有兩個理由:
- 出于效率考慮——不希望我的函數(shù)調(diào)用是虛的
- 出于安全考慮——確保我的類不被用作基類(這樣我拷貝對象時就不用擔心對象被切割(slicing)了)[譯注:“對象切割”指,將派生類對象賦給基類變量時,根據(jù)C++的類型轉(zhuǎn)換機制,只有包括在派生類中的基類部分被拷貝,其余部分被“切割”掉了。]
如果為了和“虛函數(shù)調(diào)用”說byebye,那么確實有給類繼承體系“封頂”的需要。在設計前,不訪先問問自己,這些函數(shù)為何要被設計成虛的。我確實見過這樣的例子:性能要求苛刻的函數(shù)被設計成虛的,僅僅因為“我們習慣這樣做”!
好了,無論如何,說了那么多,畢竟你只是想知道,為了某種合理的理由,你能不能防止別人繼承你的類。答案是可以的。可惜,這里給出的解決之道不夠干凈利落。你不得不在在你的“封頂類”中虛擬繼承一個無法構(gòu)造的輔助基類。還是讓例子來告訴我們一切吧:
class Usable;
class Usable_lock {
friend class Usable;
private:
Usable_lock() {}
Usable_lock(const Usable_lock&) {}
};
class Usable : public virtual Usable_lock {
// ...
public:
Usable();
Usable(char*);
// ...
};
Usable a;
class DD : public Usable { };
DD dd; // error: DD::DD() cannot access
// Usable_lock::Usable_lock(): private member
Q: 為什么我無法限制模板的參數(shù)?
讓我們來看這段代碼:
template<class Container> void draw_all(Container& c) { for_each(c.begin(),c.end(),mem_fun(&Shape::draw)); }
為了早點捕捉到這個錯誤,我們可以這樣寫代碼:
template<class Container> void draw_all(Container& c) { Shape* p = c.front(); // accept only containers of Shape*s for_each(c.begin(),c.end(),mem_fun(&Shape::draw)); }
template<class Container> void draw_all(Container& c) { typedef typename Container::value_type T; Can_copy<T,Shape*>(); // accept containers of only Shape*s for_each(c.begin(),c.end(),mem_fun(&Shape::draw)); }
template<class T1, class T2> struct Can_copy { static void constraints(T1 a, T2 b) { T2 c = a; b = a; } Can_copy() { void(*p)(T1,T2) = constraints; } };
- 你可以不通過定義/拷貝變量就表達出constraints[譯注:實則定義/拷貝變量的工作被封裝在Can_copy模板中了] ,從而可以不必作任何“那個類型是這樣被初始化”之類假設,也不用去管對象能否被拷貝、銷毀(除非這正是constraints所在)。[譯注:即——除非constraints正是“可拷貝”、“可銷毀”。如果用易理解的偽碼描述,就是template <typename T as Copy_Enabled> xxx,template <typename T as Destructible> xxx 。]
- 如果使用現(xiàn)代編譯器,constraints不會帶來任何額外代碼
- 定義或者使用constraints均不需使用宏定義
- 如果constraints沒有被滿足,編譯器給出的錯誤消息是容易理解的。事實上,給出的錯誤消息包括了單詞“constraints” (這樣,編碼者就能從中得到提示)、constraints的名稱、具體的出錯原因(比如“cannot initialize Shape* by double*”)
既然如此,我們干嗎不干脆在C++語言本身中定義類似Can_copy()或者更優(yōu)雅簡潔的語法呢?The Design and Evolution of C++分析了此做法帶來的困難。已經(jīng)有許許多多設計理念浮出水面,只為了讓含constraints的模板類易于撰寫,同時還要讓編譯器在constraints不被滿足時給出容易理解的出錯消息。比方說,我在Can_copy中“使用函數(shù)指針”的設計就來自于Alex Stepanov和Jeremy Siek。我認為我的Can_copy()實作還不到可以標準化的程度——它需要更多實踐的檢驗。另外,C++使用者會遭遇許多不同類型的constraints,目前看來還沒有哪種形式的帶constraints的模板獲得壓倒多數(shù)的支持。
已有不少關(guān)于constraints的“內(nèi)置語言支持”方案被提議和實作。但其實要表述constraint根本不需要什么異乎尋常的東西:畢竟,當我們寫一個模板時,我們擁有C++帶給我們的強有力的表達能力。讓代碼來為我的話作證吧:
template<class T, class B> struct Derived_from { static void constraints(T* p) { B* pb = p; } Derived_from() { void(*p)(T*) = constraints; } }; template<class T1, class T2> struct Can_copy { static void constraints(T1 a, T2 b) { T2 c = a; b = a; } Can_copy() { void(*p)(T1,T2) = constraints; } }; template<class T1, class T2 = T1> struct Can_compare { static void constraints(T1 a, T2 b) { a==b; a!=b; a<b; } Can_compare() { void(*p)(T1,T2) = constraints; } }; template<class T1, class T2, class T3 = T1> struct Can_multiply { static void constraints(T1 a, T2 b, T3 c) { c = a*b; } Can_multiply() { void(*p)(T1,T2,T3) = constraints; } }; struct B { }; struct D : B { }; struct DD : D { }; struct X { }; int main() { Derived_from<D,B>(); Derived_from<DD,B>(); Derived_from<X,B>(); Derived_from<int,B>(); Derived_from<X,int>(); Can_compare<int,float>(); Can_compare<X,B>(); Can_multiply<int,float>(); Can_multiply<int,float,double>(); Can_multiply<B,X>(); Can_copy<D*,B*>(); Can_copy<D,B*>(); Can_copy<int,B*>(); } // the classical "elements must derived from Mybase*" constraint: template<class T> class Container : Derived_from<T,Mybase> { // ... };
Q: 我們已經(jīng)有了 "美好的老qsort()",為什么還要用sort()?
qsort(array,asize,sizeof(elem),elem_compare);
sort(vec.begin(),vec.end());
struct Record { string name; // ... }; struct name_compare { // compare Records using "name" as the key bool operator()(const Record& a, const Record& b) const { return a.name<b.name; } }; void f(vector<Record>& vs) { sort(vs.begin(), vs.end(), name_compare()); // ... }
另外,還有許多人欣賞sort()的類型安全性——要使用它可不需要任何強制的類型轉(zhuǎn)換。對于標準類型,也不必寫compare()函數(shù),省事不少。如果想看更詳盡的解釋,參看我的《Learning Standard C++ as a New Language》一文。
另外,為何sort()要比qsort()快?因為它更好地利用了C++的內(nèi)聯(lián)語法語義。
Q: 什么是function object?
Function object的涵義比通常意義上的函數(shù)更廣泛,因為它可以在多次調(diào)用之間保持某種“狀態(tài)”——這和靜態(tài)局部變量有異曲同工之妙;不過這種“狀態(tài)”還可以被初始化,還可以從外面來檢測,這可要比靜態(tài)局部變量強了。我們來看一個例子:
class Sum { int val; public: Sum(int i) :val(i) { } operator int() const { return val; } // extract value int operator()(int i) { return val+=i; } // application }; void f(vectorv) { Sum s = 0; // initial value 0 s = for_each(v.begin(), v.end(), s); // gather the sum of all elements cout << "the sum is " << s << "\n"; // or even: cout << "the sum is " << for_each(v.begin(), v.end(), Sum(0)) << "\n"; }
在標準庫中function objects被廣泛使用,這給標準庫帶來了極大的靈活性和可擴展性。
[譯注:C++是一個博采眾長的語言,function object的概念就是從functional programming中借來的;而C++本身的強大和表現(xiàn)力的豐富也使這種“拿來主義”成為可能。一般而言,在使用function object的地方也常可以使用函數(shù)指針;在我們還不熟悉function object的時候我們也常常是使用指針的。但定義一個函數(shù)指針的語法可不是太簡單明了,而且在C++中指針早已背上了“錯誤之源”的惡名。更何況,通過指針調(diào)用函數(shù)增加了間接開銷。所以,無論為了語法的優(yōu)美還是效率的提高,都應該提倡使用function objects。
下面我們再從設計模式的角度來更深入地理解function objects:這是Visitor模式的典型應用。當我們要對某個/某些對象施加某種操作,但又不想將這種操作限定死,那么就可以采用Visitor模式。在Design Patterns一書中,作者把這種模式實作為:通過一個Visitor類來提供這種操作(在前面Bjarne Stroustrup的代碼中,Sum就是一個Visitor的變體),用Visitor類實例化一個visitor對象(當然,在前面的代碼中對應的是s);然后在Iterator的迭代過程中,為每一個對象調(diào)用visitor.visit()。這里visit()是Visitor類的一個成員函數(shù),作用相當于Sum類中那個“特殊的成員函數(shù)”——operator();visit()也完全可以被定義為內(nèi)聯(lián)函數(shù),以去除間接性,提高性能。在此提請讀者注意,C++把重載的操作符也看作函數(shù),只不過是具有特殊函數(shù)名的函數(shù)。所以實際上Design Patterns一書中Visitor模式的示范實作和這里function object的實作大體上是等價的。一個function object也就是一個特殊的Visitor。]
Q: 我應該怎樣處理內(nèi)存泄漏?
#include<vector> #include<string> #include<iostream> #include<algorithm> using namespace std; int main() // small program messing around with strings { cout << "enter some whitespace-separated words:\n"; vector<string> v; string s; while (cin>>s) v.push_back(s); sort(v.begin(),v.end()); string cat; typedef vector<string>::const_iterator Iter; for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+"; cout << cat << '\n'; }
請注意這里沒有顯式的內(nèi)存管理代碼。沒有宏,沒有類型轉(zhuǎn)換,沒有溢出檢測,沒有強制的大小限制,也沒有指針。如果使用function object和標準算法[譯注:指標準庫中提供的泛型算法],我連Iterator也可以不用。不過這畢竟只是一個小程序,殺雞焉用牛刀?
當然,這些方法也并非無懈可擊,而且說起來容易做起來難,要系統(tǒng)地使用它們也并不總是很簡單。不過,無論如何,它們的廣泛適用性令人驚訝,而且通過移去大量的顯式內(nèi)存分配/釋放代碼,它們確實增強了代碼的可讀性和可管理性。早在1981年,我就指出通過大幅度減少需要顯式加以管理的對象數(shù)量,使用C++“將事情做對”將不再是一件極其費神的艱巨任務。
如果你的應用領(lǐng)域沒有能在內(nèi)存管理方面助你一臂之力的類庫,那么如果你還想讓你的軟件開發(fā)變得既快捷又能輕松得到正確結(jié)果,最好是先建立這樣一個庫。
如果你無法讓內(nèi)存分配和釋放成為對象的“自然行為”,那么至少你可以通過使用資源句柄來盡量避免內(nèi)存泄漏。這里是一個示例:假設你需要從函數(shù)返回一個對象,這個對象是在自由內(nèi)存堆上分配的;你可能會忘記釋放那個對象——畢竟我們無法通過檢查指針來確定其指向的對象是否需要被釋放,我們也無法得知誰應該負責釋放它。那么,就用資源句柄吧。比如,標準庫中的auto_ptr就可以幫助澄清:“釋放對象”責任究竟在誰。我們來看:
#include<memory> #include<iostream> using namespace std; struct S { S() { cout << "make an S\n"; } ~S() { cout << "destroy an S\n"; } S(const S&) { cout << "copy initialize an S\n"; } S& operator=(const S&) { cout << "copy assign an S\n"; } }; S* f() { return new S; // who is responsible for deleting this S? }; auto_ptr<S> g() { return auto_ptr<S>(new S); // explicitly transfer responsibility for deleting this S } int main() { cout << "start main\n"; S* p = f(); cout << "after f() before g()\n"; // S* q = g(); // caught by compiler auto_ptr<S> q = g(); cout << "exit main\n"; // leaks *p // implicitly deletes *q }
這里只是內(nèi)存資源管理的例子;至于其它類型的資源管理,可以如法炮制。
如果在你的開發(fā)環(huán)境中無法系統(tǒng)地使用這種方法(比方說,你使用了第三方提供的古董代碼,或者遠古“穴居人”參與了你的項目開發(fā)),那么你在開發(fā)過程中可千萬要記住使用內(nèi)存防漏檢測程序,或者干脆使用垃圾收集器(Garbage Collector)。
Q: 為何捕捉到異常后不能繼續(xù)執(zhí)行后面的代碼呢?
嗯,從異常處理代碼返回到異常拋出處繼續(xù)執(zhí)行后面的代碼的想法很好[譯注:現(xiàn)行異常機制的設計是:當異常被拋出和處理后,從處理代碼所在的那個catch塊往下執(zhí)行],但主要問題在于——exception handler不可能知道為了讓后面的代碼正常運行,需要做多少清除異常的工作[譯注:畢竟,當有異常發(fā)生,事情就有點不太對勁了,不是嗎;更何況收拾爛攤子永遠是件麻煩的事],所以,如果要讓“繼續(xù)執(zhí)行”能夠正常工作,寫throw代碼的人和寫catch代碼的人必須對彼此的代碼都很熟悉,而這就帶來了復雜的相互依賴關(guān)系[譯注:既指開發(fā)人員之間的“相互依賴”,也指代碼間的相互依賴——緊耦合的代碼可不是好代碼哦 :O) ],會帶來很多麻煩的維護問題。
在我設計C++的異常處理機制的時候,我曾認真地考慮過這個問題;在C++標準化的過程中,這個問題也被詳細地討論過。(參見《The Design and Evolution of C++》中關(guān)于異常處理的章節(jié))如果你想試試看在拋出異常之前能不能解決問題然后繼續(xù)往下執(zhí)行,你可以先調(diào)用一個“檢查—恢復”函數(shù),然后,如果還是不能解決問題,再把異常拋出。一個這樣的例子是new_handler。
Q: 為何C++中沒有C中realloc()的對應物?
在C++中,處理內(nèi)存重分配的較好辦法是使用標準庫中的容器,比如vector。[譯注:這些容器會自己管理需要的內(nèi)存,在必要時會“增長尺寸”——進行重分配。]
Q: 我如何使用異常處理?
Q: 我如何從標準輸入中讀取string?
#include<iostream> #include<string> using namespace std; int main() { cout << "Please enter a word:\n"; string s; cin>>s; cout << "You entered " << s << '\n'; }
如果你需要一次讀一整行,可以這樣:
#include<iostream> #include<string> using namespace std; int main() { cout << "Please enter a line:\n"; string s; getline(cin, s); cout << "You entered " << s << '\n'; }
Q: 為何C++不提供“finally”結(jié)構(gòu)?
class File_handle { FILE* p; public: File_handle(const char* n, const char* a) { p = fopen(n,a); if (p==0) throw Open_error(errno); } File_handle(FILE* pp) { p = pp; if (p==0) throw Open_error(errno); } ~File_handle() { fclose(p); } operator FILE*() { return p; } // ... }; void f(const char* fn) { File_handle f(fn,"rw"); // open fn for reading and writing // use file through f }
另外,請看看《The C++ Programming Language》附錄E中的資源管理例子。
Q: 那個auto_ptr是什么東東啊?為什么沒有auto_array?
#include<memory> using namespace std; struct X { int m; // .. }; void f() { auto_ptr<X> p(new X); X* q = new X; p->m++; // use p just like a pointer q->m++; // ... delete q; }
如果在代碼用// ...標注的地方拋出異常,那么p會被正常刪除——這個功勞應該記在auto_ptr的析構(gòu)函數(shù)頭上。不過,q指
向的X類型對象就沒有被釋放(因為不是用auto_ptr定義的)。詳情請見《The C++ Programming Language》14.4.2節(jié)。
Auto_ptr是一個輕量級的類,沒有引入引用計數(shù)機制。如果你把一個auto_ptr(比如,ap1)賦給另一個auto_ptr(比如,ap2),那么ap2將持有實際指針,而ap1將持有零指針。例如:
#include<memory> #include<iostream> using namespace std; struct X { int m; // .. }; int main() { auto_ptr<X> p(new X); auto_ptr<X> q(p); cout << "p " << p.get() << " q " << q.get() << "\n"; }
p 0x0 q 0x378d0
這里,語義似乎是“轉(zhuǎn)移”,而非“拷貝”,這或許有點令人驚訝。特別要注意的是,不要把auto_ptr作為標準容器的參數(shù)——標準容器要求通常的拷貝語義。例如:
std::vector<auto_ptr<X> >v; // error
一個auto_ptr只能持有指向單個元素的指針,而不是數(shù)組指針:
void f(int n) { auto_ptr<X> p(new X[n]); // error // ... }
那么,看來我們應該用一個使用delete[]來釋放指針的,叫auto_array的類似東東來放數(shù)組了?哦,不,不,沒有什么auto_array。理由是,不需要有啊——我們完全可以用vector嘛:
void f(int n) { vector<X> v(n); // ... }
Q: C和C++風格的內(nèi)存分配/釋放可以混用嗎?
不可以——從你無法delete一個以malloc()分配而來之對象的意義上而言。你也無法free()或realloc()一個由new分配而來的對象。
C++的new和delete運算符確保構(gòu)造和析構(gòu)正常發(fā)生,但C風格的malloc()、calloc()、free()和realloc()可不保證這點。而且,沒有任何人能向你擔保,new/delete和malloc/free所掌控的內(nèi)存是相互“兼容”的。如果在你的代碼中,兩種風格混用而沒有給你造成麻煩,那我只能說:直到目前為止,你是非常幸運的 :O)
如果你因為思念“美好的老realloc()”(許多人都思念她)而無法割舍整個古老的C內(nèi)存分配機制(愛屋及烏?),那么考慮使用標準庫中的vector吧。例如:
// read words from input into a vector of strings: vector<string> words; string s; while (cin>>s && s!=".") words.push_back(s);
我的《Learning Standard C++ as a New Language》一文中給出了其它例子,可以參考。
Q: 我想從void *轉(zhuǎn)換,為什么必須使用換型符?
A: 在C中,你可以隱式轉(zhuǎn)換,但這是不安全的,例如:
#include<stdio.h> int main() { char i = 0; char j = 0; char* p = &i; void* q = p; int* pp = q; /* unsafe, legal C, not C++ */ printf("%d %d\n",i,j); *pp = -1; /* overwrite memory starting at &i */ printf("%d %d\n",i,j); }
int* pp = (int*)q;
int* pp = static_cast<int*>(q);
在C中一類最常見的不安全換型發(fā)生在將malloc()分配而來的內(nèi)存賦給某個指針之時,例如:
int* p = malloc(sizeof(int));
int* p = new int;
- new不會“偶然”地分配錯誤大小的內(nèi)存
- new自動檢查內(nèi)存是否已枯竭
- new支持初始化
typedef std::complex<double> cmplx; /* C style: */ cmplx* p = (cmplx*)malloc(sizeof(int)); /* error: wrong size */ /* forgot to test for p==0 */ if (*p == 7) { /* ... */ } /* oops: forgot to initialize *p */ // C++ style: cmplx* q = new cmplx(1,2); // will throw bad_alloc if memory is exhausted if (*q == 7) { /* ... */ }
A: 如何在類中定義常量?
Q: 如果你想得到一個可用于常量表達式中的常量,例如數(shù)組大小的定義,那么你有兩種選擇:
class X { static const int c1 = 7; enum { c2 = 19 }; char v1[c1]; char v2[c2]; // ... };
一眼望去,c1的定義似乎更加直截了當,但別忘了只有static的整型或枚舉型量才能如此初始化。這就很有局限性,例如:
class Y { const int c3 = 7; // error: not static static int c4 = 7; // error: not const static const float c5 = 7; // error not integral };
我還是更喜歡玩“enum戲法”,因為這種定義可移植性好,而且不會引誘我去使用非標準的“類內(nèi)初始化”擴展語法。
那么,為何要有這些不方便的限制?因為類通常聲明在頭文件中,而頭文件往往被許多單元所包含。[所以,類可能會被重復聲明。]但是,為了避免鏈接器設計的復雜化,C++要求每個對象都只能被定義一次。如果C++允許類內(nèi)定義要作為對象被存在內(nèi)存中的實體,那么這項要求就無法滿足了。關(guān)于C++設計時的一些折衷,參見《The Design and Evolution of C++》。
如果這個常量不需要被用于常量表達式,那么你的選擇余地就比較大了:
class Z { static char* p; // initialize in definition const int i; // initialize in constructor public: Z(int ii) :i(ii) { } }; char* Z::p = "hello, there";
class AE { // ... public: static const int c6 = 7; static const int c7 = 31; }; const int AE::c7; // definition int f() { const int* p1 = &AE::c6; // error: c6 not an lvalue const int* p2 = &AE::c7; // ok // ... }
Q: 為何delete操作不把指針置零?
delete p; // ... delete p;
哦,不不,這個主意不夠“好”。一個理由是,被delete的指針未必是左值。我們來看:
delete p+1; delete f(x);
T* p = new T; T* q = p; delete p; delete q; // ouch!
如果你覺得釋放內(nèi)存時把指針置零很重要,那么不妨寫這樣一個destroy函數(shù):
template<class T> inline void destroy(T*& p) { delete p; p = 0; }
不妨把delete帶來的麻煩看作“盡量少用new/delete,多用標準庫中的容器”之另一條理由吧 :O)
請注意,把指針作為引用傳遞(以便delete可以把指針置零)會帶來額外的效益——防止右值被傳遞給destroy() :
int* f(); int* p; // ... destroy(f()); // error: trying to pass an rvalue by non-const reference destroy(p+1); // error: trying to pass an rvalue by non-const reference
Q: 我可以寫"void main()"嗎?
void main() { /* ... */ }
int main() { /* ... */ }
int main(int argc, char* argv[]) { /* ... */ }
#include<iostream> int main() { std::cout << "This program returns the integer value 0\n"; }
#include<iostream> main() { /* ... */ }
Q: 為何我不能重載“.”、“::”和“sizeof”等操作符?
而“sizeof”無法被重載是因為不少內(nèi)部操作,比如指針加法,都依賴于它,例如:
X a[10]; X* p = &a[3]; X* q = &a[3]; p++; // p points to a[4] // thus the integer value of p must be // sizeof(X) larger than the integer value of q
在N::m中,N和m都不是表達式,它們只是編譯器“認識”的名字,“::”執(zhí)行的實際操作是編譯時的名字域解析,并沒有表達式的運算牽涉在內(nèi)。或許有人會覺得重載一個“x::y”(其中x是實際對象,而非名字域或類名)是一個好主意,但這樣做引入了新的語法[譯注:重載的本意是讓操作符可以有新的語義,而不是更改語法——否則會引起混亂],我可不認為新語法帶來的復雜性會給我們什么好處。
原則上來說,“.”運算符是可以被重載的,就像“->”一樣。不過,這會帶來語義的混淆——我們到底是想和“.”后面的對象打交道呢,還是“.”后面的東東所實際指向的實體打交道呢?看看這個例子(它假設“.”重載是可以的):
class Y { public: void f(); // ... }; class X { // assume that you can overload . Y* p; Y& operator.() { return *p; } void f(); // ... }; void g(X& x) { x.f(); // X::f or Y::f or error? }
這個問題有好幾種解決方案。在C++標準化之時,何種方案為佳并不明顯。細節(jié)請參見《The Design and Evolution of C++》。
Q: 我怎樣才能把整數(shù)轉(zhuǎn)化為字符串?
#include<iostream> #include<string> #include<sstream> using namespace std; string itos(int i) // convert int to string { stringstream s; s << i; return s.str(); } int main() { int i = 127; string ss = itos(i); const char* p = ss.c_str(); cout << ss << " " << p << "\n"; }
Q: “int* p;”和“int *p;”,到底哪個正確?
不過如果讓人來讀,兩者的含義就有所不同了。代碼的書寫風格是很重要的。C風格的表達式和聲明式常被看作比“necessary evil”[譯注:“必要之惡”,意指為了達到某種目的而不得不付出的代價。例如有人認為環(huán)境的破壞是經(jīng)濟發(fā)展帶來的“necessary evil”]更糟的東西,而C++則很強調(diào)類型。所以,“int *p”和“int* p”之間并無對錯之分,只有風格之爭。
一個典型的C程序員會寫“int *p”,而且振振有詞地告訴你“這表示‘*p是一個int’”——聽上去挺有道理的。這里,*和p綁在了一起——這就是C的風格。這種風格強調(diào)的是語法。
而一個典型的C++程序員會寫“int* p”,并告訴你“p是一個指向int的指針,p的類型是int*”。這種風格強調(diào)的是類型。當然,我喜歡這種風格 :O) 而且,我認為,類型是非常重要的概念,我們應該注重類型。它的重要性絲毫不亞于C++語言中的其它“較為高級的部分”。[譯注:諸如RTTI,各種cast,template機制等,可稱為“較高級的部分”了吧,但它們其實也是類型概念的擴展和運用。我曾寫過兩篇談到C++和OOP的文章發(fā)表在本刊上,文中都強調(diào)了理解“類型”之重要性。我還曾譯過Object Unencapsulated (這本書由作者先前所著在網(wǎng)上廣為流傳的C++?? A Critique修訂而來)中講類型的章節(jié),這本書的作者甚至稱Object Oriented Programming應該正名為Type Oriented Programming——“面向類型編程”!這有點矯枉過正了,但類型確是編程語言之核心部分。]
當聲明單個變量時,int *和int*的差別并不是特別突出,但當我們要一次聲明多個變量時,易混淆之處就全暴露出來了:
int* p, p1; // probable error: p1 is not an int*
int *p, p1; // probable error?
int* p = &i; int p1 = p; // error: int initialized by int*
每當達到某種目的有兩條以上途徑,就會有些人被搞糊涂;每當一些選擇是出于個人喜好,爭論就會無休無止。堅持一次只聲明一個指針并在聲明時順便初始化,困擾我們已久的混淆之源就會隨風逝去。如果你想了解有關(guān)C的聲明語法的更多討論,參見《The Design and Evolution of C++》 。
Q: 何種代碼布局風格為佳?
我個人喜歡使用“K&R”風格,如果算上那些C語言中不存在的構(gòu)造之使用慣例,那么人們有時也稱之為“Stroustrup”風格。例如:
class C : public B { public: // ... }; void f(int* p, int max) { if (p) { // ... } for (int i = 0; i<max; ++i) { // ... } }
正確的縮進非常重要。
一些設計問題,比如使用抽象類來表示重要的界面、使用模板來表示靈活而可擴展的類型安全抽象、正確使用“異常”來表示錯誤,遠遠要比代碼風格重要。
[譯注:《The Practice of Programming》中有一章對“代碼風格”問題作了詳細的闡述。]
Q: 我該把const寫在類型前面還是后面?
const int a = 1; // ok int const b = 2; // also ok
為什么會這樣?當我發(fā)明“const”(最早是被命名為“readonly”且有一個叫“writeonly”的對應物)時,我讓它在前面和后面都行,因為這不會帶來二義性。當時的C/C++編譯器對修飾符很少有強加的語序規(guī)則。
我不記得當時有過什么關(guān)于語序的深思熟慮或相關(guān)的爭論。一些早期的C++使用者(特別是我)當時只是單純地覺得const int c = 10;要比int const c = 10;好看而已。或許,我是受了這件事實的影響:許多我早年寫的例子是用“readonly”修飾的,而readonly int c = 10;確實看上去要比int readonly c = 10;舒服。而最早的使用“const”的C/C++代碼是我用全局查找替換功能把readonly換成const而來的。我還記得和幾個人討論過關(guān)于語法“變體”問題,包括Dennis Ritchie。不過我不記得當時我們談的是哪幾種語言了。
另外,請注意:如果指針本身不可被修改,那么const應該放在“*”的后面。例如:
int *const p1 = q; // constant pointer to int variable int const* p2 = q; // pointer to constant int const int* p3 = q; // pointer to constant int
Q: 宏有什么不好嗎?
#include "someheader.h"
struct S {
int alpha;
int beta;
};
如果有人(不明智地)寫了一個叫“alpha”或者“beta”的宏,那么這段代碼無法通過編譯,甚至可能更糟——編譯出一些你
未曾預料的結(jié)果。比方說:如果“someheader.h”包含了如下定義:
#define alpha 'a' #define beta b[2]
那么前面的代碼就完全背離本意了。
不幸的是,你無法確保其他程序員不犯你所認為的“愚蠢的”錯誤。比方說,近來有人告訴我,他們遇到一個含“goto”語句的宏。我見到過這樣的代碼,也聽到過這樣的論點——有時宏中的“goto”是有用的。例如:
#define prefix get_ready(); int ret__ #define Return(i) ret__=i; do_something(); goto exit #define suffix exit: cleanup(); return ret__ void f() { prefix; // ... Return(10); // ... Return(x++); //... suffix; }
一個常見而微妙的問題是,函數(shù)風格的宏不遵守函數(shù)參數(shù)調(diào)用規(guī)則。例如:
#define square(x) (x*x) void f(double d, int i) { square(d); // fine square(i++); // ouch: means (i++*i++) square(d+1); // ouch: means (d+1*d+1); that is, (d+d+1) // ... }
#define square(x) ((x)*(x)) /* better */
我知道有些(其它語言中)被稱為“宏”的東西并不象C/C++預處理器所處理的“宏”那樣缺陷多多、麻煩重重,但我并不想改進C++的宏,而是建議你正確使用C++語言中的其他機制,比如內(nèi)聯(lián)函數(shù)、模板、構(gòu)造函數(shù)、析構(gòu)函數(shù)、異常處理等。
posted on 2005-11-30 11:58 夢在天涯 閱讀(3666) 評論(2) 編輯 收藏 引用 所屬分類: CPlusPlus