線上訂房服務-台灣趴趴狗聯合訂房中心
發文 回覆 瀏覽次數:1489
推到 Plurk!
推到 Facebook!

論C++構造函數中的不合理設計

 
jackkcg
站務副站長


發表:891
回覆:1050
積分:848
註冊:2002-03-23

發送簡訊給我
#1 引用回覆 回覆 發表時間:2002-10-28 04:51:04 IP:61.221.xxx.xxx 未訂閱
此為轉貼資料 論C++構造函數中的不合理設計  在C++中,構造函數是一個在構件物件的時候調用的特殊的函數,其目的是對物件進行初始化的工作,從而使物件被使用之前可以處於一種合理的狀態。但是,構造函數的設計並不完美,甚至有些不合理的特性。比如說,限定構造函數名稱與類的名稱相同的條件。這些特性在構造C++編譯器的時候是值得引起注意的。還有,在今後C++的標準修訂或者制定其他面向物件的設計語言時候應當避免這些特性。這裏也提出了一些解決的方案。 C++中,任何類都有一個(至少有一個)構造函數,甚至在沒有構造函數被聲明的時候亦是如此。在物件被聲明的時候,或者被動態生成的時候,這些構造函數就會被調用。構造函數做了許多不可見的工作,即使構造函數中沒有任何代碼,這些工作包括對物件的記憶體分配和通過賦值的方式對成員進行初始化。構造函數的名稱必須與類的名稱相同,但是可以有許多不同的重載版本來提供,通過參數類型來區分構造函數的版本。構造函數可以顯式的通過用戶代碼來調用,或者當代碼不存在是通過編譯程序來隱式插入。當然,顯式地通過代碼調用是推薦的方法,因?隱式調用的效果可能不是我們所預料的,特別是在處理動態記憶體分配方面。代碼通過參數來調用唯一的構造函數。構造函數沒有返回值,儘管在函數體中可以又返回語句。每個構造函數可以以不同的方式來實例化一個物件,因?每個類都有構造函數,至少也是缺省構造函數,所以每個物件在使用之前都相應的使用構造函數。構造函數的調用如圖1所示。        圖1. The activities involved in the execution of a constructor     因?構造函數是一種函數,所以他的可見性無非是三種public、private、protected。通常,構造函數都被聲明?public型。如果構造函數被聲明?private或protected,就限制了物件的實例化。這在阻止類被其他人實例化的方面很有效。構造函數中可以有任何C++的語句,比如,一條列印語句,可以被加入到構造函數中來表明調用的位置。    構造函數的類型     C++中構造函數有許多種類型,最常用的式缺省構造函數和拷貝構造函數,也存在一些不常用的構造函數。下面介紹了四種不同的構造函數。    1、缺省構造函數 缺省構造函數是沒有參數的函數。另外,缺省構造函數也可以在參數列表中以參數缺省值的方式聲明。缺省構造函數的作用是把物件初始化?缺省的狀態。如果在類中沒有顯式定義構造函數,那?編譯器會自動的隱式創建一個,這個隱式創建的構造函數和一個空的構造函數很相像。他除了?生物件的實例以外什?工作都不做。在許多情況下,缺省構造函數都會被自動的調用,例如在一個物件被聲明的時候,就會引起缺省構造函數的調用。    2、拷貝構造函數 拷貝構造函數,經常被稱作X(X&),是一種特殊的構造函數,他由編譯器調用來完成一些基於同一類的其他物件的構件及初始化。它的唯一的一個參數(物件的引用)是不可變的(因?是const型的)。這個函數經常用在函數調用期間于用戶定義類型的值傳遞及返回。拷貝構造函數要調用基類的拷貝構造函數和成員函數。如果可以的話,它將用常量方式調用,另外,也可以用非常量方式調用。 在C++中,下面三種物件需要拷貝的情況。因此,拷貝構造函數將會被調用。 1). 一個物件以值傳遞的方式傳入函數體 2). 一個物件以值傳遞的方式從函數返回 3). 一個物件需要通過另外一個物件進行初始化 以上的情況需要拷貝構造函數的調用。如果在前兩種情況不使用拷貝構造函數的時候,就會導致一個指標指向已經被刪除的記憶體空間。對於第三種情況來說,初始化和賦值的不同含義是構造函數調用的原因。事實上,拷貝構造函數是由普通構造函數和賦值操作賦共同實現的。描述拷貝構造函數和賦值運算符的異同的參考資料有很多。 拷貝構造函數不可以改變它所引用的物件,其原因如下:當一個物件以傳遞值的方式傳一個函數的時候,拷貝構造函數自動的被調用來生成函數中的物件。如果一個物件是被傳入自己的拷貝構造函數,它的拷貝構造函數將會被調用來拷貝這個物件這樣複製才可以傳入它自己的拷貝構造函數,這會導致無限迴圈。 除了當物件傳入函數的時候被隱式調用以外,拷貝構造函數在物件被函數返回的時候也同樣的被調用。換句話說,你從函數返回得到的只是物件的一份拷貝。但是同樣的,拷貝構造函數被正確的調用了,你不必擔心。 如果在類中沒有顯式的聲明一個拷貝構造函數,那?,編譯器會私下裏?你制定一個函數來進行物件之間的位元拷貝(bitwise copy)。這個隱含的拷貝構造函數簡單的關聯了所有的類成員。許多作者都會提及這個默認的拷貝構造函數。注意到這個隱式的拷貝構造函數和顯式聲明的拷貝構造函數的不同在於對於成員的關聯方式。顯式聲明的拷貝構造函數關聯的只是被實例化的類成員的缺省構造函數除非另外一個構造函數在類初始化或者在構造列表的時候被調用。 拷貝構造函數是程式更加有效率,因?它不用再構造一個物件的時候改變構造函數的參數列表。設計拷貝構造函數是一個良好的風格,即使是編譯系統提供的幫助你申請記憶體默認拷貝構造函數。事實上,默認拷貝構造函數可以應付許多情況。    3、用戶定義的構造函數 用戶定義的構造函數允許物件在被定義的時候同時被初始化。這種構造函數可以有任何類型的參數。一個用戶定義的和其他類型的構造函數在類 mystring 中得以體現:     class mystring  {...... public: mystring(); // Default constructor mystring (mystring &src) // Copy constructor mystring (char * scr); // Coercion constructor mystring ( char scr[ ], size_t len); // User-Defined constructor     };    4、強制構造函數 C++中,可以聲明一個只有一個參數的構造函數來進行類型轉換。強制構造函數定一個從參數類型進行的一個類型轉換(隱式的或顯式的)。換句話說,編譯器可以用任何參數的實例來調用構造函數。這樣做的目的是建立一個臨時實例來替換一個參數類型的實例。注意標準新近加入C++的關鍵字explicit 是用來禁止隱式的類型轉換。然而,這一特性還沒能被所有的編譯器支援。下面是一個強制構造函數的例子:     class A  { public : A(int ){ } }; void f(A) { }  void g() { A My_Object= 17; A a2 = A(57); A a3(64); My_Object = 67; f(77); }     像A My_Object= 17;這種聲明意味著A(int)構造函數被調用來從整型變數生成一個物件。這樣的構造函數就是強制構造函數。    普遍特性     下面是一些C++構造函數的不合理設計,當然,可能還有其他一些不合理之處。但是,大多數情況下,我們還是要和這些特性打交道,我們要逐一說明。    1、構造函數可以?內聯,但不要這樣做 一般來講,大多數成員函數都可以在前面加入"inline"關鍵字而成?內聯函數,構造函數也不例外,但是別這?做!一個被定義?內聯的構造函數如下:     class x  {.......... public : x (int ); : : }; inline x::x(int ) {...}     在上面的代碼中,函數並不是作?一個單獨的實體而是被插入到程式碼中。這對於只有一兩條語句的函數來說會提到效率,因?這裏沒有調用函數的開銷。 用內聯的構造函數的危險性可以在定義一個靜態內聯構造函數中體現。在這種情況下,靜態的構造函數應當是只被調用一次。然而,如果頭文件中含有靜態內聯構造函數,並被其他單元包括的話,函數就會?生多次拷貝。這樣,在程式?動時就會調用所有的函數拷貝,而不是程式應當調用的一份拷貝。這其中的根本原因是靜態函數是在以函數?裝下的真實物件。  應該牢記的一件事是內聯是建議而不是強制,編譯器?生內聯代碼。這意味著內聯是與實現有關的編譯器的不同可能帶來很多差異。另一方面,內聯函數中可能包括比代碼更多的東西。構造函數被聲明?內聯,所有包含物件的構造函數和基類的構造函數都需要被調用。這些調用是隱含在構造函數中的。這可能會創建很大的內聯函數段,所以,不推薦使用內聯的構造函數。    2、構造函數沒有任何返回類型 對一個構造函數指定一個返回類型是一個錯誤,因?這樣會引入構造函數的位址。這意味著將無法處理出錯。這樣,一個構造函數是否成功的創建一個物件將不可以通過返回之來確定。事實上,儘管C++的構造函數不可以返回,也有一個方法來確定是否記憶體分配成功地進行。這種方法是內建在語言內部來處理緊急情況的機制。一個預定好的函數指標 new-handler,它可以被設置?用戶定制的對付new操作符失敗的函數,這個函數可以進行任何的動作,包括設置錯誤標誌、重新申請記憶體、退出程式或者?出異常。你可以安心的使用系統內建的new-handler。最好的使構造函數發出出錯信號的方法,就是?出異常。在構造函數中?出異常將清除錯誤之前創建的任何物件及分配的記憶體。 如果構造函數失敗而使用異常處理的話,那?,在另一個函數中進行初始化可能是一個更好的主意。這樣,程式師就可以安全的構件物件並得到一個合理的指標。然後,初始化函數被調用。如果初始化失敗的話,物件直接被清除。    3、構造函數不可以被聲明?static C++中,每一個類的物件都擁有類資料成員的一份拷貝。但是,靜態成員則沒有這樣而是所有的物件共用一個靜態成員。靜態函數是作用於類的操作,而不是作用在物件上。可以用類名和作用控制操作符來調用一個靜態函數。這其中的一個例外就是構造函數,因?它違反了面向物件的概念。 關於這些的一個相似的現象是靜態物件,靜態物件的初始化是在程式的一開始階段就進行的(在main()函數之前)。下面的代碼解釋了這種情況。     MyClass static_object(88, 91);     void bar() { if (static_object.count( ) > 14) { ... } }     在這個例子中,靜態變數在一開始的時候就被初始化。通常這些物件由兩部分構成。第一部分是資料段,靜態變數被讀取到全局的資料段中。第二部分是靜態的初始化函數,在main()函數之前被調用。我們發現,一些編譯器沒有對初始化的可靠性進行檢查。所以你得到的是未經初始化的物件。解決的方案是,寫一個封裝函數,將所有的靜態物件的引用都置於這個函數的調用中,上面的例子應當這樣改寫。      static MyClass* static_object = 0;     MyClass* getStaticObject() { if (!static_object) static_object =  new MyClass(87, 92); return static_object; }     void bar() { if (getStaticObject()->count( ) > 15) { ... } }    4、構造函數不能成?虛函數 虛構造函數意味著程式師在運行之前可以在不知道物件的準確類型的情況下創建物件。虛構造函數在C++中是不可能實現的。最通常遇到這種情況的地方是在物件上實現I/O的時候。即使足夠的類的內部資訊在文件中給出,也必須找到一種方法實例化相應的類。然而,有經驗的C++程式師會有其他的辦法來類比虛構造函數。 類比虛函數需要在創建物件的時候指定調用的構造函數,標準的方法是調用虛的成員函數。很不幸,C++在語法上不支援虛構造函數。?了繞過這個限制,一些現成的方法可以在運行時刻確定構件的物件。這些等同於虛構造函數,但是這是C++中根本不存在的東西。 第一個方法是用switch或者if-else選擇語句來手動實現選擇。在下面的例子中,選擇是基於標準庫的type_info構造,通過打開運行時刻類型資訊支援。但是你也可以通過虛函數來實現RTTI     class Base { public: virtual const char* get_type_id() const; staticBase* make_object (const char* type_name); };     const char* Base::get_type_id() const { return typeid(*this).raw_name(); }     class Child1: public Base { };     class Child2: public Base { };     Base* Base::make_object(const char* type_name) { if (strcmp(type_name, typeid(Child1).raw_name()) == 0) return new Child1; else if (strcmp(type_name,typeid (Child2).raw_name()) == 0) return new Child2; else { throw exception ("unrecognized type name passed"); return 0X00; // represent NULL } } 這一實現是非常直接的,它需要程式師在main_object中保存一個所有類的表。這就破壞了基類的封裝性,因?基類必須知道自己的子類。 一個更面向物件的方法類解決虛構造函數叫做標本實例。它的基本思想是程式中生成一些全局的實例。這些實例只再虛構造函數的機制中存在:     class Base { public: staticBase* make_object(const char* typename) { if (!exemplars.empty()) { Base* end = *(exemplars.end()); list<Base*>::iterator iter = exemplars.begin(); while (*iter != end) { Base* e = *iter ; if (strcmp(typename, e->get_typename()) == 0) return e->clone(); } } return 0X00 // Represent NULL; } virtual ~Base() { }; virtual const char* get_typename() const { return typeid(*this).raw_name(); } virtual Base* clone() const = 0; protected: static list<Base*> exemplars; }; list<Base*> Base::exemplars; // T must be a concrete class // derived from Base, above template class exemplar: public T { public: exemplar() { exemplars.push_back(this); } ~exemplar() { exemplars.remove(this); } }; class Child: public Base { public: ~Child() { } Base* clone() const { return new Child; } }; exemplar Child_exemplar; 在這種設計中,程式師要創建一個類的時候要做的是創建一個相應的exampler類。注意到在這個例子中,標本是自己的標本類的實例。這提供了一種高校得實例化方法。 5、創建一個缺省構造函數 當繼承被使用的時候,卻省構造函數就會被調用。更明確地說,當繼承層次的最晚層的類被構造的時候,所有基類的構造函數都在派生基類之前被調用,舉個例子來說,看下面的代碼: #include class Base { int x; public : Base() : x(0) { } // The NULL constructor Base(int a) : x(a) { } }; class alpha : virtual public Base { int y; public : alpha(int a) : Base(a), y(2) { } }; class beta : virtual public Base { int z; public : beta(int a) : Base(a), z(3) { } }; class gamma : public alpha, public beta { int w; public : gamma ( int a, int b) : alpha(a), beta(b), w(4) { } }; main() {..... } 在這個例子中,我們沒有在gamma的頭文件中提供任何的初始化函數。編譯器會?基類使用缺省的構造函數。但是因?你提供了一個構造函數,編譯器就不會提供任何缺省構造函數。正如你看到的這段包含缺省構造函數的代碼一樣,如果刪除其中的缺省構造函數,編譯就無法通過。 如果基類的構造函數中引入一些副效應的話,比如說打開文件或者申請記憶體,這樣程式師就得確保中間基類沒有初始化虛基類。也就是,只有虛基類的構造函數可以被調用。 虛基類的卻省構造函數完成一些不需要任何依賴於派生類的參數的初始化。你加入一個init()函數,然後再從虛基類的其他函數中調用它,或在其他類中的構造函數裏調用(你的確保它只調用了一次)。 6、不能取得構造函數的位址 C 中,不能把構造函數當作函數指標來進行傳遞,指向構造函數的的指標也不可以直接傳遞。允許這些就可以通過調用指標來創建物件。一種達到這種目的的方法是借助於一個創建並返回新物件的靜態函數。指向這樣的函數的指標用於新物件需要的地方。下面是一個例子: class A { public: A( ); // cannot take the address of this // constructor directly static A* createA(); // This function creates a new A object // on the heap and returns a pointer to it. // A pointer to this function can be passed // in lieu of a pointer to the constructor. }; 這一方法設計簡單,只需要將抽象類置入頭文件即可。這給new留下了一個問題,因?準確的類型必須是可見的。上面的靜態函數可以用來包裝隱藏子類。 7、位元拷貝在動態申請記憶體的類中不可行 C 中,如果沒有提供一個拷貝構造函數,編譯器會自動生成一個。生成的這個拷貝構造函數對物件的實例進行位元拷貝。這對沒有指標成員的類來說沒什?,但是,對用了動態申請的類就不是這樣的了。?了澄清這一點,設想一個物件以值傳遞的方式傳入一個函數,或者從函數中返回,物件是以?拷貝的方式複製。這種位元拷貝對含有指向其他物件指標的類是沒有作用的(見圖2)。當一個含有指標的類以值傳遞的方式傳入函數的時候,物件被複製,包括指標的位址,還有,新的物件的作用域是這個函數。在函數結束的時候,很不幸,析構函數要破壞這個物件。因此,物件的指標被刪除了。這導致原來的物件的指標指向一塊空的記憶體區域-一個錯誤。在函數返回的時候,也有類似的情況發生。 圖2. The automatic copy constructor that makes a bitwise copy of the class. 這個問題可以簡單的通過在類中定義一個含有記憶體申請的拷貝構造函數來解決,這種靠叫做深拷貝,是在堆中分配記憶體給各個物件的。 8、編譯器可以隱式指定強制構造函數 因?編譯器可以隱式選擇強制構造函數,你就失去了調用函數的選擇權。如果需要控制的話,不要聲明只有一個參數的構造函數,取而代之,定義helper函數來負責轉換,如下面的例子: #include #include class Money { public: Money(); // Define conversion functions that can only be // called explicitly. static Money Convert( char * ch ) { return Money( ch ); } static Money Convert( double d ) { return Money( d ); } void Print() { printf( "\n%f", _amount ); } private: Money( char *ch ) { _amount = atof( ch ); } Money( double d ) { _amount = d; } double _amount; }; void main() { // Perform a conversion from type char * // to type Money. Money Account = Money::Convert( "57.29" ); Account.Print(); // Perform a conversion from type double to type // Money. Account = Money::Convert( 33.29 ); Account.Print(); } 在上面的代碼中,強制構造函數定義?private而不可以被用來做類型轉換。然而,它可以被顯式的調用。因?轉換函數是靜態的,他們可以不用引用任何一個物件來完成調用。 總結 要澄清一點是,這裏提到的都是我們所熟知的ANSI C 能夠接受的。許多編譯器都對ANSI C 進行了自己的語法修訂。這些可能根據編譯器的不同而不同。很明顯,許多編譯器不能很好的處理這幾點。探索這幾點的緣故是引起編譯構造的注意,也是在C 標準化的過程中移除一些瑕疵。 參考文獻: 1. Stroustrup, Bjarne. The C Programming Language, 3rd ed., Addison-Wesley, Reading, MA, 1997. 2. Ellis, Margaret and Bjarne Stroustrup. The Annotated C Reference Manual, Addison-Wesley, Reading, MA, 1990. 3. Stroustrup, Bjarne. The Design and Evolution of C , Addison-Wesley, Reading, MA, 1994. 4. Murry, Robert B. C Strategies and Tactics, Addison-Wesley, Reading, MA, 1993. 5. Farres-Casals, J. "Proving Correctness of Constructor Implementations," Mathematical Foundations of Computer Science 1989 Proceedings. 6. Breymann, Ulrich. Designing Components with the C STL, Addison-Wesley, Reading, MA,1998. 7. Lippman, Stanley and Josee LaJoie. C Primer, 3rd ed., Addison-Wesley, Reading, MA, 1998. 8. Skelly, C. "Getting A Handle On The New-Handler," C Report, 4(2):1-18, February 1992. 9. Coggins, J. M. "Handling Failed Constructors Gracefully," C Report, 4(1):20-22, January 1992. 10. Sabatella, M. "Laser Evaluation of C Static Constructors," SIGPLAN Notices, 27(6):29-36 (June 1992). 11. Eckel, B. "Virtual Constructors," C Report, 4(4):13-16,May 1992. 12. Coplien, James O. Advanced C : Programming Styles and Idioms, Addison-Wesley, Reading, MA, 1992.
------
**********************************************************
哈哈&兵燹
最會的2大絕招 這個不會與那個也不會 哈哈哈 粉好

Delphi K.Top的K.Top分兩個字解釋Top代表尖端的意思,希望本討論區能提供Delphi的尖端新知
K.表Knowlege 知識,就是本站的標語:Open our mind
系統時間:2024-04-26 12:42:18
聯絡我們 | Delphi K.Top討論版
本站聲明
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。
2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。
3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇!