C++痛恨者手冊(有意思:)

來源:互聯網
上載者:User
文章目錄
  • 文法的吐根糖漿(Syrup of Ipecac,一種毒藥)
  • 抽象些什嗎?
  • C++對於C來說,就如同是肺癌對於肺
  •  
  • 程式員進化史
C++
90年代的COBOL

---------------------------------------------------------------------------
---
問:"C"和"C++"的名字是怎麼來的?
答:這是他們的成績

——Jerry Leichter

再沒有比C++更能體現Unix“絕不給使用者好臉”的哲學思想的了。

物件導向編程可以追溯到60年代的Simula語言,在70年代的Smalltalk語言上得
到極大發展。許多書會告訴你物件導向語言如何能提高編程效率,使代碼更健壯,
和減少維護費用。不過你甭想在C++裡得到這些。

這是因為C++根本就沒理解物件導向的實質。非但沒有簡化什麼,反而增加了更
多的複雜性。和Unix一樣,C++從沒被好好設計過,它從一個錯誤走向另一個錯
誤,是件打滿補丁的破衣服。連自己的文法都沒有嚴格定義(沒一個語言敢這樣),
所以你甚至無法知道一行代碼是不是合法。

把C++比做COBOL,其實是對COBOL的汙辱。COBOL在那個時代的技術條件下,是做
出了很不同凡響的貢獻的。如果有誰用C++做成過什麼事,那就算是很不同凡響
了。幸運的是,很多不錯的程式員知道必須盡量避免C++的傷害,他們只用C,對
那些荒唐費解的功能敬而遠之。通常,這意味著他們必須自己寫個非物件導向的
工具,以獲得自己想要的功能。當然,他們的代碼會顯得極為怪異,失去相容性,
難於理解和重用。不過只要有一點兒C++的味道,就足夠說服頭頭批准他們的項
目。

許多公司已經被多年遺留下來的混亂難懂的COBOL代碼搞得焦頭爛額了。那些轉
而使用C++的公司剛剛意識到自己上了當。當然,這已經太晚了。軟體災難的種
子已經播下了,澆水施肥,得到悉心照料,就等著十幾年後長成參天大樹了。等
著瞧吧!

物件導向的組合語言

C++沒有一絲一毫高層次語言的特性。為什麼這麼說?讓我們看看高層次語言應
該具備那些特性:

 

優雅:在表示方式及其所表達的概念之間有著簡單易懂的關係

抽象:高層次語言的每個運算式只表示一個概念。概念能夠被獨立表達?br>⒛蘢雜勺
楹稀?

強大:高層次語言的能夠對任何精確完整的程式行為進行提供直接了當?br>拿枋齜絞健?

高層次語言使程式員能夠採用問題空間的方式描述解決方案。高層次的程式很容
易維護,因為它們的目的性(intent)十分明確。根據一行高層次程式碼,現代
編譯器能夠為各種平台產生高效的代碼,所以高層次程式的可移植性和可重用性
自然會很強。

使用低層次語言則需要對考慮無數細節,其中大部分是和機器內部操作有關的東
西,而不是要解決的問題本身。這不但造成代碼難於理解,而且很容易過時。現
在幾乎每隔今年就要更新系統,過時的必須花費很高代價修改低層代碼或者徹底
重寫。

對不起,你的記憶體流失了

高層次語言對於常見問題有內建解決方案。例如,眾所周知記憶體管理是產生錯誤
最多的地方。在使用一個對象之前,你必須為它分配記憶體,適當進行初始化,小
心跟蹤使用,並正確釋放。當然,每件事兒都異常乏味而且很容易出錯,極小的
一個錯誤可能會導致災難性後果。定位和修改這類錯誤是臭名昭著的困難,因為
它們對於配置或使用方式的變化極其敏感。

使用未分配記憶體的結構指標會造成程式崩潰。使用未正確初始化的結構也會使你
的程式崩潰,不過不一定立刻完蛋。如果未能好好跟蹤結構的使用方式,則很可
能釋放一塊還在使用中的記憶體。還是崩潰。最好再分配一些結構用來跟蹤那些結
構對象。不過如果你太保守,不去釋放那些不很肯定未在使用的記憶體,那麼你可
要小心了。記憶體中很快就會充斥著無用的對象,直到記憶體耗盡,程式崩潰。這就
是恐怖的“記憶體流失”。

如果你的記憶體空間片段太多,那該怎麼辦呢?解決辦法是通過移動對象對記憶體重
新歸整,不過在C++裡沒戲——如果你忘了更新對象的所有引用(reference),那
麼你就會搞亂程式,結果還是崩潰。

很多真正的高層次語言提供瞭解決辦法——那就是記憶體回收(garbage
collector)。它記錄跟蹤所有的對象,如果對象用完了會加以回收,永遠不會出
錯。如果你使用有記憶體回收功能的語言,會得到不少好處:

 

大量的bug立馬無影無蹤。是不是很爽呀?

代碼會變得更短小更易讀,因為它不必為記憶體管理的細節操心。

代碼更有可能在不同平台和不同配置下高效運行。

 

唉,C++使用者必須自己動手去揀垃圾。他們中的許多人被洗了腦子,認為這樣會
比那些專家提供的記憶體回收工具更為高效,如果要建立磁碟檔案,他們估計不會
使用檔案名稱,而更願意和磁軌扇區打交道。手動回收垃圾可能會對一兩中配置顯
得更高效些,不過你當然不會這麼去使用文書處理軟體。

你不必相信我們這裡說的。可以去讀一下B. Zorn的《保守記憶體回收的代價測量》
(科羅拉多大學Boulder分校,技術報告CU-CS-573-92),文中對程式員用C手動優
化的記憶體回收技術和標準記憶體回收行程進行了效能比較,結果表明C程式員自己寫
的記憶體回收行程效能要差一些。

OK,假設你是個幡然醒悟的C++程式員,想使用記憶體回收。你並不孤單,很多人
認為這是個好主意,決定寫一個。老天爺,猜猜怎麼著?你會發現根本沒法在
C++中提供其他語言內建的那樣好的記憶體回收。其中一個原因是,(驚訝!)C++裡
的對象在編譯後和運行時就不再是對象了。它們只是一塊十六進位的爛泥巴。沒
有動態類型資訊——記憶體回收行程(還有調試器)沒法知道任何一塊記憶體裡的對象究
竟是什麼,類型是什麼,以及是否有人此時正在使用它。

另一個原因是,即使你能寫個記憶體回收行程,如果你用了其他未使用垃圾
回收功能的代碼,你還是會被幹掉。因為C++沒有標準的記憶體回收行程,而且很有
可能永遠也不會有。假設我寫了一個使用了我的記憶體回收功能的資料庫程式,你
寫了一個使用你自己的記憶體回收功能的視窗系統。但你關閉一個裝有我的資料記
錄的視窗,你的視窗不會去通知我的資料記錄,告訴它已經沒有人引用它了。這
個對象將不會被釋放,直到記憶體耗盡——記憶體流失,老朋友又見面了。

學起來困難?這就對了

C++和組合語言很相象——難學難用,要想學好用好就更難了。

日期: Mon, 8 Apr 91 11:29:56 PDT
發信人: Daniel Weise
收信人: UNIX-HATERS
主題: From their cradle to our grave (從他們的搖籃到我們的墳墓)

造成Unix程式如此脆弱的一個原因是C程式員從啟蒙時期就是這麼被教
育的。例如,Stroustrup(C++之父)的《C++程式設計語言》第一個完整
程式(就是那個300K大小的"hello world"程式之後的那個)是一個英制/
公制轉換程式。使用者用結尾"i"表示英制輸入,用結尾"c"表示公制輸入。
下面是這個程式的概要,是用真正的Unix/C風格寫的:

#include

main() {
[變數聲明]
cin >> x >> ch;
;; A design abortion.
;; 讀入x,然後讀入ch。

if (ch == 'i') [handle "i" case]
else if (ch == 'c') [handle "c" case]
else in = cm = 0;
;; 好樣的,決不報告錯誤。
;; 隨便做點兒什麼就成了。

[進行轉換]

往後翻13頁(第31頁),給了一個索引範圍從n到m的數組(而不是從0到
m)的實現例子。如果程式員使用了超出範圍的索引,這個程式只是笑
嬉嬉地返回數組的第一個元素。Unix的終極腦死亡。

文法的吐根糖漿(Syrup of Ipecac,一種毒藥)

文法糖蜜(Syntactic sugar)是分號癌症的罪魁禍首。
——Alan Perlis

在使用C程式設計語言中所能遇到的所有語法錯誤幾乎都能被C++接受,成功編譯。不
幸的是,這些語法錯誤並不總能產生正確的代碼,這是因為人不是完美
的,他們總是敲錯鍵盤。C一般總能在編譯是發現這些錯誤。C++則不然,它讓你
順利通過編譯,不過如果真的運行起來,就等著頭痛吧。

C++的文法形成也和它自身的發展密不可分。C++從來沒有被好好設計過:它只是
逐步進化。在進化過程中,一些結構的加入造成了語言的二義性。特別
的規則被用於解決這些二義性,這些難懂的規則使得C++複雜難學。於是不少程
序員把它們抄在卡片上以供不時之需,或者根本就不去使用這些功能。

例如,C++有個規則說如果一個字串既可以被解析為聲明也可以被解析為語句,
那麼它將被當做聲明。解析器專家看到這個規則往往會渾身發冷,他們知道很難
能正確實現它。AT&T自己甚至都搞不對。比如,當Jim Roskind想理解一個結構
的意思時(他覺得正常人會對它有不同的理解),他寫了段測試代碼,把它交給
AT&T的"cfront"編譯器。Cfront崩潰了。

事實上,如果你從ics.uci.edu上下載Jim Roskind的開放C++文法,你會發現
ftp/pub目錄裡的c++grammar2.0.tar.Z有這樣的說明:“注意我
的文法和cfront不一定保持一致,因為 a) 我的文法內部是一致的(這源於它的
規範性以及yacc的確證。b) yacc產生的解析器不會吐核(core
dump)。(這條可能會招來不少臭雞蛋,不過...每次當我想知道某種結構的文法
含義是,如果ARM(Annotated C++ Reference Manual, 帶注釋的C++參考手冊)對
它的表述不清楚,我就會拿cfront來編譯它,cfront這時總是吐核(core dump))”

日期: Sun, 21 May 89 18:02:14 PDT
發信人: tiemann (Michael Tiemann)
收信人: sdm@cs.brown.edu
抄送: UNIX-HATERS
主題: C++ Comments (C++注釋)

日期: 21 May 89 23:59:37 GMT
發信人: sdm@cs.brown.edu (Scott Meyers)
新聞群組: comp.lang.c++
組織: 布朗大學電腦系

看看下面這行C++代碼:

//********************** color=green>

C++編譯器該如何處理它呢?GNU g++編譯器認為這是一行由一堆星星
(*)組成的注釋,然而AT&T編譯器認為這是一個斜杠加上一個注釋開始
符(/*)。我想知道哪個是正確解析方式,可是Stroustrup的書(《C++編
程語言》)裡面卻找不到答案。

實際上如果使用-E選項進行編譯,就會發現是前置處理器(preprocessor)
搞的鬼,我的問題是:

 

這是否AT&T前置處理器的bug?如果不是,為什嗎?如果是bug,2.0版是否會得到修
正?還是只能這麼下去了?

這是否GNU前置處理器的bug?如果是,為什嗎?

Scott Meyers
sdm@cs.brown.edu

UNIX解析中有個古老的規則,盡量接受最長的文法單元(token)。這樣'
foo'就不會被看成三個變數名('f', 'o'和'o'),而只被當成一個變數'
foo'。看看這個規則在下面這個程式中是多麼的有用(還有選擇'/*'作
為注釋開始符是多麼的明智):

 

double qdiv (p, q)
double *p, *q;
{
return *p/*q;
}

為什麼這個規則沒有被應用到C++中呢?很簡單,這是個bug。

Michael

糟糕的還在後頭,C++最大的問題是它的代碼難讀難理解,即使對於每天都用它
的人也是如此。把另一個程式員的C++的代碼拿來看看,不暈才怪。C++沒有一絲
品位,是個亂七八糟的醜八怪。C++自稱為物件導向語言,卻不願意承擔任何面
向對象的責任。C++認為如果有誰的程式複雜到需要記憶體回收,動態載入或其他
功能,那麼說明他有足夠的能力自己寫一個,並且有足夠的時間進行調試。

C++操作符重載(operator overloading)的強大功能在於,你可以把一段明顯直
白的代碼變成能和最糟糕的APL, ADA或FORTH代碼相媲美的東西。每個C++程式員
都能建立自己的方言(dialect),把別的C++程式員徹底搞暈。

不過——嘿——在C++裡甚至標準的方言也是私人的(private)。

抽象些什嗎?

你可能會覺得C++文法是它最糟糕的部分,不過當你開始學習C++時,就會知道你
錯了。一旦你開始用C++編寫一個正式的大型軟體,你會發現C++的抽象機制從根
兒上就爛了。每本電腦科學教材都會這樣告訴你,抽象是良好設計之源。

系統各個部分的關聯會產生複雜性。如果你有一個100,000行的程式,其中每一
行都和其他行代碼的細節相關,那你就必須照應著10,000,000,000種不同的關聯。
抽象能夠通過建立清晰的介面來減少這種關聯。一段實現某種功能的代碼被隱藏
在模組化牆壁之後發揮作用。

類(class)是C++的核心,然而類的實現卻反而阻礙著程式的模組化。類暴露了如
此多的內部實現,以至於類的使用者強烈倚賴著類的具體實現。許多情況下,對類
做一點兒改變,就不得不重新編譯所有使用它的代碼,這常常造成開發的停滯。
你的軟體將不再“柔軟”和“可塑”了,而成了一大塊混凝土。

你將不得不把一半代碼放到標頭檔裡面,以對類進行聲明。當然,類聲明所提供
的public/private的區分是沒有什麼用的,因為“私人”(private)資訊就放在
了標頭檔裡,所以成了公開(public)資訊。一旦放到標頭檔裡,你就不大願意去
修改它,因為這會導致煩人的重編譯。程式員於是通過修補實現機制,以避免修
改標頭檔。當然還有其他一些保護機制,不過它們就象是減速障礙一樣,可以被
心急的傢伙任意繞過。只要把所有對象都轉換(cast)成void*,再也沒有
了討厭的類型檢查,這下世界清淨了。

其他許多語言都各自提供了設計良好的抽象機制。C++丟掉了其中一些最為重要
的部分,對於那些提供的部分也叫人迷惑不解。你是否遇到過真正喜歡模板
(template)的人?模板使得類的實現根據上下文不同而不同。許多重要的概念無
法通過這種簡單的方式加以表達;即使表達出來了,也沒法給它一個直接的名字
供以後調用。

例如,名空間(namespace)能夠避免你一部分代碼的名字和其他部分發生衝突。
一個服裝製造軟體可能有個對象叫做"Button"(鈕扣),它可能會
和一個使用者介面庫進行連結,那裡面也有個類叫做"Button"(按鈕
)。如果使用了名空間,就不會有問題了,因為用法和每個概念的意思都很
明確,沒有歧義。

C++裡則並非如此。你無法保證不會去使用那些已經在其他地方被定義了的名字,
這往往會導致災難性後果。你唯一的希望是給名稱都加上首碼,比如
ZjxButton,並但願其他人不會用同一個名字。

日期: Fri, 18 Mar 94 10:52:58 PST
發信人: Scott L. Burson
主題: preprocessor (前置處理器)

C語言迷們會告訴你C的一個最好的功能是前置處理器。可事實上,它可能
一個最蹩腳的功能。許多C程式由一堆蜘蛛網似的#ifdef組成 (如果各
個Unix之間能夠互相相容,就幾乎不會弄成這樣)。不過這僅僅是開始。

C前置處理器的最大問題是它把Unix鎖在了文字檔的監牢裡,然後扔掉
了牢門鑰匙。這樣除了文字檔以外,C原始碼不可能以任何其他方式
儲存。為什嗎?因為未被預先處理的C代碼不可能被解析。例如:

#ifdef BSD
int foo() {
#else
void foo() {
#endif
/* ... */
}

這裡函數foo有兩種不同的開頭,根據宏'BSD'是否被定義而不同。直接
對它進行解析幾乎是不可能的 (就我們所知,從來沒實現過)。

這為什麼如此可惡?因為這阻礙了我們為編程環境加入更多智能。許多
Unix程式員從沒見過這樣的環境,不知道自己被剝奪了什麼。可是如果
能夠對代碼進行自動分析,那麼就能提供很多非常有用的功能。

讓我們再看一個例子。在C語言當道的時代,前置處理器被認為是唯一能
提供開碼(open-coded,是指直接把代碼嵌入到指令流中,而不是通過
函數調用)的方式。對於每個簡單常用的運算式,開碼是一個很高效的
技術。比如,取小函數min可以使用宏實現:

#define main(x,y) ((x) < (y) ? (x) : (y))color=green>

假設你想寫個工具列印一個程式中所有調用了min的函數。聽上去不是
很難,是不是?但是你如果不解析這個程式就無法知道函數的邊界,你
如果不做經過前置處理器就無法進行解析,可是,一旦經過了預先處理,所
有的min就不複存在了!所以,你的只能去用grep了。

使用前置處理器實現開碼還有其他問題。例如,在上面的min宏裡你一定
注意到了那些多餘的括弧。事實上,這些括弧是必不可少的,否則當
min在另一個運算式中被展開時,結果可能不是你想要的。(老實說,這
些括弧不都是必需的——至於那些括弧是可以省略的,這留做給讀者的
練習吧)。

min宏最險惡的問題是,雖然它用起來象是個函數調用,它並不真是函
數。看這個例子:

a = min(b++, c);

前置處理器做了替換之後,變成了:

a = ((b++) < (c) ? (b++) : (c)color=green>

如果'b'小於'c','b'會被增加兩次而不是一次,返回的將
是'b'的原始值加一。

如果min真是函數,那麼'b'將只會被增加一次,傳回值將是'b'的原
始值。

 

C++對於C來說,就如同是肺癌對於肺

“如果說C語言給了你足夠的繩子弔死自己,那麼C++給的繩子除了夠你上
吊之外,還夠綁上你所有的鄰居,並提供一艘帆船所需的繩索。”
——匿名

悲哀的是,學習C++成了每個電腦科學家和嚴肅程式最為有利可圖的投資。它
迅速成為簡曆中必不可少的一行。在過去的今年中,我們見過不少C++程式員,
他們能夠用C++寫出不錯的代碼,不過...

....他們憎惡它。

 程式員進化史

初中/高中

10 PRINT "HELLO WORLD"
20 END

大學一年級

program Hello(input, output);
begin
writeln('Hello world');
end.

大學四年級

(defun hello ()
(print (list 'HELLO 'WORLD)))

剛參加工作

#include
main (argc, argv)
int argc;
char **argv; {
printf ("Hello World!\n");
}

老手

#include

const int MAXLEN = 80;

class outstring;
class outstring {
private:

int size;
char str[MAXLEN];

public:
outstring() { size=0; }
~outstring() { size=0; }
void print();
void assign(char *chrs);
};
void outstring::print() {
int in;
for (i=0; i cout << str[i];
cout << "\n";
}
void outstring::assign(char* chrs) {
int i;
for (i=0; chars[i]!='\0'; i++)
str[i] = chrs[i];
size=i;
}

main (int argc, char **argv) {
outstring string;
string.assign("Hello World!");
string.print();
}

老闆

“喬治,我需要一個能列印'Hello World!'的程式”

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.