最近CKER工作很忙,實在對不起關心我的朋友......
真誠致歉.....:)
本文中包含大家經常問到的關於C++ 風格與技術的問題。若您有更好的的問題與建議請發信到 [email protected]。要知道我不可能將所有的時間花在更新我的網頁上。
更普通的問題,參閱 general FAQ.
術語和概念,參閱 C++ glossary. 請記住這裏只是些問題和答案。並非您在一本好書中可以見到的精挑細選的例子和解釋。某些說明可能也沒有參考手冊和標準中那樣精準。關於C++設計的問題您可以去The Design and Evolution of C++看看。關於C++和其標準庫的使用問題可以參看The C++ Programming Language。爲什麼我編譯起來忒慢?
您的編譯器或許有問題。也許太老了,或者安裝的有問題,也可能您的計算機已經過時了。如果這樣我也無能爲力。
但是,這看起來更像是您要編譯的程序設計的太差。因而在編譯的時候,編譯器不得不檢查數以百計的頭文件和成千上萬行的代碼。原則上,這是可以避免的。如果問題處在您的程序庫開發商的設計上,您也幹不了什麼(除了選擇一個更好的庫),但您可以將您的代碼結構化,來減少每次改動後重新編譯所需的時間。一個能體現良好的對象關係分割的設計通常總是不錯的,維護性也好。
考慮如下面向對象編程的經典例子:
class Shape { public: // Shapes的用戶接口 virtual void draw() const; virtual void rotate(int degrees); // ... protected: // 通用數據實現 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; // ... }; 思路是用戶通過公共接口來操縱shape對象,並且派生類的實現(比如Circle和Triangle)共享由保護成員所代表的特徵。
- 定義對所有子類都有用的共享特徵並不容易。原因在於,保護成員們似乎比公共接口變化得要快得多。比如,儘管可以論證“Center”對所有的shape來說都是存在的。但被迫維護一個▲的中心點實在很麻煩-對▲來說 ,計算其中心多半是毫無意義的,除非有人對此有特別的興趣。
- 保護成員似乎更依賴於用戶Shape的具體實現,但這種依賴關係不是必須存在的。比如,使用Shape的多數(絕大多數?)代碼和 "Color"的定義在邏輯上是無關的,但Shape定義中Color的存在,使得通常需要編譯描述操作系統對顏色定義的頭文件。
- 當保護體中的某些部分發生變化的時候,用戶的Shape不得不重新編譯-儘管只有派生類的實現才能訪問這些保護成員。
因此,這些在基類中"對派生類實現有用的信息"同時也充當了用戶接口。這就是造成派生類實現的不穩定性;(在基類中改變信息時)不合邏輯的重新編譯用戶代碼;以及在用戶代碼中過度包含頭文件(因爲"對派生類實現有用的信息"需要這些頭文件)的根源。有時我們將這種現象稱之爲"brittle base class problem"("致命的基類問題")。
解決之道顯然是在用作用戶接口的類中去掉這些"對派生類實現有用的信息"。這就是說:只生成接口,純接口。也就是代表接口的純虛基類。
的用戶接口 無數據用戶現在從派生類實現的變化中隔離開來了。我已經發現這個技術使得編譯時間有數量級的減少。
但的確在所有派生類(或者只是一部分派生類)中有需要某些信息的時候該怎麼辦?只需將這些信息封裝在另一個類中,然後實現派生類時同時也繼承此類:
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 { //譯者注:呵呵,多繼承。唉....BCB不支持啊...:( public: void draw() const; void rotate(int); Point center() const; // ... protected: Point a, b, c; };
爲什麼空類的大小不是零?
這是爲了確保兩個不同的對象擁有不同的地址。出於同樣的原因,new總是返回一個唯一的對象指針。考慮如下代碼:
不可能:快向您的編譯器廠商報錯!不可能:快向您的編譯器廠商報錯!
關於空基類有個有趣的規則,就是空基類無需單獨用一個字節代表:
struct X : Empty { //譯者注:從Empty基類繼承 int a; // ... }; void f(X* p) { void* p1 = p; void* p2 = &p->a; if (p1 == p2) cout << "nice: good optimizer/n很好:不錯的優化"; } 這種優化是安全的,並且很有用。它允許程序員使用空類來描述十分簡單的概念而無需重載。目前已經有一些編譯器提供這種 叫做"empty base class optimization"的優化。『譯者注:BCB提供對空基類的優化,DEV C++ 好像不行.......』
爲什麼我不得不將數據放在我的 類定義部分?
您不該這麼做。如果您的接口不需要數據,不要將它們放在接口定義類中。應該放在派生類中。參見Why do my compiles take so long?.
有時,您不得不在類中描述數據。考慮complex類: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; };複數類被設計爲用作系統內置類型。此處要想創建真正的本地對象(比如:對象在棧中分配,而不是在堆中)在類聲明中的描述是必須的。這樣才能確保簡單操作的正確內聯。本地對象和內聯對複數類取得與系統內置複數類型相近的性能來說是必須的。
爲什麼成員函數缺省不是虛的 (virtual) ?
因爲很多類都不是用作基類的。例子請參閱class complex。
同時,帶一個虛函數的類的對象需要額外的空間。這是虛函數調用機制所要求的-通常是一個對象一個word(字)大小。這種開支是有意義的,但也使規劃來自其他語言的數據變得複雜起來。(比如:C和Fortan)
參閱 The Design and Evolution of C++ 可以得到關於合理設計的更多資訊。
爲什麼析構函數缺省不是虛的 (virtual)?
因爲很多類不是設計來用作基類的。虛函數只在用作派生對象的接口類中才有意義(通常在堆中分配,並通過引用指針訪問)。
因此,什麼時候需要將虛析構函數呢?當類包含至少一個虛函數時。包含虛函數意味着這個類被用作派生類的接口,這時一個派生類對象就有可能由基類指針釋放銷燬。比如:
虛析構函數用來確保調用派生類析構函數
如果基類的析構函數不是虛的,派生類的析構函數不可能被調用-這個副作用很明顯,由派生類分配的資源沒有釋放。『譯者注:這也是BCB中所有的TObject類的析構函數都必須聲明爲虛的原因。』
爲什麼沒有虛構造函數?
虛調用是一種在可以在只有部分信息的情況下工作的機制。特別是允許我們調用一個只知道接口而不知道其準確的對象類型的函數。但要創建一個對象您需要全部的信息,特別是必須要知道對象的準確類型。因此,構造函數不能是虛的。
但仍然有可以間接實現像"虛構造函數"那樣來創建對象的技術。例子參見TC++PL3 15.6.2.
下面的例子就是一種使用抽象類來生成適當類型的對象的技術:struct F { //對象創建函數的接口 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(); // 生成適當類型的對象A B* q = fac.make_a_B(); // 生成適當類型的對象B // ... } struct FX : F { A* make_an_A() const { return new AX(); } // AX 從A繼承而來 B* make_a_B() const { return new BX(); } // BX 從B繼承而來 }; struct FY : F { A* make_an_A() const { return new AY(); } // AY 從A繼承而來 B* make_a_B() const { return new BY(); } // BY 從B繼承而來 }; int main() { user(FX()); // 此用戶生成了AX和BX user(FY()); // 此用戶生成了AY和BY // ... } 這個變化通常稱作"the factory pattern"工廠模式。關鍵在於user()將比如AX和AY這樣的類信息完全隔離掉了。
爲什麼派生類沒有重載?
這個問題(有很多變化)通常會像下面這樣提出來:
運行結果:
f(double): 3.3 f(double): 3.6而不是像某些人(錯誤)的想象:
換句話說,在D和B之間沒有發生重載解析。編譯器在D的作用域中查找,並發現了唯一的函數"double f(double)"並調用它。它永遠不會打擾B(封裝)的作用域。在C++中,沒有跨作用域的重載-派生類作用域也不會例外。(詳見 D&E 或 TC++PL3)。
但我如何從我的基類和派生類對所有的f()進行重載?使用using聲明很容易做到。
使得所有來自於的都可用此時的輸出會是
這就是說,重載解析同時應用於B的f()和D的f(),並選擇調用最合適的f()。
我能在構造函數中調用一個虛函數麼?
可以,但必須小心。它可能不像您想象的那樣運行。在構造函數中,虛調用機制被禁用。原因是來自於派生類的重載還沒有發生。對象構造時首先調用基類的方法,"base before derived"(基類先於派生類)。
考慮如下代碼:
程序編譯運行如下
注意,沒有D::f。想想如果改變規則的話會發生什麼。B::B()將調用 D::f()。因爲構造函數D::D()還沒有運行,D::f()試圖將參數賦給沒有初始化的字符串s。最有可能的結果是程序立刻崩潰。
析構的次序則遵照 "derived class before base class"(派生類先於基類)的次序。所以虛函數的行爲跟構造函數相同。只有本地的定義被使用-不會調用重載的函數以避免接觸(已經銷燬的)派生類對象的部分。
這條規則看起來好像是人爲加上的。但不是這樣。事實上,要讓構造函數調用虛函數的行爲與其他函數完全一致的規則很容易實現。可是,這樣做同時也暗示了不能書寫任何依賴於基類創建的常量的虛函數。實在是可怕的混亂。