C++ Style and Technique

C++ Style and Technique FAQ (中文版)

Bjarne Stroustrup 著, 紫雲英 譯
 
[注: 本訪談錄之譯文經Stroustrup博士授權。如要轉載,請和我聯繫: [email protected] ]

 

Q: 這個簡單的程序……我如何把它搞定?

A: 常常有人問我一些簡單的程序該如何寫,這在學期之初時尤甚。一個典型的問題是:如何讀入一些數字,做些處理(比如數學運算),然後輸出……好吧好吧,這裏我給出一個“通用示範程序”:

 
程序很簡單,是吧。這裏是對它的一些“觀察報告”:
  • 這是一個用標準C++寫的程序,使用了標準庫[譯註:標準庫主要是將原來的C運行支持庫(Standard C Library)、iostream庫、STL(Standard Template Library,標準模板庫)等標準化而得的] 。標準庫提供的功能都位於namespace std之中,使用標準庫所需包含的頭文件是不含.h擴展名的。[譯註:有些編譯器廠商爲了兼容性也提供了含.h擴展名的頭文件。]
  • 如果你在Windows下編譯,你需要把編譯選項設爲“console application”。記住,你的源代碼文件的擴展名必須爲.cpp,否則編譯器可能會把它當作C代碼來處理。
  • 主函數main()要返回一個整數。[譯註:有些編譯器也支持void main()的定義,但這是非標準做法]
  • 將輸入讀入標準庫提供的vector容器可以保證你不會犯“緩衝區溢出”之類錯誤——對於初學者來說,硬是要求“把輸入讀到一個數組之中,不許犯任何‘愚蠢的錯誤’”似乎有點過份了——如果你真能達到這樣的要求,那你也不能算完全的初學者了。如果你不相信我的這個論斷,那麼請看看我寫的《Learning Standard C++ as a New Language》一文。 [譯註:CSDN文檔區有該文中譯。]
  • 代碼中“ !cin.eof() ”是用來測試輸入流的格式的。具體而言,它測試讀輸入流的循環是否因遇到EOF而終止。如果不是,那說明輸入格式不對(不全是數字)。還有細節地方不清楚,可以參看你使用的教材中關於“流狀態”的章節。
  • Vector是知道它自己的大小的,所以不必自己清點輸入了多少元素。
  • 這個程序不含任何顯式內存管理代碼,也不會產生內存泄漏。Vector會自動配置內存,所以用戶不必爲此煩心。
  • 關於如何讀入字符串,請參閱後面的“我如何從標準輸入中讀取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),導致系統無法識別EOF字符。如果是這樣,那麼也許下面這個有稍許改動的程序更適合你:這個程序以單詞“end”作爲輸入終結的標誌。
    
    
《The C++ Programming Language 》第三版中關於標準庫的章節裏有更多更詳細例子,你可以通過它們學會如何使用標準庫來“輕鬆搞定簡單任務”。
 

Q: 爲何我編譯一個程序要花那麼多時間?

A: 也許是你的編譯器有點不太對頭——它是不是年紀太大了,或者沒有安裝正確?也可能你的電腦該進博物館了……對於這樣的問題我可真是愛莫能助了。

不過,也有可能原因在於你的程序——看看你的程序設計還能不能改進?編譯器是不是爲了順利產出正確的二進制碼而不得不喫進成百個頭文件、幾萬行的源代碼?原則上,只要對源碼適當優化一下,編譯緩慢的問題應該可以解決。如果癥結在於你的類庫供應商,那麼你大概除了“換一家類庫供應商”外確實沒什麼可做的了;但如果問題在於你自己的代碼,那麼完全可以通過重構(refactoring)來讓你的代碼更爲結構化,從而使源碼一旦有更改時需重編譯的代碼量最小。這樣的代碼往往是更好的設計:因爲它的藕合程度較低,可維護性較佳。

我們來看一個OOP的經典例子:


 
上述代碼展示的設計理念是:讓用戶通過Shape的公共界面來處理“各種形狀”;而Shape的保護成員提供了各繼承類(比如Circle,Triangle)共同需要的功能。也就是說:將各種形狀(shapes)的公共因素劃歸到基類Shape中去。這種理念看來很合理,不過我要提請你注意:
  • 要確認“哪些功能會被所有的繼承類用到,而應在基類中實作”可不是件簡單的事。所以,基類的保護成員或許會隨着要求的變化而變化,其頻度遠高於公共界面之可能變化。例如,儘管我們把“center”作爲所有形狀的一個屬性(從而在基類中聲明)似乎是天經地義的,但因此而要在基類中時時維護三角形的中心座標是很麻煩的,還不如只在需要時才計算——這樣可以減少開銷。
  • 和抽象的公共界面不同,保護成員可能會依賴實作細節,而這是Shape類的使用者所不願見到的。例如,絕大部分使用Shape的代碼應該邏輯上和color無關;但只要color的聲明在Shape類中出現了,就往往會導致編譯器將定義了“該操作系統中顏色表示”的頭文件讀入、展開、編譯。這都需要時間!
  • 當基類中保護成員(比如前面說的center,color)的實作有所變化,那麼所有使用了Shape類的代碼都需要重新編譯——哪怕這些代碼中只有很少是真正要用到基類中的那個“語義變化了的保護成員”。

所以,在基類中放一些“對於繼承類之實作有幫助”的功能或許是出於好意,但實則是麻煩的源泉。用戶的要求是多變的,所以實作代碼也是多變的。將多變的代碼放在許多繼承類都要用到的基類之中,那麼變化可就不是局部的了,這會造成全局影響的!具體而言就是:基類所倚賴的一個頭文件變動了,那麼所有繼承類所在的文件都需重新編譯。

這樣分析過後,解決之道就顯而易見了:僅僅把基類用作爲抽象的公共界面,而將“對繼承類有用”的實作功能移出。


 
這樣,繼承類的變化就被孤立起來了。由變化帶來的重編譯時間可以極爲顯著地縮短。

但是,如果確實有一些功能是要被所有繼承類(或者僅僅幾個繼承類)共享的,又不想在每個繼承類中重複這些代碼,那怎麼辦?也好辦:把這些功能封裝成一個類,如果繼承類要用到這些功能,就讓它再繼承這個類:


[譯註:這裏作者的思路就是孤立變化,減少耦合。從這個例子中讀者可以學到一點Refactoring的入門知識 :O) ]
 

Q: 爲何空類的大小不是零?

A: 爲了確保兩個不同對象的地址不同,必須如此。也正因爲如此,new返回的指針總是指向不同的單個對象。我們還是來看代碼吧:

 
另外,C++中有一條有趣的規則——空基類並不需要另外一個字節來表示:

 
如果上述代碼中p1和p2相等,那麼說明編譯器作了優化。這樣的優化是安全的,而且非常有用。它允許程序員用空類來表示非常簡單的概念,而不需爲此付出額外的(空間)代價。一些現代編譯器提供了這種“虛基類優化”功能。
 

Q: 爲什麼我必須把數據放到類的聲明之中?

A: 沒人強迫你這麼做。如果你不希望界面中有數據,那麼就不要把它放在定義界面的類中,放到繼承類中好了。參看“爲何我編譯一個程序要花那麼多時間”條目。[譯註:本FAQ中凡原文爲declare/declaration的均譯爲聲明;define/definition均譯爲定義。兩者涵義之基本差別參見後面“‘int* p;’和‘int *p;’到底哪個正確條目中的譯註。通常而言,我們還是將下面的示例代碼稱爲complex類的定義,而將單單一行“class complex;”稱作聲明。]
 
但也有的時候你確實需要把數據放到類聲明裏面,比如下面的複數類的例子:

 
這個complex(複數)類是被設計成像C++內置類型那樣使用的,所以數據表示必須出現在聲明之中,以便可以建立真正的本地對象(即在堆棧上分配的對象,而非在堆中分配),這同時也確保了簡單操作能被正確內聯化。“本地對象”和“內聯”這兩點很重要,因爲這樣纔可以使我們的複數類達到和內置複數類型的語言相當的效率。

[譯註:我覺得Bjarne的這段回答有點“逃避問題”之嫌。我想,提問者的真實意圖或許是想知道如何用C++將“界面”與“實作”完全分離。不幸的是,C++語言和類機制本身不提供這種方式。我們都知道,類的“界面”部分往往被定義爲公有(一般是一些虛函數);“實作”部分則往往定義爲保護或私有(包括函數和數據);但無論是“public”段還是“protected”、“private”段都必須出現在類的聲明中,隨類聲明所在的頭文件一起提供。想來這就是“爲何數據必須放到類聲明中”問題的由來吧。爲了解決這個問題,我們有個變通的辦法:使用Proxy模式(參見《Design Patterns : Elements of Reusable Object-Oriented Software》一書),我們可以將實作部分在proxy類中聲明(稱爲“對象組合”),而不將proxy類的聲明暴露給用戶。例如:



在這個例子中,Implementer類就是proxy。在Interface中暴露給用戶的只是一個impl對象的“存根”,而無實作內容。Implementer類可以如下聲明:

  };

 

Q: 爲何成員函數不是默認爲虛?

A: 因爲許多類不是被用來做基類的。[譯註:用來做基類的類常類似於其它語言中的interface概念——它們的作用是爲一組類定義一個公共介面。但C++中的類顯然還有許多其他用途——比如表示一個具體的擴展類型。] 例如,複數類就是如此。

另外,有虛函數的類有虛機制的開銷[譯註:指存放vtable帶來的空間開銷和通過vtable中的指針間接調用帶來的時間開銷],通常而言每個對象增加的空間開銷是一個字長。這個開銷可不小,而且會造成和其他語言(比如C,Fortran)的不兼容性——有虛函數的類的內存數據佈局和普通的類是很不一樣的。[譯註:這種內存數據佈局的兼容性問題會給多語言混合編程帶來麻煩。]

《The Design and Evolution of C++》 中有更多關於設計理念的細節。

Q: 爲何析構函數不是默認爲虛?

A: 哈,你大概知道我要說什麼了 :O) 仍然是因爲——許多類不是被用來做基類的。只有在類被作爲interface使用時虛函數纔有意義。(這樣的類常常在內存堆上實例化對象並通過指針或引用訪問。)

那麼,何時我該讓析構函數爲虛呢?哦,答案是——當類有其它虛函數的時候,你就應該讓析構函數爲虛。有其它虛函數,就意味着這個類要被繼承,就意味着它有點“interface”的味道了。這樣一來,程序員就可能會以基類指針來指向由它的繼承類所實例化而來的對象,而能否通過基類指針來正常釋放這樣的對象就要看析構函數是否爲虛了。 例如:


 
如果Base的析構函數不是虛的,那麼Derived的析構函數就不會被調用——這常常會帶來惡果:比如,Derived中分配的資源沒有被釋放。

Q: C++中爲何沒有虛擬構造函數?

A: 虛擬機制的設計目的是使程序員在不完全瞭解細節(比如只知該類實現了某個界面,而不知該類確切是什麼東東)的情況下也能使用對象。但是,要建立一個對象,可不能只知道“這大體上是什麼”就完事——你必須完全瞭解全部細節,清楚地知道你要建立的對象是究竟什麼。所以,構造函數當然不能是虛的了。
 
不過有時在建立對象時也需要一定的間接性,這就需要用點技巧來實現了。(詳見《The C++ Programming Language》,第三版,15.6.2)這樣的技巧有時也被稱作“虛擬構造函數”。我這裏舉個使用抽象類來“虛擬構造對象”的例子:

 
看明白了沒有?上述代碼其實運用了Factory模式的一個變體。關鍵之處是,user()被完全孤立開了——它對AX,AY這些類一無所知。(嘿嘿,有時無知有無知的好處 ^_^)
 

Q: 爲何無法在派生類中重載?

A: 這個問題常常是由這樣的例子中產生的:

 
程序運行結果是:

而不是某些人(錯誤地)猜想的那樣:

換句話說,在D和B之間沒有重載發生。你調用了pd->f(),編譯器就在D的名字域裏找啊找,找到double f(double)後就調用它了。編譯器懶得再到B的名字域裏去看看有沒有哪個函數更符合要求。記住,在C++中,沒有跨域重載——繼承類和基類雖然關係很親密,但也不能壞了這條規矩。詳見《The Design and Evolution of C++》或者《The C++ Programming Language》第三版。

不過,如果你非得要跨域重載,也不是沒有變通的方法——你就把那些函數弄到同一個域裏來好了。使用一個using聲明就可以搞定。


這樣一來,結果就是

重載發生了——因爲D中的那句 using B::f 明確告訴編譯器,要把B域中的f引入當前域,請編譯器“一視同仁”。

Q: 我能從構造函數調用虛函數嗎?

A: 可以。不過你得悠着點。當你這樣做時,也許你自己都不知道自己在幹什麼!在構造函數中,虛擬機制尚未發生作用,因爲此時overriding尚未發生。萬丈高樓平地起,總得先打地基吧?對象的建立也是這樣——先把基類構造完畢,然後在此基礎上構造派生類。
 
看看這個例子:

 
這段程序經編譯運行,得到這樣的結果:


析構則正相反,遵循從繼承類到基類的順序(拆房子總得從上往下拆吧?),所以其調用虛函數的行爲和在構造函數中一樣:虛函數此時此刻被綁定到哪裏(當然應該是基類啦——因爲繼承類已經被“拆”了——析構了!),調用的就是哪個函數。

更多細節請見《The Design and Evolution of C++》,13.2.4.2 或者《The C++ Programming Language》第三版,15.4.3 。

有時,這條規則被解釋爲是由於編譯器的實作造成的。[譯註:從實作角度可以這樣解釋:在許多編譯器中,直到構造函數調用完畢,vtable才被建立,此時虛函數才被動態綁定至繼承類的同名函數。] 但事實上不是這麼一回事——讓編譯器實作成“構造函數中調用虛函數也和從其他函數中調用一樣”是很簡單的[譯註:只要把vtable的建立移至構造函數調用之前即可]。關鍵還在於語言設計時的考量——讓虛函數可以求助於基類提供的通用代碼。[譯註:先有雞還是先有蛋?Bjarne實際上是在告訴你,不是“先有實作再有規則”,而是“如此實作,因爲規則如此”。]

Q: 有"placement delete"嗎?

A: 沒有。不過如果你真的想要,你就說嘛——哦不,我的意思是——你可以自己寫一個。
 
我們來看看將對象放至某個指定場所的placement new:
 






 
現在我們可以寫:

但之後我們如何正確刪除這些對象?沒有內置“placement delete”的理由是,沒辦法提供一個通用的placement delete。C++的類型系統沒辦法讓我們推斷出p1是指向被放置在a1中的對象。即使我們能夠非常天才地推知這點,一個簡單的指針賦值操作也會讓我們重陷茫然。不過,程序員本人應該知道在他自己的程序中什麼指向什麼,所以可以有解決方案:

 
這樣我們就可以寫:
 
  destroy(p1,a1);
  destroy(p2,a2);
  destroy(p3,a3);
 
如果Arena自身跟蹤放置其中的對象,那麼你可以安全地寫出destroy()函數 ,把“保證無錯”的監控任務交給Arena,而不是自己承擔。

如何在類繼承體系中定義配對的operator new() 和 operator delete() 可以參看 《The C++ Programming Language》,Special Edition,15.6節 ,《The Design and Evolution of C++》,10.4節,以及《The C++ Programming Language》,Special Edition,19.4.5節。[譯註:此處按原文照譯。前面有提到“參見《The C++ Programming Language》第三版”的,實際上特別版(Special Edition)和較近重印的第三版沒什麼區別。]

 

A: 可以的,但何必呢?好吧,也許有兩個理由:

  • 出於效率考慮——不希望我的函數調用是虛的
  • 出於安全考慮——確保我的類不被用作基類(這樣我拷貝對象時就不用擔心對象被切割(slicing)了)[譯註:“對象切割”指,將派生類對象賦給基類變量時,根據C++的類型轉換機制,只有包括在派生類中的基類部分被拷貝,其餘部分被“切割”掉了。]
根據我的經驗,“效率考慮”常常純屬多餘。在C++中,虛函數調用如此之快,和普通函數調用並沒有太多的區別。請注意,只有通過指針或者引用調用時纔會啓用虛擬機制;如果你指名道姓地調用一個對象,C++編譯器會自動優化,去除任何的額外開銷。

如果爲了和“虛函數調用”說byebye,那麼確實有給類繼承體系“封頂”的需要。在設計前,不訪先問問自己,這些函數爲何要被設計成虛的。我確實見過這樣的例子:性能要求苛刻的函數被設計成虛的,僅僅因爲“我們習慣這樣做”!

好了,無論如何,說了那麼多,畢竟你只是想知道,爲了某種合理的理由,你能不能防止別人繼承你的類。答案是可以的。可惜,這裏給出的解決之道不夠乾淨利落。你不得不在在你的“封頂類”中虛擬繼承一個無法構造的輔助基類。還是讓例子來告訴我們一切吧:



 
(參見《The Design and Evolution of C++》,11.4.3節)

Q: 爲什麼我無法限制模板的參數?

A: 呃,其實你是可以的。而且這種做法並不難,也不需要什麼超出常規的技巧。

讓我們來看這段代碼:


如果c不符合constraints,出現了類型錯誤,那麼錯誤將發生在相當複雜的for_each解析之中。比如說,參數化的類型被要求實例化int型,那麼我們無法爲之調用Shape::draw()。而我們從編譯器中得到的錯誤信息是含糊而令人迷惑的——因爲它和標準庫中複雜的for_each糾纏不清。

爲了早點捕捉到這個錯誤,我們可以這樣寫代碼:


我們注意到,前面加了一行Shape *p的定義(儘管就程序本身而言,p是無用的)。如果不可將c.front()賦給Shape *p,那麼就大多數現代編譯器而言,我們都可以得到一條含義清晰的出錯信息。這樣的技巧在所有語言中都很常見,而且對於所有“不同尋常的構造”都不得不如此。[譯註:意指對於任何語言,當我們開始探及極限,那麼不得不寫一些高度技巧性的代碼。]
 
不過這樣做不是最好。如果要我來寫實際代碼,我也許會這樣寫:

 
這就使代碼通用且明顯地體現出我的意圖——我在使用斷言[譯註:即明確斷言typename Container是draw_all()所接受的容器類型,而不是令人迷惑地定義了一個Shape *指針,也不知道會不會在後面哪裏用到]。Can_copy()模板可被這樣定義:

Can_copy在編譯期間檢查確認T1可被賦於T2。Can_copy<T,Shape*>檢查確認T是一個Shape*類型,或者是一個指向Shape的公有繼承類的指針,或者是用戶自定義的可被轉型爲Shape *的類型。注意,這裏Can_copy()的實現已經基本上是最優化的了:一行代碼用來指明需要檢查的constraints[譯註:指第1行代碼;constraints爲T2],和要對其做這個檢查的類型[譯註:要作檢查的類型爲T1] ;一行代碼用來精確列出所要檢查是否滿足的constraints(constraints()函數) [譯註:第2行之所以要有2個子句並不是重複,而是有原因的。如果T1,T2均是用戶自定義的類,那麼T2 c = a; 檢測能否缺省構造;b = a; 檢測能否拷貝構造] ;一行代碼用來提供執行這些檢查的機會 [譯註:指第3行。Can_copy是一個模板類;constraints是其成員函數,第2行只是定義,而未執行] 。
 
[譯註:這裏constraints實現的關鍵是依賴C++強大的類型系統,特別是類的多態機制。第2行代碼中T2 c = a; b = a; 能夠正常通過編譯的條件是:T1實現了T2的接口。具體而言,可能是以下4種情況:(1) T1,T2 同類型 (2) 重載operator = (3) 提供了 cast operator (類型轉換運算符)(4) 派生類對象賦給基類指針。說到這裏,記起我曾在以前的一篇文章中說到,C++的genericity實作——template不支持constrained genericity,而Eiffel則從語法級別支持constrained genericity(即提供類似於template <typename T as Comparable> xxx 這樣的語法——其中Comparable即爲一個constraint)。曾有讀者指出我這樣說是錯誤的,認爲C++ template也支持constrained genericity。現在這部分譯文給出了通過使用一些技巧,將OOP和GP的方法結合,從而在C++中巧妙實現constrained genericity的方法。對於愛好C++的讀者,這種技巧是值得細細品味的。不過也不要因爲太執著於各種細枝末節的代碼技巧而喪失了全局眼光。有時語言支持方面的欠缺可以在設計層面(而非代碼層面)更優雅地彌補。另外,這能不能算“C++的template支持constrained genericity”,我保留意見。正如,用C通過一些技巧也可以OOP,但我們不說C語言支持OOP。]
 
請大家再注意,現在我們的定義具備了這些我們需要的特性:
  • 你可以不通過定義/拷貝變量就表達出constraints[譯註:實則定義/拷貝變量的工作被封裝在Can_copy模板中了] ,從而可以不必作任何“那個類型是這樣被初始化”之類假設,也不用去管對象能否被拷貝、銷燬(除非這正是constraints所在)。[譯註:即——除非constraints正是“可拷貝”、“可銷燬”。如果用易理解的僞碼描述,就是template <typename T as Copy_Enabled> xxx,template <typename T as Destructible> xxx 。]
  • 如果使用現代編譯器,constraints不會帶來任何額外代碼
  • 定義或者使用constraints均不需使用宏定義
  • 如果constraints沒有被滿足,編譯器給出的錯誤消息是容易理解的。事實上,給出的錯誤消息包括了單詞“constraints” (這樣,編碼者就能從中得到提示)、constraints的名稱、具體的出錯原因(比如“cannot initialize Shape* by double*”)

既然如此,我們幹嗎不乾脆在C++語言本身中定義類似Can_copy()或者更優雅簡潔的語法呢?The Design and Evolution of C++分析了此做法帶來的困難。已經有許許多多設計理念浮出水面,只爲了讓含constraints的模板類易於撰寫,同時還要讓編譯器在constraints不被滿足時給出容易理解的出錯消息。比方說,我在Can_copy中“使用函數指針”的設計就來自於Alex Stepanov和Jeremy Siek。我認爲我的Can_copy()實作還不到可以標準化的程度——它需要更多實踐的檢驗。另外,C++使用者會遭遇許多不同類型的constraints,目前看來還沒有哪種形式的帶constraints的模板獲得壓倒多數的支持。

已有不少關於constraints的“內置語言支持”方案被提議和實作。但其實要表述constraint根本不需要什麼異乎尋常的東西:畢竟,當我們寫一個模板時,我們擁有C++帶給我們的強有力的表達能力。讓代碼來爲我的話作證吧:


事實上Derived_from並不檢查繼承性,而是檢查可轉換性。不過Derive_from常常是一個更好的名字——有時給constraints起個好名字也是件需細細考量的活兒。

 

Q: 我們已經有了 "美好的老qsort()",爲什麼還要用sort()?

A: 對於初學者而言,

 
看上去有點古怪。還是

 
比較好理解,是吧。那麼,這點理由就足夠讓你舍qsort而追求sort了。對於老手來說,sort()要比qsort()快的事實也會讓你心動不已。而且sort是泛型的,可以用於任何合理的容器組合、元素類型和比較算法。例如:

另外,還有許多人欣賞sort()的類型安全性——要使用它可不需要任何強制的類型轉換。對於標準類型,也不必寫compare()函數,省事不少。如果想看更詳盡的解釋,參看我的《Learning Standard C++ as a New Language》一文。

另外,爲何sort()要比qsort()快?因爲它更好地利用了C++的內聯語法語義。

Q: 什麼是function object?

A: Function object是一個對象,不過它的行爲表現像函數。一般而言,它是由一個重載了operator()的類所實例化得來的對象。

Function object的涵義比通常意義上的函數更廣泛,因爲它可以在多次調用之間保持某種“狀態”——這和靜態局部變量有異曲同工之妙;不過這種“狀態”還可以被初始化,還可以從外面來檢測,這可要比靜態局部變量強了。我們來看一個例子:


 
這裏我要提請大家注意:一個function object可被漂亮地內聯化(inlining),因爲對於編譯器而言,沒有討厭的指針來混淆視聽,所以這樣的優化很容易進行。[譯註:這指的是將operator()定義爲內聯函數,可以帶來效率的提高。] 作爲對比,編譯器幾乎不可能通過優化將“通過函數指針調用函數”這一步驟所花的開銷省掉,至少目前如此。

在標準庫中function objects被廣泛使用,這給標準庫帶來了極大的靈活性和可擴展性。

[譯註:C++是一個博採衆長的語言,function object的概念就是從functional programming中借來的;而C++本身的強大和表現力的豐富也使這種“拿來主義”成爲可能。一般而言,在使用function object的地方也常可以使用函數指針;在我們還不熟悉function object的時候我們也常常是使用指針的。但定義一個函數指針的語法可不是太簡單明瞭,而且在C++中指針早已背上了“錯誤之源”的惡名。更何況,通過指針調用函數增加了間接開銷。所以,無論爲了語法的優美還是效率的提高,都應該提倡使用function objects。

下面我們再從設計模式的角度來更深入地理解function objects:這是Visitor模式的典型應用。當我們要對某個/某些對象施加某種操作,但又不想將這種操作限定死,那麼就可以採用Visitor模式。在Design Patterns一書中,作者把這種模式實作爲:通過一個Visitor類來提供這種操作(在前面Bjarne Stroustrup的代碼中,Sum就是一個Visitor的變體),用Visitor類實例化一個visitor對象(當然,在前面的代碼中對應的是s);然後在Iterator的迭代過程中,爲每一個對象調用visitor.visit()。這裏visit()是Visitor類的一個成員函數,作用相當於Sum類中那個“特殊的成員函數”——operator();visit()也完全可以被定義爲內聯函數,以去除間接性,提高性能。在此提請讀者注意,C++把重載的操作符也看作函數,只不過是具有特殊函數名的函數。所以實際上Design Patterns一書中Visitor模式的示範實作和這裏function object的實作大體上是等價的。一個function object也就是一個特殊的Visitor。]

Q: 我應該怎樣處理內存泄漏?

A: 很簡單,只要寫“不漏”的代碼就完事了啊。顯然,如果你的代碼到處是new、delete、指針運算,那你想讓它“不漏”都難。不管你有多麼小心謹慎,君爲人,非神也,錯誤在所難免。最終你會被自己越來越複雜的代碼逼瘋的——你將投身於與內存泄漏的奮鬥之中,對bug們不離不棄,直至山峯沒有棱角,地球不再轉動。而能讓你避免這樣困境的技巧也不復雜:你只要倚重隱含在幕後的分配機制——構造和析構,讓C++的強大的類系統來助你一臂之力就OK了。標準庫中的那些容器就是很好的實例。它們讓你不必化費大量的時間精力也能輕鬆愜意地管理內存。我們來看看下面的示例代碼——設想一下,如果沒有了string和vector,世界將會怎樣?如果不用它們,你能第一次就寫出毫無內存錯誤的同樣功能代碼嗎?

請注意這裏沒有顯式的內存管理代碼。沒有宏,沒有類型轉換,沒有溢出檢測,沒有強制的大小限制,也沒有指針。如果使用function object和標準算法[譯註:指標準庫中提供的泛型算法],我連Iterator也可以不用。不過這畢竟只是一個小程序,殺雞焉用牛刀?

當然,這些方法也並非無懈可擊,而且說起來容易做起來難,要系統地使用它們也並不總是很簡單。不過,無論如何,它們的廣泛適用性令人驚訝,而且通過移去大量的顯式內存分配/釋放代碼,它們確實增強了代碼的可讀性和可管理性。早在1981年,我就指出通過大幅度減少需要顯式加以管理的對象數量,使用C++“將事情做對”將不再是一件極其費神的艱鉅任務。

如果你的應用領域沒有能在內存管理方面助你一臂之力的類庫,那麼如果你還想讓你的軟件開發變得既快捷又能輕鬆得到正確結果,最好是先建立這樣一個庫。

如果你無法讓內存分配和釋放成爲對象的“自然行爲”,那麼至少你可以通過使用資源句柄來儘量避免內存泄漏。這裏是一個示例:假設你需要從函數返回一個對象,這個對象是在自由內存堆上分配的;你可能會忘記釋放那個對象——畢竟我們無法通過檢查指針來確定其指向的對象是否需要被釋放,我們也無法得知誰應該負責釋放它。那麼,就用資源句柄吧。比如,標準庫中的auto_ptr就可以幫助澄清:“釋放對象”責任究竟在誰。我們來看:


這裏只是內存資源管理的例子;至於其它類型的資源管理,可以如法炮製。

如果在你的開發環境中無法系統地使用這種方法(比方說,你使用了第三方提供的古董代碼,或者遠古“穴居人”參與了你的項目開發),那麼你在開發過程中可千萬要記住使用內存防漏檢測程序,或者乾脆使用垃圾收集器(Garbage Collector)。

 

Q: 爲何捕捉到異常後不能繼續執行後面的代碼呢?

A: 這個問題,換句話說也就是:爲什麼C++不提供這樣一個原語,能使你處理異常過後返回到異常拋出處繼續往下執行?[譯註:比如,一個簡單的resume語句,用法和已有的return語句類似,只不過必須放在exception handler的最後。]

嗯,從異常處理代碼返回到異常拋出處繼續執行後面的代碼的想法很好[譯註:現行異常機制的設計是:當異常被拋出和處理後,從處理代碼所在的那個catch塊往下執行],但主要問題在於——exception handler不可能知道爲了讓後面的代碼正常運行,需要做多少清除異常的工作[譯註:畢竟,當有異常發生,事情就有點不太對勁了,不是嗎;更何況收拾爛攤子永遠是件麻煩的事],所以,如果要讓“繼續執行”能夠正常工作,寫throw代碼的人和寫catch代碼的人必須對彼此的代碼都很熟悉,而這就帶來了複雜的相互依賴關係[譯註:既指開發人員之間的“相互依賴”,也指代碼間的相互依賴——緊耦合的代碼可不是好代碼哦 :O) ],會帶來很多麻煩的維護問題。

在我設計C++的異常處理機制的時候,我曾認真地考慮過這個問題;在C++標準化的過程中,這個問題也被詳細地討論過。(參見《The Design and Evolution of C++》中關於異常處理的章節)如果你想試試看在拋出異常之前能不能解決問題然後繼續往下執行,你可以先調用一個“檢查—恢復”函數,然後,如果還是不能解決問題,再把異常拋出。一個這樣的例子是new_handler。

Q: 爲何C++中沒有C中realloc()的對應物?

A: 如果你一定想要的話,你當然可以使用realloc()。不過,realloc() 只和通過malloc()之類C函數分配得到的內存“合作愉快”,在分配的內存中不能有具備用戶自定義構造函數的對象。請記住:與某些天真的人們的想象相反,realloc()必要時是會拷貝大塊的內存到新分配的連續空間中的。所以,realloc沒什麼好的 ^_^

在C++中,處理內存重分配的較好辦法是使用標準庫中的容器,比如vector。[譯註:這些容器會自己管理需要的內存,在必要時會“增長尺寸”——進行重分配。]

Q: 我如何使用異常處理?

A: 參見《The C++ Programming Language》14章8.3節,以及附錄E。附錄E主要闡述如何撰寫“exception-safe”代碼,這個附錄可不是寫給初學者看的。一個關鍵技巧是“資源分配即初始化”——這種技巧通過“類的析構函數”給易造成混亂的“資源管理”帶來了“秩序的曙光”。

Q: 我如何從標準輸入中讀取string?

A: 如果要讀以空白結束的單個單詞,可以這樣:

 
請注意,這裏沒有顯式的內存管理代碼,也沒有限制尺寸而可能會不小心溢出的緩衝區。 [譯註:似乎Bjarne常驕傲地宣稱這點——因爲這是string乃至整個標準庫帶來的重大好處之一,確實值得自豪;而在老的C語言中,最讓程序員抱怨的也是內置字符串類型的缺乏以及由此引起的“操作字符串所需要之複雜內存管理措施”所帶來的麻煩。Bjarne一定在得意地想,“哈,我的叫C++的小baby終於長大了,趨向完美了!” :O) ]

如果你需要一次讀一整行,可以這樣:


 
關於標準庫所提供之功能的簡介(諸如iostream,stream),參見《The C++ Programming Language》第三版的第三章。如果想看C和C++的輸入輸出功能使用之具體比較,參看我的《Learning Standard C++ as a New Language》一文。

Q: 爲何C++不提供“finally”結構?

A: 因爲C++提供了另一種機制,完全可以取代finally,而且這種機制幾乎總要比finally工作得更好:就是——“分配資源即初始化”。(見《The C++ Programming Language》14.4節)基本的想法是,用一個局部對象來封裝一個資源,這樣一來局部對象的析構函數就可以自動釋放資源。這樣,程序員就不會“忘記釋放資源”了。 [譯註:因爲C++的對象“生命週期”機制替他記住了 :O) ] 下面是一個例子:

 
在一個系統中,每一樣資源都需要一個“資源局柄”對象,但我們不必爲每一個資源都寫一個“finally”語句。在實作的系統中,資源的獲取和釋放的次數遠遠多於資源的種類,所以“資源分配即初始化”機制產生的代碼要比“finally”機制少。
 
[譯註:Object Pascal,Java,C#等語言都有finally語句塊,常用於發生異常時對被分配資源的資源的處理——這意味着有多少次分配資源就有多少finally語句塊(少了一個finally就意味着有一些資源分配不是“exception safe”的);而“資源分配即初始化”機制將原本放在finally塊中的代碼移到了類的析構函數中。我們只需爲每一類資源提供一個封裝類即可。需代碼量孰多孰少?除非你的系統中每一類資源都只被使用一次——這種情況下代碼量是相等的;否則永遠是前者多於後者 :O) ]

另外,請看看《The C++ Programming Language》附錄E中的資源管理例子。

Q: 那個auto_ptr是什麼東東啊?爲什麼沒有auto_array?

A: 哦,auto_ptr是一個很簡單的資源封裝類,是在<memory>頭文件中定義的。它使用“資源分配即初始化”技術來保證資源在發生異常時也能被安全釋放(“exception safety”)。一個auto_ptr封裝了一個指針,也可以被當作指針來使用。當其生命週期到了盡頭,auto_ptr會自動釋放指針。例如:


Auto_ptr是一個輕量級的類,沒有引入引用計數機制。如果你把一個auto_ptr(比如,ap1)賦給另一個auto_ptr(比如,ap2),那麼ap2將持有實際指針,而ap1將持有零指針。例如:


 
運行結果應該是先顯示一個零指針,然後纔是一個實際指針,就像這樣:

 
auto_ptr::get()返回實際指針。

這裏,語義似乎是“轉移”,而非“拷貝”,這或許有點令人驚訝。特別要注意的是,不要把auto_ptr作爲標準容器的參數——標準容器要求通常的拷貝語義。例如:


一個auto_ptr只能持有指向單個元素的指針,而不是數組指針:


 
上述代碼會出錯,因爲析構函數是使用delete而非delete[]來釋放指針的,所以後面的n-1個X沒有被釋放。

那麼,看來我們應該用一個使用delete[]來釋放指針的,叫auto_array的類似東東來放數組了?哦,不,不,沒有什麼auto_array。理由是,不需要有啊——我們完全可以用vector嘛:


 
如果在 // ... 部分發生了異常,v的析構函數會被自動調用。
 

Q: C和C++風格的內存分配/釋放可以混用嗎?

A: 可以——從你可在一個程序中同時使用malloc()和new的意義上而言。

不可以——從你無法delete一個以malloc()分配而來之對象的意義上而言。你也無法free()或realloc()一個由new分配而來的對象。

C++的new和delete運算符確保構造和析構正常發生,但C風格的malloc()、calloc()、free()和realloc()可不保證這點。而且,沒有任何人能向你擔保,new/delete和malloc/free所掌控的內存是相互“兼容”的。如果在你的代碼中,兩種風格混用而沒有給你造成麻煩,那我只能說:直到目前爲止,你是非常幸運的 :O)

如果你因爲思念“美好的老realloc()”(許多人都思念她)而無法割捨整個古老的C內存分配機制(愛屋及烏?),那麼考慮使用標準庫中的vector吧。例如:


 
Vector會按需要自動增長的。

我的《Learning Standard C++ as a New Language》一文中給出了其它例子,可以參考。

Q: 想從void *轉換,爲什麼必須使用換型符?

A: 在C中,你可以隱式轉換,但這是不安全的,例如:


 
如果你使用T*類型的指針,該指針卻不指向T類型的對象,後果可能是災難性的;所以在C++中如果你要將void*換型爲T*,你必須使用顯式換型:

 
或者,更好的是,使用新的換型符,以使換型操作更爲醒目:

 
當然,最好的還是——不要換型。

在C中一類最常見的不安全換型發生在將malloc()分配而來的內存賦給某個指針之時,例如:


 
在C++中,應該使用類型安全的new操作符:

 
而且,new還有附帶的好處:
  • new不會“偶然”地分配錯誤大小的內存
  • new自動檢查內存是否已枯竭
  • new支持初始化
例如: 

A: 如何在類中定義常量?

Q: 如果你想得到一個可用於常量表達式中的常量,例如數組大小的定義,那麼你有兩種選擇:





那麼,爲何要有這些不方便的限制?因爲類通常聲明在頭文件中,而頭文件往往被許多單元所包含。[所以,類可能會被重複聲明。]但是,爲了避免鏈接器設計的複雜化,C++要求每個對象都只能被定義一次。如果C++允許類內定義要作爲對象被存在內存中的實體,那麼這項要求就無法滿足了。關於C++設計時的一些折衷,參見《The Design and Evolution of C++》。

如果這個常量不需要被用於常量表達式,那麼你的選擇餘地就比較大了:


 
只有當static成員是在類外被定義的,你纔可以獲取它的地址,例如:

Q: 爲何delete操作不把指針置零?

A: 嗯,問得挺有道理的。我們來看:

 
如果代碼中的//...部分沒有再次給p分配內存,那麼這段代碼就對同一片內存釋放了兩次。這是個嚴重的錯誤,可惜C++無法有效地阻止你寫這種代碼。不過,我們都知道,釋放空指針是無危害的,所以如果在每一個delete p;後面都緊接一個p = 0;,那麼兩次釋放同一片內存的錯誤就不會發生了。儘管如此,在C++中沒有任何語法可以強制程序員在釋放指針後立刻將該指針歸零。所以,看來避免犯這樣的錯誤的重任只能全落在程序員肩上了。或許,delete自動把指針歸零真是個好主意?

哦,不不,這個主意不夠“好”。一個理由是,被delete的指針未必是左值。我們來看:


 
你讓delete把什麼自動置零?也許這樣的例子不常見,但足可證明“delete自動把指針歸零”並不保險。[譯註:事實上,我們真正想要的是:“任何指向被釋放的內存區域的指針都被自動歸零”——但可惜除了Garbage Collector外沒什麼東東可以做到這點。] 再來看個簡單例子:

 
C++標準其實允許編譯器實作爲“自動把傳給delete的左值置零”,我也希望編譯器廠商這樣做,但看來廠商們並不喜歡這樣。一個理由就是上述例子——第3行語句如果delete把p自動置零了又如何呢?q又沒有被自動置零,第4行照樣出錯。

如果你覺得釋放內存時把指針置零很重要,那麼不妨寫這樣一個destroy函數:


不妨把delete帶來的麻煩看作“儘量少用new/delete,多用標準庫中的容器”之另一條理由吧 :O)

請注意,把指針作爲引用傳遞(以便delete可以把指針置零)會帶來額外的效益——防止右值被傳遞給destroy() :


 

Q: 我可以寫"void main()"嗎?

A: 這樣的定義

 
不是C++,也不是C。(參見ISO C++ 標準 3.6.1[2] 或 ISO C 標準 5.1.2.2.1) 一個遵從標準的編譯器實作應該接受

和 

 
編譯器也可以提供main()的更多重載版本,不過它們都必須返回int,這個int是返回給你的程序的調用者的,這是種“負責”的做法,“什麼都不返回”可不大好哦。如果你程序的調用者不支持用“返回值”來交流,這個值會被自動忽略——但這也不能使void main()成爲合法的C++或C代碼。即使你的編譯器支持這種定義,最好也不要養成這種習慣——否則你可能被其他C/C++認爲淺薄無知哦。
 
在C++中,如果你嫌麻煩,可以不必顯式地寫出return語句。編譯器會自動返回0。例如:

 
麻煩嗎?不麻煩,int main()比void main()還少了一個字母呢 :O)另外,還要請你注意:無論是ISO C++還是C99都不允許你省略返回類型定義。這也就是說,和C89及ARM C++[譯註:指Margaret Ellis和Bjarne Stroustrup於1990年合著的《The Annotated C++ Reference Manual》中描述的C++]不同,int並不是缺省返回值。所以,

會出錯,因爲main()函數缺少返回類型。
 

Q: 爲何我不能重載“.”、“::”和“sizeof”等操作符?

A: 大部分的操作符是可以被重載的,例外的只有“.”、“::”、“?:”和“sizeof”。沒有什麼非禁止operator?:重載的理由,只不過沒有必要而已。另外,expr1?expr2:expr3的重載函數無法保證expr2和expr3中只有一個被執行。

而“sizeof”無法被重載是因爲不少內部操作,比如指針加法,都依賴於它,例如:


 
這樣,sizeof(X)無法在不違背基本語言規則的前提下表達什麼新的語義。

在N::m中,N和m都不是表達式,它們只是編譯器“認識”的名字,“::”執行的實際操作是編譯時的名字域解析,並沒有表達式的運算牽涉在內。或許有人會覺得重載一個“x::y”(其中x是實際對象,而非名字域或類名)是一個好主意,但這樣做引入了新的語法[譯註:重載的本意是讓操作符可以有新的語義,而不是更改語法——否則會引起混亂],我可不認爲新語法帶來的複雜性會給我們什麼好處。

原則上來說,“.”運算符是可以被重載的,就像“->”一樣。不過,這會帶來語義的混淆——我們到底是想和“.”後面的對象打交道呢,還是“.”後面的東東所實際指向的實體打交道呢?看看這個例子(它假設“.”重載是可以的):


這個問題有好幾種解決方案。在C++標準化之時,何種方案爲佳並不明顯。細節請參見《The Design and Evolution of C++》。

Q: 我怎樣才能把整數轉化爲字符串?

A: 最簡單的方法是使用stringstream :

 
當然,很自然地,你可以用這種方法來把任何可通過“<<”輸出的類型轉化爲string。想知道string流的細節嗎?參見《The C++ Programming Language》,21.5.3節。
 

Q: “int* p;”和“int *p;”,到底哪個正確?

A: 如果讓計算機來讀,兩者完全等同,都是正確的。我們還可以聲明成“int * p”或“int*p”。編譯器不會理會你是不是在哪裏多放了幾個空格。

不過如果讓人來讀,兩者的含義就有所不同了。代碼的書寫風格是很重要的。C風格的表達式和聲明式常被看作比“necessary evil”[譯註:“必要之惡”,意指爲了達到某種目的而不得不付出的代價。例如有人認爲環境的破壞是經濟發展帶來的“necessary evil”]更糟的東西,而C++則很強調類型。所以,“int *p”和“int* p”之間並無對錯之分,只有風格之爭。

一個典型的C程序員會寫“int *p”,而且振振有詞地告訴你“這表示‘*p是一個int’”——聽上去挺有道理的。這裏,*和p綁在了一起——這就是C的風格。這種風格強調的是語法。

而一個典型的C++程序員會寫“int* p”,並告訴你“p是一個指向int的指針,p的類型是int*”。這種風格強調的是類型。當然,我喜歡這種風格 :O) 而且,我認爲,類型是非常重要的概念,我們應該注重類型。它的重要性絲毫不亞於C++語言中的其它“較爲高級的部分”。[譯註:諸如RTTI,各種cast,template機制等,可稱爲“較高級的部分”了吧,但它們其實也是類型概念的擴展和運用。我曾寫過兩篇談到C++和OOP的文章發表在本刊上,文中都強調了理解“類型”之重要性。我還曾譯過Object Unencapsulated (這本書由作者先前所著在網上廣爲流傳的C++?? A Critique修訂而來)中講類型的章節,這本書的作者甚至稱Object Oriented Programming應該正名爲Type Oriented Programming——“面向類型編程”!這有點矯枉過正了,但類型確是編程語言之核心部分。]

當聲明單個變量時,int *和int*的差別並不是特別突出,但當我們要一次聲明多個變量時,易混淆之處就全暴露出來了:


 
這裏,p1的類型到底是int還是int *呢?把*放得離p近一點也同樣不能澄清問題:

 
看來爲了保險起見,只好一次聲明一個變量了——特別是當聲明伴隨着初始化之時。[譯註:本FAQ中凡原文爲declare/declaration的均譯爲聲明;define/definition均譯爲定義。通常認爲,兩者涵義之基本差別是:“聲明”只是爲編譯器提供信息,讓編譯器在符號表中爲被聲明的符號(比如類型名,變量名,函數名等)保留位置,而不用指明該符號所對應的具體語義——即:沒有任何內存空間的分配或者實際二進制代碼的生成。而“定義”則須指明語義——如果把“聲明”比作在辭典中爲一個新詞保留條目;那麼“定義”就好比在條目中對這個詞的意思、用法給出詳細解釋。當我們說一個C++語句是“定義”,那麼編譯器必定會爲該語句產生對應的機器指令或者分配內存,而被稱爲“聲明”的語句則不會被編譯出任何實際代碼。從這個角度而言,原文中有些地方雖作者寫的是“對象、類、類型的聲明(declaration)”,但或許改譯爲“定義”較符合我們的理解。不過本譯文還是採用忠於原文的譯法,並不按照我的理解而加以更改。特此說明。另外,譯文中凡涉及我個人對原文的理解、補充之部分均以譯註形式給出,供讀者參考。]人們一般不太可能寫出像這樣的代碼:

 
如果真的有人這樣寫,編譯器也不會同意——它會報錯的。

每當達到某種目的有兩條以上途徑,就會有些人被搞糊塗;每當一些選擇是出於個人喜好,爭論就會無休無止。堅持一次只聲明一個指針並在聲明時順便初始化,困擾我們已久的混淆之源就會隨風逝去。如果你想了解有關C的聲明語法的更多討論,參見《The Design and Evolution of C++》 。

Q: 何種代碼佈局風格爲佳?

A: 哦,這是個人品味問題了。人們常常很重視代碼佈局之風格,但或許風格的一致性要比選擇何種風格更重要。如果非要我爲我的個人偏好建立“邏輯證明”,和別人一樣,我會頭大的 :O)

我個人喜歡使用“K&R”風格,如果算上那些C語言中不存在的構造之使用慣例,那麼人們有時也稱之爲“Stroustrup”風格。例如:


 
這種風格比較節省“垂直空間”——我喜歡讓儘量多的內容可以顯示在一屏上 :O) 而函數定義開始的花括號之所以如此放置,是因爲這樣一來就和類定義區分開來,我就可以一眼看出:噢,這是函數!

正確的縮進非常重要。

一些設計問題,比如使用抽象類來表示重要的界面、使用模板來表示靈活而可擴展的類型安全抽象、正確使用“異常”來表示錯誤,遠遠要比代碼風格重要。

[譯註:《The Practice of Programming》中有一章對“代碼風格”問題作了詳細的闡述。]

Q: 我該把const寫在類型前面還是後面?

A: 我是喜歡寫在前面的。不過這只是個人口味的問題。“const T”和“T const”均是允許的,而且它們是等價的。例如:

 
我想,使用第一種寫法更合乎語言習慣,比較不容易讓人迷惑 :O)

爲什麼會這樣?當我發明“const”(最早是被命名爲“readonly”且有一個叫“writeonly”的對應物)時,我讓它在前面和後面都行,因爲這不會帶來二義性。當時的C/C++編譯器對修飾符很少有強加的語序規則。

我不記得當時有過什麼關於語序的深思熟慮或相關的爭論。一些早期的C++使用者(特別是我)當時只是單純地覺得const int c = 10;要比int const c = 10;好看而已。或許,我是受了這件事實的影響:許多我早年寫的例子是用“readonly”修飾的,而readonly int c = 10;確實看上去要比int readonly c = 10;舒服。而最早的使用“const”的C/C++代碼是我用全局查找替換功能把readonly換成const而來的。我還記得和幾個人討論過關於語法“變體”問題,包括Dennis Ritchie。不過我不記得當時我們談的是哪幾種語言了。

另外,請注意:如果指針本身不可被修改,那麼const應該放在“*”的後面。例如:


Q: 宏有什麼不好嗎?

A: 宏不遵循C++的作用域和類型規則,這會帶來許多麻煩。因此,C++提供了能和語言其它部分“合作愉快”的替代機制,比如內聯函數、模板、名字空間機制。讓我們來看這樣的代碼:




把宏(而且只有宏)的名稱全部用大寫字母表示確實有助於緩解問題,但宏是沒有語言級保護機制的。例如,在以上例子中alpha和beta在S的作用域中,是S的成員變量,但這對於宏毫無影響。宏的展開是在編譯前進行的,展開程序只是把源文件看作字符流而已。這也是C/C++程序設計環境的欠缺之處:計算機和電腦眼中的源文件的涵義是不同的。

不幸的是,你無法確保其他程序員不犯你所認爲的“愚蠢的”錯誤。比方說,近來有人告訴我,他們遇到一個含“goto”語句的宏。我見到過這樣的代碼,也聽到過這樣的論點——有時宏中的“goto”是有用的。例如:


 
如果你是一個負責維護的程序員,這樣的代碼被提交到你面前,而宏定義(爲了給這個“戲法”增加難度而)被藏到了一個頭文件中(這種情況並非罕見),你作何感想?是不是一頭霧水?

一個常見而微妙的問題是,函數風格的宏不遵守函數參數調用規則。例如:


 
“d+1”的問題可以通過給宏定義加括號解決:

 
但是,“i++”被執行兩次的問題仍然沒有解決。

我知道有些(其它語言中)被稱爲“宏”的東西並不象C/C++預處理器所處理的“宏”那樣缺陷多多、麻煩重重,但我並不想改進C++的宏,而是建議你正確使用C++語言中的其他機制,比如內聯函數、模板、構造函數、析構函數、異常處理等。 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章