C++ Style and Technique FAQ (中文版)
Q: 這個簡單的程序……我如何把它搞定?
- 這是一個用標準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”作爲輸入終結的標誌。
Q: 爲何我編譯一個程序要花那麼多時間?
不過,也有可能原因在於你的程序——看看你的程序設計還能不能改進?編譯器是不是爲了順利產出正確的二進制碼而不得不喫進成百個頭文件、幾萬行的源代碼?原則上,只要對源碼適當優化一下,編譯緩慢的問題應該可以解決。如果癥結在於你的類庫供應商,那麼你大概除了“換一家類庫供應商”外確實沒什麼可做的了;但如果問題在於你自己的代碼,那麼完全可以通過重構(refactoring)來讓你的代碼更爲結構化,從而使源碼一旦有更改時需重編譯的代碼量最小。這樣的代碼往往是更好的設計:因爲它的藕合程度較低,可維護性較佳。
我們來看一個OOP的經典例子:
- 要確認“哪些功能會被所有的繼承類用到,而應在基類中實作”可不是件簡單的事。所以,基類的保護成員或許會隨着要求的變化而變化,其頻度遠高於公共界面之可能變化。例如,儘管我們把“center”作爲所有形狀的一個屬性(從而在基類中聲明)似乎是天經地義的,但因此而要在基類中時時維護三角形的中心座標是很麻煩的,還不如只在需要時才計算——這樣可以減少開銷。
- 和抽象的公共界面不同,保護成員可能會依賴實作細節,而這是Shape類的使用者所不願見到的。例如,絕大部分使用Shape的代碼應該邏輯上和color無關;但只要color的聲明在Shape類中出現了,就往往會導致編譯器將定義了“該操作系統中顏色表示”的頭文件讀入、展開、編譯。這都需要時間!
- 當基類中保護成員(比如前面說的center,color)的實作有所變化,那麼所有使用了Shape類的代碼都需要重新編譯——哪怕這些代碼中只有很少是真正要用到基類中的那個“語義變化了的保護成員”。
所以,在基類中放一些“對於繼承類之實作有幫助”的功能或許是出於好意,但實則是麻煩的源泉。用戶的要求是多變的,所以實作代碼也是多變的。將多變的代碼放在許多繼承類都要用到的基類之中,那麼變化可就不是局部的了,這會造成全局影響的!具體而言就是:基類所倚賴的一個頭文件變動了,那麼所有繼承類所在的文件都需重新編譯。
這樣分析過後,解決之道就顯而易見了:僅僅把基類用作爲抽象的公共界面,而將“對繼承類有用”的實作功能移出。
但是,如果確實有一些功能是要被所有繼承類(或者僅僅幾個繼承類)共享的,又不想在每個繼承類中重複這些代碼,那怎麼辦?也好辦:把這些功能封裝成一個類,如果繼承類要用到這些功能,就讓它再繼承這個類:
Q: 爲何空類的大小不是零?
Q: 爲什麼我必須把數據放到類的聲明之中?
Q: 爲何成員函數不是默認爲虛?
另外,有虛函數的類有虛機制的開銷[譯註:指存放vtable帶來的空間開銷和通過vtable中的指針間接調用帶來的時間開銷],通常而言每個對象增加的空間開銷是一個字長。這個開銷可不小,而且會造成和其他語言(比如C,Fortran)的不兼容性——有虛函數的類的內存數據佈局和普通的類是很不一樣的。[譯註:這種內存數據佈局的兼容性問題會給多語言混合編程帶來麻煩。]
《The Design and Evolution of C++》 中有更多關於設計理念的細節。
Q: 爲何析構函數不是默認爲虛?
那麼,何時我該讓析構函數爲虛呢?哦,答案是——當類有其它虛函數的時候,你就應該讓析構函數爲虛。有其它虛函數,就意味着這個類要被繼承,就意味着它有點“interface”的味道了。這樣一來,程序員就可能會以基類指針來指向由它的繼承類所實例化而來的對象,而能否通過基類指針來正常釋放這樣的對象就要看析構函數是否爲虛了。 例如:
Q: C++中爲何沒有虛擬構造函數?
Q: 爲何無法在派生類中重載?
換句話說,在D和B之間沒有重載發生。你調用了pd->f(),編譯器就在D的名字域裏找啊找,找到double f(double)後就調用它了。編譯器懶得再到B的名字域裏去看看有沒有哪個函數更符合要求。記住,在C++中,沒有跨域重載——繼承類和基類雖然關係很親密,但也不能壞了這條規矩。詳見《The Design and Evolution of C++》或者《The C++ Programming Language》第三版。
不過,如果你非得要跨域重載,也不是沒有變通的方法——你就把那些函數弄到同一個域裏來好了。使用一個using聲明就可以搞定。
Q: 我能從構造函數調用虛函數嗎?
析構則正相反,遵循從繼承類到基類的順序(拆房子總得從上往下拆吧?),所以其調用虛函數的行爲和在構造函數中一樣:虛函數此時此刻被綁定到哪裏(當然應該是基類啦——因爲繼承類已經被“拆”了——析構了!),調用的就是哪個函數。
更多細節請見《The Design and Evolution of C++》,13.2.4.2 或者《The C++ Programming Language》第三版,15.4.3 。
有時,這條規則被解釋爲是由於編譯器的實作造成的。[譯註:從實作角度可以這樣解釋:在許多編譯器中,直到構造函數調用完畢,vtable才被建立,此時虛函數才被動態綁定至繼承類的同名函數。] 但事實上不是這麼一回事——讓編譯器實作成“構造函數中調用虛函數也和從其他函數中調用一樣”是很簡單的[譯註:只要把vtable的建立移至構造函數調用之前即可]。關鍵還在於語言設計時的考量——讓虛函數可以求助於基類提供的通用代碼。[譯註:先有雞還是先有蛋?Bjarne實際上是在告訴你,不是“先有實作再有規則”,而是“如此實作,因爲規則如此”。]
Q: 有"placement delete"嗎?
destroy(p2,a2);
destroy(p3,a3);
如何在類繼承體系中定義配對的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++的類型轉換機制,只有包括在派生類中的基類部分被拷貝,其餘部分被“切割”掉了。]
如果爲了和“虛函數調用”說byebye,那麼確實有給類繼承體系“封頂”的需要。在設計前,不訪先問問自己,這些函數爲何要被設計成虛的。我確實見過這樣的例子:性能要求苛刻的函數被設計成虛的,僅僅因爲“我們習慣這樣做”!
好了,無論如何,說了那麼多,畢竟你只是想知道,爲了某種合理的理由,你能不能防止別人繼承你的類。答案是可以的。可惜,這裏給出的解決之道不夠乾淨利落。你不得不在在你的“封頂類”中虛擬繼承一個無法構造的輔助基類。還是讓例子來告訴我們一切吧:
Q: 爲什麼我無法限制模板的參數?
讓我們來看這段代碼:
爲了早點捕捉到這個錯誤,我們可以這樣寫代碼:
- 你可以不通過定義/拷貝變量就表達出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++帶給我們的強有力的表達能力。讓代碼來爲我的話作證吧:
Q: 我們已經有了 "美好的老qsort()",爲什麼還要用sort()?
另外,還有許多人欣賞sort()的類型安全性——要使用它可不需要任何強制的類型轉換。對於標準類型,也不必寫compare()函數,省事不少。如果想看更詳盡的解釋,參看我的《Learning Standard C++ as a New Language》一文。
另外,爲何sort()要比qsort()快?因爲它更好地利用了C++的內聯語法語義。
Q: 什麼是function object?
Function object的涵義比通常意義上的函數更廣泛,因爲它可以在多次調用之間保持某種“狀態”——這和靜態局部變量有異曲同工之妙;不過這種“狀態”還可以被初始化,還可以從外面來檢測,這可要比靜態局部變量強了。我們來看一個例子:
在標準庫中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: 我應該怎樣處理內存泄漏?
請注意這裏沒有顯式的內存管理代碼。沒有宏,沒有類型轉換,沒有溢出檢測,沒有強制的大小限制,也沒有指針。如果使用function object和標準算法[譯註:指標準庫中提供的泛型算法],我連Iterator也可以不用。不過這畢竟只是一個小程序,殺雞焉用牛刀?
當然,這些方法也並非無懈可擊,而且說起來容易做起來難,要系統地使用它們也並不總是很簡單。不過,無論如何,它們的廣泛適用性令人驚訝,而且通過移去大量的顯式內存分配/釋放代碼,它們確實增強了代碼的可讀性和可管理性。早在1981年,我就指出通過大幅度減少需要顯式加以管理的對象數量,使用C++“將事情做對”將不再是一件極其費神的艱鉅任務。
如果你的應用領域沒有能在內存管理方面助你一臂之力的類庫,那麼如果你還想讓你的軟件開發變得既快捷又能輕鬆得到正確結果,最好是先建立這樣一個庫。
如果你無法讓內存分配和釋放成爲對象的“自然行爲”,那麼至少你可以通過使用資源句柄來儘量避免內存泄漏。這裏是一個示例:假設你需要從函數返回一個對象,這個對象是在自由內存堆上分配的;你可能會忘記釋放那個對象——畢竟我們無法通過檢查指針來確定其指向的對象是否需要被釋放,我們也無法得知誰應該負責釋放它。那麼,就用資源句柄吧。比如,標準庫中的auto_ptr就可以幫助澄清:“釋放對象”責任究竟在誰。我們來看:
這裏只是內存資源管理的例子;至於其它類型的資源管理,可以如法炮製。
如果在你的開發環境中無法系統地使用這種方法(比方說,你使用了第三方提供的古董代碼,或者遠古“穴居人”參與了你的項目開發),那麼你在開發過程中可千萬要記住使用內存防漏檢測程序,或者乾脆使用垃圾收集器(Garbage Collector)。
Q: 爲何捕捉到異常後不能繼續執行後面的代碼呢?
嗯,從異常處理代碼返回到異常拋出處繼續執行後面的代碼的想法很好[譯註:現行異常機制的設計是:當異常被拋出和處理後,從處理代碼所在的那個catch塊往下執行],但主要問題在於——exception handler不可能知道爲了讓後面的代碼正常運行,需要做多少清除異常的工作[譯註:畢竟,當有異常發生,事情就有點不太對勁了,不是嗎;更何況收拾爛攤子永遠是件麻煩的事],所以,如果要讓“繼續執行”能夠正常工作,寫throw代碼的人和寫catch代碼的人必須對彼此的代碼都很熟悉,而這就帶來了複雜的相互依賴關係[譯註:既指開發人員之間的“相互依賴”,也指代碼間的相互依賴——緊耦合的代碼可不是好代碼哦 :O) ],會帶來很多麻煩的維護問題。
在我設計C++的異常處理機制的時候,我曾認真地考慮過這個問題;在C++標準化的過程中,這個問題也被詳細地討論過。(參見《The Design and Evolution of C++》中關於異常處理的章節)如果你想試試看在拋出異常之前能不能解決問題然後繼續往下執行,你可以先調用一個“檢查—恢復”函數,然後,如果還是不能解決問題,再把異常拋出。一個這樣的例子是new_handler。
Q: 爲何C++中沒有C中realloc()的對應物?
在C++中,處理內存重分配的較好辦法是使用標準庫中的容器,比如vector。[譯註:這些容器會自己管理需要的內存,在必要時會“增長尺寸”——進行重分配。]
Q: 我如何使用異常處理?
Q: 我如何從標準輸入中讀取string?
如果你需要一次讀一整行,可以這樣:
Q: 爲何C++不提供“finally”結構?
另外,請看看《The C++ Programming Language》附錄E中的資源管理例子。
Q: 那個auto_ptr是什麼東東啊?爲什麼沒有auto_array?
Auto_ptr是一個輕量級的類,沒有引入引用計數機制。如果你把一個auto_ptr(比如,ap1)賦給另一個auto_ptr(比如,ap2),那麼ap2將持有實際指針,而ap1將持有零指針。例如:
這裏,語義似乎是“轉移”,而非“拷貝”,這或許有點令人驚訝。特別要注意的是,不要把auto_ptr作爲標準容器的參數——標準容器要求通常的拷貝語義。例如:
一個auto_ptr只能持有指向單個元素的指針,而不是數組指針:
那麼,看來我們應該用一個使用delete[]來釋放指針的,叫auto_array的類似東東來放數組了?哦,不,不,沒有什麼auto_array。理由是,不需要有啊——我們完全可以用vector嘛:
Q: C和C++風格的內存分配/釋放可以混用嗎?
不可以——從你無法delete一個以malloc()分配而來之對象的意義上而言。你也無法free()或realloc()一個由new分配而來的對象。
C++的new和delete運算符確保構造和析構正常發生,但C風格的malloc()、calloc()、free()和realloc()可不保證這點。而且,沒有任何人能向你擔保,new/delete和malloc/free所掌控的內存是相互“兼容”的。如果在你的代碼中,兩種風格混用而沒有給你造成麻煩,那我只能說:直到目前爲止,你是非常幸運的 :O)
如果你因爲思念“美好的老realloc()”(許多人都思念她)而無法割捨整個古老的C內存分配機制(愛屋及烏?),那麼考慮使用標準庫中的vector吧。例如:
我的《Learning Standard C++ as a New Language》一文中給出了其它例子,可以參考。
Q: 我想從void *轉換,爲什麼必須使用換型符?
A: 在C中,你可以隱式轉換,但這是不安全的,例如:
在C中一類最常見的不安全換型發生在將malloc()分配而來的內存賦給某個指針之時,例如:
- new不會“偶然”地分配錯誤大小的內存
- new自動檢查內存是否已枯竭
- new支持初始化
A: 如何在類中定義常量?
Q: 如果你想得到一個可用於常量表達式中的常量,例如數組大小的定義,那麼你有兩種選擇:
那麼,爲何要有這些不方便的限制?因爲類通常聲明在頭文件中,而頭文件往往被許多單元所包含。[所以,類可能會被重複聲明。]但是,爲了避免鏈接器設計的複雜化,C++要求每個對象都只能被定義一次。如果C++允許類內定義要作爲對象被存在內存中的實體,那麼這項要求就無法滿足了。關於C++設計時的一些折衷,參見《The Design and Evolution of C++》。
如果這個常量不需要被用於常量表達式,那麼你的選擇餘地就比較大了:
Q: 爲何delete操作不把指針置零?
哦,不不,這個主意不夠“好”。一個理由是,被delete的指針未必是左值。我們來看:
如果你覺得釋放內存時把指針置零很重要,那麼不妨寫這樣一個destroy函數:
不妨把delete帶來的麻煩看作“儘量少用new/delete,多用標準庫中的容器”之另一條理由吧 :O)
請注意,把指針作爲引用傳遞(以便delete可以把指針置零)會帶來額外的效益——防止右值被傳遞給destroy() :
Q: 我可以寫"void main()"嗎?
Q: 爲何我不能重載“.”、“::”和“sizeof”等操作符?
而“sizeof”無法被重載是因爲不少內部操作,比如指針加法,都依賴於它,例如:
在N::m中,N和m都不是表達式,它們只是編譯器“認識”的名字,“::”執行的實際操作是編譯時的名字域解析,並沒有表達式的運算牽涉在內。或許有人會覺得重載一個“x::y”(其中x是實際對象,而非名字域或類名)是一個好主意,但這樣做引入了新的語法[譯註:重載的本意是讓操作符可以有新的語義,而不是更改語法——否則會引起混亂],我可不認爲新語法帶來的複雜性會給我們什麼好處。
原則上來說,“.”運算符是可以被重載的,就像“->”一樣。不過,這會帶來語義的混淆——我們到底是想和“.”後面的對象打交道呢,還是“.”後面的東東所實際指向的實體打交道呢?看看這個例子(它假設“.”重載是可以的):
這個問題有好幾種解決方案。在C++標準化之時,何種方案爲佳並不明顯。細節請參見《The Design and Evolution of C++》。
Q: 我怎樣才能把整數轉化爲字符串?
Q: “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*的差別並不是特別突出,但當我們要一次聲明多個變量時,易混淆之處就全暴露出來了:
每當達到某種目的有兩條以上途徑,就會有些人被搞糊塗;每當一些選擇是出於個人喜好,爭論就會無休無止。堅持一次只聲明一個指針並在聲明時順便初始化,困擾我們已久的混淆之源就會隨風逝去。如果你想了解有關C的聲明語法的更多討論,參見《The Design and Evolution of C++》 。
Q: 何種代碼佈局風格爲佳?
我個人喜歡使用“K&R”風格,如果算上那些C語言中不存在的構造之使用慣例,那麼人們有時也稱之爲“Stroustrup”風格。例如:
正確的縮進非常重要。
一些設計問題,比如使用抽象類來表示重要的界面、使用模板來表示靈活而可擴展的類型安全抽象、正確使用“異常”來表示錯誤,遠遠要比代碼風格重要。
[譯註:《The Practice of Programming》中有一章對“代碼風格”問題作了詳細的闡述。]
Q: 我該把const寫在類型前面還是後面?
爲什麼會這樣?當我發明“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: 宏有什麼不好嗎?
不幸的是,你無法確保其他程序員不犯你所認爲的“愚蠢的”錯誤。比方說,近來有人告訴我,他們遇到一個含“goto”語句的宏。我見到過這樣的代碼,也聽到過這樣的論點——有時宏中的“goto”是有用的。例如:
一個常見而微妙的問題是,函數風格的宏不遵守函數參數調用規則。例如:
我知道有些(其它語言中)被稱爲“宏”的東西並不象C/C++預處理器所處理的“宏”那樣缺陷多多、麻煩重重,但我並不想改進C++的宏,而是建議你正確使用C++語言中的其他機制,比如內聯函數、模板、構造函數、析構函數、異常處理等。