Bjarne Stroustrup的 C++ 風格與技術常見問題與答案 (節譯一) (轉)

worldblog發表於2007-12-07
Bjarne Stroustrup的 C++ 風格與技術常見問題與答案 (節譯一) (轉)[@more@] Bjarne Stroustrup的 C++ 風格與技術常見問題與答案(節譯一)

最近CKER工作很忙,實在對不起關心我的朋友......

真誠致歉.....:)

本文中包含大家經常問到的關於C++ 風格與技術的問題。若您有更好的的問題與建議請發信到 to:bs@research.att.com">bs@research.att.com。要知道我不可能將所有的時間花在我的網頁上。

更普通的問題,參閱 .

術語和概念,參閱 .

請記住這裡只是些問題和答案。並非您在一本好書中可以見到的精挑細選的例子和解釋。某些說明可能也沒有參考手冊和標準中那樣精準。關於C++設計的問題您可以去看看。關於C++和其標準庫的使用問題可以參看。


您的或許有問題。也許太老了,或者的有問題,也可能您的已經過時了。如果這樣我也無能為力。

但是,這看起來更像是您要編譯的設計的太差。因而在編譯的時候,編譯器不得不檢查數以百計的頭和成千上萬行的程式碼。原則上,這是可以避免的。如果問題處在您的程式庫開發商的設計上,您也幹不了什麼(除了選擇一個更好的庫),但您可以將您的程式碼結構化,來減少每次改動後重新編譯所需的時間。一個能體現良好的關係分割的設計通常總是不錯的,維護性也好。

考慮如下物件導向的經典例子:

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 ; // ... }; 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: // Shape的使用者介面 virtual void draw() const = 0; virtual void rotate(int degrees) = 0; virtual Point center() const = 0; // ... // 無資料 }; 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 { //譯者注:呵呵,多繼承。唉....BCB不支援啊...:( public: void draw() const; void rotate(int); Point center() const; // ... protected: Point a, b, c; };


 


這是為了確保兩個不同的物件擁有不同的地址。出於同樣的原因,new總是返回一個唯一的物件指標。考慮如下程式碼:

class Empty { }; void f() { Empty a, b; if (&a == &b) cout << "impossible: report error to compiler supplier n不可能:快向您的編譯器廠商報錯!"; Empty* p1 = new Empty; Empty* p2 = new Empty; if (p1 == p2) cout << "impossible: report error to compiler supplier n不可能:快向您的編譯器廠商報錯!"; }


關於空基類有個有趣的規則,就是空基類無需單獨用一個位元組代表:

struct X : Empty { //譯者注:從Empty基類繼承 int a; // ... }; void f(X* p) { void* p1 = p; void* p2 = &p->a; if (p1 == p2) cout << "nice: good optimizern很好:不錯的"; }


這種最佳化是的,並且很有用。它允許程式設計師使用空類來描述十分簡單的概念而無需過載。目前已經有一些編譯器提供這種 叫做"empty base class optimization"的最佳化。『譯者注:BCB提供對空基類的最佳化,DEV C++ 好像不行.......』


您不該這麼做。如果您的介面不需要資料,不要將它們放在介面定義類中。應該放在派生類中。參見.

有時,您不得不在類中描述資料。考慮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) ?

因為很多類都不是用作基類的。例子請參閱。

同時,帶一個虛擬函式的類的物件需要額外的空間。這是虛擬函式機制所要求的-通常是一個物件一個(字)大小。這種開支是有意義的,但也使規劃來自其他語言的資料變得複雜起來。(比如:C和Fortan)

參閱 可以得到關於合理設計的更多資訊。

 


因為很多類不是設計來用作基類的。虛擬函式只在用作派生物件的介面類中才有意義(通常在堆中分配,並透過引用指標訪問)。

因此,什麼時候需要將虛解構函式呢?當類包含至少一個虛擬函式時。包含虛擬函式意味著這個類被用作派生類的介面,這時一個派生類物件就有可能由基類指標釋放銷燬。比如:

class Base { // ... virtual ~Base(); }; class Derived : public Base { // ... ~Derived(); }; void f() { Base* p = new Derived; delete p; // 虛解構函式用來確保呼叫派生類解構函式~Derived }


如果基類的解構函式不是虛的,派生類的解構函式不可能被呼叫-這個副作用很明顯,由派生類分配的資源沒有釋放。『譯者注:這也是BCB中所有的T類的解構函式都必須宣告為虛的原因。』

 


虛呼叫是一種在可以在只有部分資訊的情況下工作的機制。特別是允許我們呼叫一個只知道介面而不知道其準確的物件型別的函式。但要建立一個物件您需要全部的資訊,特別是必須要知道物件的準確型別。因此,建構函式不能是虛的。

但仍然有可以間接實現像"虛建構函式"那樣來建立物件的技術。例子參見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這樣的類資訊完全隔離掉了。


這個問題(有很多變化)通常會像下面這樣提出來:

#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; B* pb = pd; 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之間沒有發生過載解析。編譯器在D的作用域中查詢,並發現了唯一的函式"double f(double)"並呼叫它。它永遠不會打擾B(封裝)的作用域。在C++中,沒有跨作用域的過載-派生類作用域也不會例外。(詳見 或 )。

但我如何從我的基類和派生類對所有的f()進行過載?使用using宣告很容易做到。

class D : public B { public: using B::f; // 使得所有來自於B的f都可用 double f(double d) { cout << "f(double): "; return d; } // ... };

此時的輸出會是

f(int): 3 f(double): 3.6


這就是說,過載解析同時應用於B的f()和D的f(),並選擇呼叫最合適的f()。 


可以,但必須小心。它可能不像您想象的那樣執行。在建構函式中,虛呼叫機制被禁用。原因是來自於派生類的過載還沒有發生。物件構造時首先呼叫基類的方法,"base before derived"(基類先於派生類)。

考慮如下程式碼:

#include< string> #include< iostream> using namespace std; class B { public: B(const string& ss) { cout << "B constructorn"; f(ss); } virtual void f(const string&) { cout << "B::fn";} }; class D : public B { public: D(const string & ss) :B(ss) { cout << "D constructorn";} void f(const string& ss) { cout << "D::fn"; s = ss; } private: string s; }; int main() { D d("Hello"); }

程式編譯執行如下

B constructor B::f D constructor


注意,沒有D::f。想想如果改變規則的話會發生什麼。B::B()將呼叫 D::f()。因為建構函式D::D()還沒有執行,D::f()試圖將引數賦給沒有初始化的字串s。最有可能的結果是程式立刻崩潰。

析構的次序則遵照 "derived class before base class"(派生類先於基類)的次序。所以虛擬函式的行為跟建構函式相同。只有本地的定義被使用-不會呼叫過載的函式以避免接觸(已經銷燬的)派生類物件的部分。

詳見 13.2.4.2 或 15.4.3.

 這條規則看起來好像是人為加上的。但不是這樣。事實上,要讓建構函式呼叫虛擬函式的行為與其他函式完全一致的規則很容易實現。可是,這樣做同時也暗示了不能書寫任何依賴於基類建立的常量的虛擬函式。實在是可怕的混亂。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-989576/,如需轉載,請註明出處,否則將追究法律責任。

相關文章