版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)
文檔簡介
C++沉思錄(中文版)注:因內(nèi)容過長上傳受限制,本文檔只顯示第一篇內(nèi)容,完整版文檔請下載此文檔后留言謝謝。目錄\h第0章序幕\h0.1第一次嘗試\h0.1.1改進(jìn)\h0.1.2另一種改進(jìn)\h0.2不用類來實現(xiàn)\h0.3為什么用C++更簡單\h0.4一個更大的例子\h0.5結(jié)論\h第一篇動機(jī)\h第1章為什么我用C++\h1.1問題\h1.2歷史背景\h1.3自動軟件發(fā)布\h1.3.1可靠性與通用性\h1.3.2為什么用C\h1.3.3應(yīng)付快速增長\h1.4進(jìn)入C++\h1.5重復(fù)利用的軟件\h1.6后記\h第2章為什么用C++工作\h2.1小項目的成功\h2.1.1開銷\h2.1.2質(zhì)疑軟件工廠\h2.2抽象\h2.2.1有些抽象不是語言的一部分\h2.2.2抽象和規(guī)范\h2.2.3抽象和內(nèi)存管理\h2.3機(jī)器應(yīng)該為人服務(wù)\h第3章生活在現(xiàn)實世界中\(zhòng)h第二篇類和繼承\(zhòng)h第4章類設(shè)計者的核查表\h第5章代理類\h5.1問題\h5.2經(jīng)典解決方案\h5.3虛復(fù)制函數(shù)\h5.4定義代理類\h5.5小結(jié)\h第6章句柄:第一部分\h6.1問題\h6.2一個簡單的類\h6.3綁定到句柄\h6.4獲取對象\h6.5簡單的實現(xiàn)\h6.6引用計數(shù)型句柄\h6.7寫時復(fù)制\h6.8討論\h第7章句柄:第二部分\h7.1回顧\h7.2分離引用計數(shù)\h7.3對引用計數(shù)的抽象\h7.4存取函數(shù)和寫時復(fù)制\h7.5討論\h第8章一個面向?qū)ο蟪绦蚍独齖h8.1問題描述\h8.2面向?qū)ο蟮慕鉀Q方案\h8.3句柄類\h8.4擴(kuò)展1:新操作\h8.5擴(kuò)展2:增加新的節(jié)點類型\h8.6反思\h第9章一個課堂練習(xí)的分析(上)\h9.1問題描述\h9.2接口設(shè)計\h9.3補(bǔ)遺\h9.4測試接口\h9.5策略\h9.6方案\h9.7圖像的組合\h9.8結(jié)論\h第10章一個課堂練習(xí)的分析(下)\h10.1策略\h10.1.1方案\h10.1.2內(nèi)存分配\h10.1.3結(jié)構(gòu)構(gòu)造\h10.1.4顯示圖像\h10.2體驗設(shè)計的靈活性\h10.3結(jié)論\h第11章什么時候不應(yīng)當(dāng)使用虛函數(shù)\h11.1適用的情況\h11.2不適用的情況\h11.2.1效率\h11.2.2你想要什么樣的行為\h11.2.3不是所有的類都是通用的\h11.3析構(gòu)函數(shù)很特殊\h11.4小結(jié)\h第三篇模板\h第12章設(shè)計容器類\h12.1包含什么\h12.2復(fù)制容器意味著什么\h12.3怎樣獲取容器的元素\h12.4怎樣區(qū)分讀和寫\h12.5怎樣處理容器的增長\h12.6容器支持哪些操作\h12.7怎樣設(shè)想容器元素的類型\h12.8容器和繼承\(zhòng)h12.9設(shè)計一個類似數(shù)組的類\h第13章訪問容器中的元素\h13.1模擬指針\h13.2獲取數(shù)據(jù)\h13.3遺留問題\h13.4指向constArray的Pointer\h13.5有用的增強(qiáng)操作\h第14章迭代器\h14.1完成Pointer類\h14.2什么是迭代器\h14.3刪除元素\h14.4刪除容器\h14.5其他設(shè)計考慮\h14.6討論\h第15章序列\(zhòng)h15.1技術(shù)狀況\h15.2基本的傳統(tǒng)觀點\h15.3增加一些額外操作\h15.4使用范例\h15.5再增加一些\h15.6請你思考\h第16章作為接口的模板\h16.1問題\h16.2第一個例子\h16.3分離迭代方式\h16.4遍歷任意類型\h16.5增加其他類型\h16.6將存儲技術(shù)抽象化\h16.7實證\h16.8小結(jié)\h第17章模板和泛型算法\h17.1一個特例\h17.2泛型化元素類型\h17.3推遲計數(shù)\h17.4地址獨立性\h17.5查找非數(shù)組\h17.6討論\h第18章泛型迭代器\h18.1一個不同的算法\h18.2需求的分類\h18.3輸入迭代器\h18.4輸出迭代器\h18.5前向迭代器\h18.6雙向迭代器\h18.7隨機(jī)存取迭代器\h18.8是繼承嗎\h18.9性能\h18.10小結(jié)\h第19章使用泛型迭代器\h19.1迭代器類型\h19.2虛擬序列\(zhòng)h19.3輸出流迭代器\h19.4輸入流迭代器\h19.5討論\h第20章迭代器配接器\h20.1一個例子\h20.2方向不對稱性\h20.3一致性和不對稱性\h20.4自動反向\h20.5討論\h第21章函數(shù)對象\h21.1一個例子\h21.2函數(shù)指針\h21.3函數(shù)對象\h21.4函數(shù)對象模板\h21.5隱藏中間類型\h21.6一種類型包羅萬象\h21.7實現(xiàn)\h21.8討論\h第22章函數(shù)配接器\h22.1為什么是函數(shù)對象\h22.2用于內(nèi)建操作符的函數(shù)對象\h22.3綁定者(Binders)\h22.4更深入地探討\h22.5接口繼承\(zhòng)h22.6使用這些類\h22.7討論\h第四篇庫\h第23章日常使用的庫\h23.1問題\h23.2理解問題:第1部分\h23.3實現(xiàn):第1部分\h23.4理解問題:第2部分\h23.5實現(xiàn):第2部分\h23.6討論\h第24章一個庫接口設(shè)計實例\h24.1復(fù)雜問題\h24.2優(yōu)化接口\h24.3溫故知新\h24.4編寫代碼\h24.5結(jié)論\h第25章庫設(shè)計就是語言設(shè)計\h25.1字符串\h25.2內(nèi)存耗盡\h25.3復(fù)制\h25.4隱藏實現(xiàn)\h25.5缺省構(gòu)造函數(shù)\h25.6其他操作\h25.7子字符串\h25.8結(jié)論\h第26章語言設(shè)計就是庫設(shè)計\h26.1抽象數(shù)據(jù)類型\h26.1.1構(gòu)造函數(shù)與析構(gòu)函數(shù)\h26.1.2成員函數(shù)及可見度控制\h26.2庫和抽象數(shù)據(jù)類型\h26.2.1類型安全的鏈接(linkage)\h26.2.2命名空間\h26.3內(nèi)存分配\h26.4按成員賦值(memberwiiseassignment)和初始化\h26.5異常處理\h26.6小結(jié)\h第五篇技術(shù)\h第27章自己跟蹤自己的類\h27.1設(shè)計一個跟蹤類\h27.2創(chuàng)建死代碼\h27.3生成對象的審計跟蹤\h27.4驗證容器行為\h27.5小結(jié)\h第28章在簇中分配對象\h28.1問題\h28.2設(shè)計方案\h28.3實現(xiàn)\h28.4加入繼承\(zhòng)h28.5小結(jié)\h第29章應(yīng)用器、操縱器和函數(shù)對象\h29.1問題\h29.2一種解決方案\h29.3另一種不同的解決方案\h29.4多個參數(shù)\h29.5一個例子\h29.6簡化\h29.7思考\h29.8歷史記錄、參考資料和致謝\h第30章將應(yīng)用程序庫從輸入輸出中分離出來\h30.1問題\h30.2解決方案1:技巧加蠻力\h30.3解決方案2:抽象輸出\h30.4解決方案3:技巧而無蠻力\h30.5評論\h第六篇總結(jié)\h第31章通過復(fù)雜性獲取簡單性\h31.1世界是復(fù)雜的\h31.2復(fù)雜性變得隱蔽\h31.3計算機(jī)也是一樣\h31.4計算機(jī)解決實際問題\h31.5類庫和語言語義\h31.6很難使事情變得容易\h31.7抽象和接口\h31.8復(fù)雜度的守恒\h第32章說了Helloworld后再做什么\h32.1找當(dāng)?shù)氐膶<襖h32.2選一種工具包并適應(yīng)它\h32.3C的某些部分是必需的\h32.4C的其他部分不是必需的\h32.5給自己設(shè)一些問題\h32.6結(jié)論\h附錄Koenig和Moo夫婦訪談\h索引\h第0章序幕有一次,我遇到一個人,他曾經(jīng)用各種語言寫過程序,唯獨沒用過C和C++。他提了一個問題:“你能說服我去學(xué)習(xí)C++,而不是C嗎?”,這個問題還真讓我想了一會兒。我給許許多多人講過C++,可是突然間我發(fā)現(xiàn)他們?nèi)际荂程序員出身。到底該如何向從沒用過C的人解釋C++呢?于是,我首先問他使用過什么與C相近的語言。他曾用Ada\h[1]編寫過大量程序——但這對我毫無用處,我不了解Ada。還好他知道Pascal,我也知道。于是我打算在我們兩個之間有限的共通點之上找到一個例子。下面看看我是如何向他解釋什么事情是C++可以做好而C做不好的。\h0.1第一次嘗試C++的核心概念就是類,所以我一開始就定義了一個類。我想寫一個完整的類定義,它要盡量小,要足夠說明問題,而且要有用。另外,我還想在例子中展示數(shù)據(jù)隱藏(datahiding),因此希望它有公有數(shù)據(jù)(publicdata)和私有數(shù)據(jù)(privatedata)。經(jīng)過幾分鐘的思索,我寫下這樣的代碼:#include<stdio.h>classTrace{public:voidprint(char*s){printf("%s",s);}};我解釋了這段代碼是如何定義一個名叫Trace的新類,以及如何用Trace對象來打印輸出消息:intmain(){Tracet;t.print("beginmain()\n");//main函數(shù)的主體t.print("endmain()\n");}到目前為止,我所做的一切都和其他語言很相似。實際上,即使是C++,直接使用printf也是很不錯的,這種先定義類,然后創(chuàng)建類的對象,再來打印這些消息的方法,簡直舍近求遠(yuǎn)。然而,當(dāng)我繼續(xù)解釋類Trace定義的工作方式時,我意識到,即便是如此簡單的例子,也已經(jīng)觸及到某些重要的因素,正是這些因素使得C++如此強(qiáng)大而靈活。\h0.1.1改進(jìn)例如,一旦我開始使用Trace類,就會發(fā)現(xiàn),如果能夠在必要時關(guān)閉跟蹤輸出(traceoutput),這將會是個有用的功能。小意思,只要改一下類的定義就行:#include<stdio.h>classTrace{public:Trace(){noisy=0;}voidprint(char*s){if(noisy)printf("%s",s);}voidon(){noisy=1;}voidoff(){noisy=0;}private:intnoisy;};此時類定義包括了兩個公有成員函數(shù)on和off,它們影響私有成員noisy的狀態(tài)。只有noisy為on(非零)才可以輸出。因此,t.off();會關(guān)閉t的對外輸出,直到我們通過下面的語句恢復(fù)t的輸出能力:t.on();我還指出,由于這些成員函數(shù)定義在Trace類自身的定義內(nèi),C++會內(nèi)聯(lián)(inline)擴(kuò)展它們,所以就使得即使在不進(jìn)行跟蹤的情況下,在程序中保留Trace對象也不必付出許多代價。我立刻想到,只要讓print函數(shù)不做任何事情,然后重新編譯程序,就可以有效地關(guān)閉所有Trace對象的輸出。\h0.1.2另一種改進(jìn)當(dāng)我問自己“如果用戶想要修改這樣的類,將會如何?”時,我獲得了更深層的理解。用戶總是要求修改程序。通常,這些修改是一般性的,例如“你能讓它隨時關(guān)閉嗎?”或者“你能讓它打印到標(biāo)準(zhǔn)輸出設(shè)備以外的東西上嗎?”我剛才已經(jīng)回答了第一個問題。接下來著手解決第二個問題,后來證明這個問題在C++里可以輕而易舉地解決,而在C里卻得大動干戈。我當(dāng)然可以通過繼承來創(chuàng)建一種新的Trace類。但是,我還是決定盡量讓示例簡單,避免介紹新的概念。所以,我修改了Trace類,用一個私有數(shù)據(jù)來存儲輸出文件的標(biāo)識,并提供了構(gòu)造函數(shù),讓用戶指定輸出文件:#include<stdio.h>classTrace{public:Trace(){noisy=0;f=stdout;}Trace(FILE*ff){noisy=0;f=ff;}voidprint(char*s){if(noisy)fprintf(f,"%s",s);}voidon(){noisy=1;}voidoff(){noisy=0;}private:intnoisy;FILE*f;};這樣改動,基于一個事實:printf(args);等價于:fprintf(stdout,args);創(chuàng)建一個沒有特殊要求的Trace類,則其對象的成員f為stdout。因此,調(diào)用fprintf所做的工作與調(diào)用前一個版本的printf是一樣的。類Trace有兩個構(gòu)造函數(shù):一個是無參構(gòu)造函數(shù),跟上例一樣輸出到stdout;另一個構(gòu)造函數(shù)允許明確指定輸出文件。因此,上面那個使用了Trace類的示例程序可以繼續(xù)工作,但也可以將輸出定向到比如說stderr上:intmain(){Tracet(stderr);t.print("beginmain()\n");//main函數(shù)的主體t.print("endmain()\n");}簡而言之,我運用C++類的特殊方式,使得對程序的改進(jìn)變得輕而易舉,而且不會影響使用這些類的代碼。\h0.2不用類來實現(xiàn)此時,我又開始想,對于這個問題,典型的C解決方案會是怎樣的。它可能會從一個類似于函數(shù)trace()(而不是類)的東西開始:#include<stdio.h>voidtrace(char*s){printf("%s\n",s);}它還可能允許我以如下形式控制輸出:#include<stdio.h>staticintnoisy=1;voidtrace(char*s){if(noisy)printf("%s\n",s);}voidtrace_on(){noisy=1;}voidtrace_off(){noisy=0;}這個方法是有效的,但與C++方法比較起來有3個明顯的缺點。首先,函數(shù)trace不是內(nèi)聯(lián)的,因此即使當(dāng)跟蹤關(guān)閉時,它還保持著函數(shù)調(diào)用的開銷\h[2]。在很多C的實現(xiàn)中,這個額外負(fù)擔(dān)都是無法避免的。第二,C版本引入了3個全局名字:trace、trace_on和trace_off,而C++只引入了1個。第三,也是最重要的一點,我們很難將這個例子一般化,使之能輸出到一個以上的文件中。為什么呢?考慮一下我們會怎樣使用這個trace函數(shù):intmain(){trace("beginmain()\n");//main函數(shù)主體trace("endmain()\n");}采用C++,可以只在創(chuàng)建Trace對象時一次性指定文件名。而在C版本中,情況相反,沒有合適的位置指定文件名。一個顯而易見的辦法就是給函數(shù)trace增加一個參數(shù),但是需要找到所有對trace函數(shù)的調(diào)用,并插入這個新增的參數(shù)。另一種辦法是引入名為trace_out的第4個函數(shù),用來將跟蹤輸出轉(zhuǎn)向到其他文件。這當(dāng)然也得要求判斷和記錄跟蹤輸出是打開還是關(guān)閉??紤]一下,譬如,main調(diào)用的一個函數(shù)恰好利用了trace_out向另一個文件輸出,則何時切換輸出的開關(guān)狀態(tài)呢?顯然,要想使結(jié)果正確需要花費相當(dāng)?shù)木?。\h0.3為什么用C++更簡單為什么在C方案中進(jìn)行擴(kuò)展會如此困難呢?難就難在沒有一個合適的位置來存儲輔助的狀態(tài)信息——在本例中是文件名和“noisy”標(biāo)記。在這里,這個問題尤其讓人惱火,因為在原來的情況下根本就不需要狀態(tài)信息,只是到后來才知道需要存儲狀態(tài)。往原本沒有考慮存儲狀態(tài)信息的設(shè)計中添加這項能力是很難的。在C中,最常見的做法就是找個地方把它藏起來,就像我這里采用“noisy”標(biāo)記一樣。但是這種技術(shù)也只能做到這樣;如果同時出現(xiàn)多個輸出文件來攪局,就很難有效控制了。C++版本則更簡單,因為C++鼓勵采用類來表示類似于輸出流的事物,而類就提供了一個理想的位置來放置狀態(tài)信息。結(jié)果是,C傾向于不存儲狀態(tài)信息,除非事先已經(jīng)規(guī)劃妥當(dāng)。因此,C程序員趨向于假設(shè)有這樣一個“環(huán)境”:存在一個位置集合,他們可以在其中找到系統(tǒng)的當(dāng)前狀態(tài)。如果只有一個環(huán)境和一個系統(tǒng),這樣考慮毫無問題。但是,系統(tǒng)在不斷增長的過程中往往需要引入某些獨一無二的東西,并且創(chuàng)建更多這類東西。\h0.4一個更大的例子我的客人認(rèn)為這個例子很有說服力。他走后,我意識到剛剛所揭示的東西跟我認(rèn)識的另一個人在一個非常大的項目里得到的經(jīng)驗非常相似。他們開發(fā)交互式事務(wù)處理系統(tǒng):屏幕上顯示著紙樣表單的電子版本,一群人圍坐在跟前。人們填寫表單,表單的內(nèi)容用于更新數(shù)據(jù)庫,等等。在項目接近尾聲的時候,客戶要求做些改動:劃分屏幕以同時顯示兩個無關(guān)的表單。這樣的改動是很恐怖的。這種程序通常充滿了各種庫函數(shù)調(diào)用,都假設(shè)知道“屏幕”在哪里和如何更新。這種改變通常要求查找出每一條用到了“屏幕”的代碼,并要把它們替換為表示“屏幕的當(dāng)前部分”的代碼。當(dāng)然,這些概念就是我們在前面的例子中看到的隱藏狀態(tài)(hiddenstate)的一種。因此,如果說在C++版本中修改這類應(yīng)用程序比在C版本中容易,就不足為奇了。所需要做的事就是改變屏幕顯示程序本身。相關(guān)的狀態(tài)信息已經(jīng)包含在類中,這樣在類的多個對象中復(fù)制它們只是小事一樁。\h0.5結(jié)論是什么使得對系統(tǒng)的改變?nèi)绱巳菀祝筷P(guān)鍵在于,一項計算的狀態(tài)作為對象的一部分應(yīng)當(dāng)是顯式可用的,而不是某些隱藏在幕后的東西。實際上,將一項計算的狀態(tài)顯式化,這個理念對于整個面向?qū)ο缶幊趟枷雭碚f,都是一個基礎(chǔ)\h[3]。小例子里可能還看不出這些考慮的重要性,但在大程序中它們就對程序的可理解性和可修改性產(chǎn)生很大的影響。如果我們看到如下的代碼:push(x);push(y);add();z=pop();我們可以理所當(dāng)然地猜測存在一個被操作的堆棧,并設(shè)置z為x和y的和,但是我們還必須知道應(yīng)該到何處去找這個堆棧。反之,如果我們看到s.push(x);s.push(y);s.add();z=s.pop();猜想堆棧就是s準(zhǔn)沒錯。確實,即使在C中,我們也可能會看到push(s,x);push(s,y);add(s);z=pop(s);但是C程序員對這樣的編程風(fēng)格通常不以為然,以至于在實踐中很少采用這種方式——除非他們發(fā)現(xiàn)確實需要更多的堆棧。原因就是C++采用類將狀態(tài)和動作綁在一起,而C則不然。C不贊成上述最后一個例子的風(fēng)格,因為要使例子運行起來,就要在函數(shù)push、add和pop之外單獨定義一個s類型。C++提供了單個地方來描述所有這些東西,表明所有東西都是相互關(guān)聯(lián)的。通過把有關(guān)系的事物聯(lián)系起來,我們就能更加清晰地用C++來表達(dá)自己的意圖。\h[1].Ada語言是在美國國防部組織下于20世紀(jì)70年代末開發(fā)的基于對象的高級語言,特別適合于高可靠性、實時的大型嵌入式系統(tǒng)軟件,在1998年之前是美國國防部唯一準(zhǔn)許的軍用軟件開發(fā)語言,至今仍然是最重要的軍用系統(tǒng)軟件開發(fā)語言?!g者注\h[2].DagBrück指出,首先考慮效率問題,是C/C++文化的“商標(biāo)”。我在寫這段文字時,不由自主地首先把效率問題提出來,可見這種文化對我的影響有多深!\h[3].關(guān)于面向?qū)ο蟪绦蛟O(shè)計和函數(shù)式程序設(shè)計(functionalprogramming)之間的區(qū)別,下面的這種說法可能算是無傷大雅的:在面向?qū)ο蟪绦蛟O(shè)計中,某項計算的結(jié)果狀態(tài)將取代先前的狀態(tài),而在函數(shù)式程序設(shè)計中,并非如此。\h第一篇動機(jī)抽象是有選擇的忽略。比如你要駕駛一輛汽車,但你又必須時時關(guān)注每樣?xùn)|西是如何運行的:發(fā)動機(jī)、傳動裝置、方向盤和車輪之間的連接等;那么你要么永遠(yuǎn)沒法開動這輛車,要么一上路就馬上發(fā)生事故。與此類似,編程也依賴于一種選擇,選擇忽略什么和何時忽略。也就是說,編程就是通過建立抽象來忽略那些我們此刻并不重視的因素。C++很有趣,它允許我們進(jìn)行范圍極其寬廣的抽象。C++使我們更容易把程序看作抽象的集合,同時也隱藏了那些用戶無須關(guān)心的抽象工作細(xì)節(jié)。C++之所以有趣的第二個原因是,它設(shè)計時考慮了特殊用戶群的需求。許多語言被設(shè)計用于探索特定的理論原理,還有些是面向特定的應(yīng)用種類。C++不然,它使程序員可以以一種更抽象的風(fēng)格來編程,與此同時,又保留了C中那些有用的和已經(jīng)深入人心的特色。因此,C++保留了不少C的優(yōu)點,比如偏重執(zhí)行速度快、可移植性強(qiáng)、與硬件和其他軟件系統(tǒng)的接口簡單等。C++是為那些信奉實用主義的用戶群準(zhǔn)備的。C和C++程序員通常都要處理雜亂而現(xiàn)實的問題;他們需要能夠解決這些問題的工具。這種實用主義在某種程度上體現(xiàn)了C++語言及其使用者的靈活性。例如,C++程序員總是為了特定的目的編寫不完整的抽象:他們會為了解決特定問題設(shè)計一個很小的類,而不在乎這個類是否提供所有用戶希望的所有功能。如果這個類夠用了,則他們可以對那些不盡如人意的地方視而不見。有的情況下,現(xiàn)在的折衷方案比未來的理想方案好得多。但是,實用主義和懶惰是有區(qū)別的。雖然很可能把C++程序?qū)懙脴O其難以維護(hù),但是也可以用C++把問題精心劃分為分割良好的模塊,使模塊與模塊之間的信息得到良好的隱藏。本書堅持以兩個思想為核心:實用和抽象。在這一篇中我們開始探討C++如何支持這些思想,后面幾篇將探索C++允許我們使用的各種抽象機(jī)制。\h第1章為什么我用C++本章介紹一些個人經(jīng)歷:我會談到那些使我第一次對使用C++產(chǎn)生興趣的事情以及學(xué)習(xí)過程中的心得體會。因此,我不會去說哪些東西是C++最重要的部分,相反會講講我是如何在特定情況下發(fā)現(xiàn)了C++的優(yōu)點。這些情形很有意思,因為它們是真實的歷史。我的問題不屬于類似于圖形、交互式用戶界面等“典型面向?qū)ο蟮膯栴}”,而是屬于一類復(fù)雜問題;人們最初用匯編語言來解決這些問題,后來多用C來解決。系統(tǒng)必須能在許多不同的機(jī)器上高效地運行,要與一大堆已有的系統(tǒng)軟件實現(xiàn)交互,還要足夠可靠,以滿足用戶群的苛刻要求。\h1.1問題我想做的事情是,使程序員們能更簡單地把自己的工作發(fā)布到不斷增加的機(jī)器中。解決方案必須可移植,還要使用一些操作系統(tǒng)提供的機(jī)制。當(dāng)時還沒有C++,所以對于那些特定的機(jī)器來說,C基本上就是唯一的選擇。我的第一個方案效果不錯,但實現(xiàn)之困難令人咋舌,主要是因為要在程序中避免武斷的限制。機(jī)器的數(shù)目迅速增加,終于超過負(fù)荷,到了必須對程序進(jìn)行大幅度修改的時候了。但是程序已經(jīng)夠復(fù)雜了,既要保證可靠性,又要保證正確性,如果讓我用C語言來擴(kuò)展這個程序,我真擔(dān)心搞不定。于是我決定嘗試用C++進(jìn)行改進(jìn)工作。結(jié)果是成功的:重寫后的版本較之老版本在效率上有了極大的提高,同時可靠性絲毫不打折扣。盡管C++程序天生不如相應(yīng)的C程序快,但是C++使我能在自己的智力所及的范圍內(nèi)使用一些高超的技術(shù),而對我來說,用C來實現(xiàn)這些技術(shù)太困難了。我被C++吸引住,很大程度上是由于數(shù)據(jù)抽象,而不是面向?qū)ο缶幊?。C++允許我定義數(shù)據(jù)結(jié)構(gòu)的屬性,還允許我在用到這些數(shù)據(jù)結(jié)構(gòu)時,把它們當(dāng)作“黑匣子”使用。這些特性用C實現(xiàn)起來將困難許多。而且,其他的語言都不能把我所需的效率和可靠性結(jié)合起來,同時還允許我對付已有的系統(tǒng)(和用戶)。\h1.2歷史背景1980年,當(dāng)時我還是AT&T貝爾實驗室計算科學(xué)研究中心的一名成員。早期的局域網(wǎng)原型剛剛作為試驗運行,管理方希望能鼓勵人們更多地利用這種新技術(shù)。為了達(dá)到這個目的,我們打算增加5臺機(jī)器,這超過了我們現(xiàn)有機(jī)器數(shù)目的兩倍。此外,根據(jù)硬件行情的趨勢來看,我們最終還會擁有多得多的機(jī)器(實際上,他們承諾使中心的網(wǎng)絡(luò)擁有50臺左右的機(jī)器)。這樣一來,我們將不得不應(yīng)對由此引發(fā)的軟件系統(tǒng)維護(hù)問題。維護(hù)問題肯定比你想象的還要困難得多。另外,類似于編譯器這樣的關(guān)鍵程序總在不斷變化。這些程序需要仔細(xì)安裝;磁盤空間不夠或者安裝時遇到硬件故障,都可能導(dǎo)致整臺機(jī)器報廢。而且,我們不具備計算中心站的優(yōu)越條件:所有的機(jī)器都由使用的人共同合作負(fù)責(zé)維護(hù)。因此,一個新程序要想運行到另一臺機(jī)器上,唯一的方法就是有人自愿負(fù)責(zé)把它放到上面。當(dāng)然,程序的設(shè)計者通常是不愿意做這件事的。所以,我們需要一個全局性的方法來解決維護(hù)問題。MikeLesk多年前就意識到了這個問題,并用一個名叫uucp的程序“部分地”加以解決,這個程序此后很有名氣。我說“部分地”,是因為Mike故意忽略了安全性問題。另外,uucp一次只允許傳遞一個文件,而且發(fā)送者無法確定傳輸是否成功。\h1.3自動軟件發(fā)布我決定扛著Mike的大旗繼續(xù)往下走。我采用uucp作為傳輸工具,通過編寫一個名叫ASD(AutomaticSoftwareDistribution,自動軟件發(fā)布)的軟件包來為程序員提供一個安全的方法,使他們能夠把自己的作品移植到其他機(jī)器上,我預(yù)料這些機(jī)器的數(shù)量會很快變得非常巨大。我決定采用兩種方式來增強(qiáng)uucp:更新完成后通知發(fā)送者,允許同時在不同的位置安裝一組文件。這些功能理論上都不是很困難,但是由于可靠性和通用性這兩個需求相互沖突,所以實現(xiàn)起來特別困難。我想讓那些與系統(tǒng)管理無關(guān)的人用ASD。為了這個目的,我應(yīng)該恰當(dāng)?shù)貪M足他們的需求,而且沒有任何瑣碎的限制。因此,我不想對文件名的長度、文件大小、一次運行所能傳遞的文件數(shù)目等問題作任何限制。而且一旦ASD里出現(xiàn)了bug,導(dǎo)致錯誤的軟件版本被發(fā)布,那就是ASD的末日,我決不會再有第二次機(jī)會。\h1.3.1可靠性與通用性C沒有內(nèi)建的可變長數(shù)組:編譯時修改數(shù)組大小的唯一方法就是動態(tài)分配內(nèi)存。因此,我想避免任何限制,就不得不導(dǎo)致大量的動態(tài)內(nèi)存分配和由此帶來的復(fù)雜性,復(fù)雜性又讓我擔(dān)心可靠性。例如,下面給出ASD中的一個典型的代碼段:/*讀取八進(jìn)制文件*/param=getfield(tf);mode=cvlong(param,strlen(param),8);/*讀入用戶號*/uid=numuid(getfield(tf));/*讀入小組號*/gid=numgid(getfield(tf));/*讀入文件名(路徑)*/path=transname(getfield(tf));/*直到行尾*/geteol(tf);這段代碼讀入文件中用tf標(biāo)識的一行的連續(xù)字段。為了實現(xiàn)這一點,它反復(fù)調(diào)用了幾次getfield,把結(jié)果傳遞到不同的會話程序中。代碼看上去簡單直觀,但是外表具有欺騙性:這個例子忽略了一個重要的細(xì)節(jié)。想知道嗎?那就想想getfield的返回類型是什么。由于getfield的值表示的是輸入行的一部分,所以顯然應(yīng)該返回一個字符串。但是C沒有字符串;最接近的做法是使用字符指針。指針必須指到某個地方;應(yīng)該什么時候用什么方法回收內(nèi)存?C里有一些解決這類問題的方法,但是都比較困難。一種辦法就是讓getfield每次都返回一個指針,這個指針指向調(diào)用它的新分配的內(nèi)存,調(diào)用者負(fù)責(zé)釋放內(nèi)存。由于我們的程序先后4次調(diào)用了getfield,所以也需要先后4次在適當(dāng)場合調(diào)用free。我可不愿意使用這種解決方法,寫這么多的調(diào)用真是很討厭,我肯定會漏掉一兩個。所以,我再一次想,假如我能承受漏寫一兩個調(diào)用的后果,也就能承受漏寫所有調(diào)用的后果。所以另一種解決方法應(yīng)該完全無需回收內(nèi)存,每次調(diào)用時,讓getfield分配內(nèi)存,然后永遠(yuǎn)不釋放。我也不能接受這種方法,因為它會導(dǎo)致內(nèi)存的過量消耗,而實際上,通過仔細(xì)地設(shè)計完全可以避免內(nèi)存不足的問題。我選擇的方法是讓getfield所返回內(nèi)存塊的有效期保持到下次調(diào)用getfield為止。這樣,總體來說,我不用老是記著要回收getfield傳回的內(nèi)存。作為代價,我必須記住,如果打算把getfield傳回的結(jié)果保留下來,那么每次調(diào)用后就必須將結(jié)果復(fù)制一份(并且記住要回收用于存放復(fù)制值的那塊內(nèi)存)。當(dāng)然,對于上述的程序片斷來說,付出這個代價是值得的,事實上,對于整個ASD系統(tǒng)來說,也是合適的。但是跟完全無需回收內(nèi)存的情況相比,使用這種策略顯然還是使得編寫程序的難度增大。結(jié)果,我為了使程序沒有這種局限性所付出的努力,大部分都花在進(jìn)行簿記工作的程序上,而不是解決實際問題的程序上。而且由于在簿記工作方面進(jìn)行了大量的手工編碼,我經(jīng)常擔(dān)心這方面的錯誤會使ASD不夠可靠。\h1.3.2為什么用C此時,你可能會問自己:“他為什么要用C來做呢?”。畢竟我所描述的簿記工作用其他的語言來寫會容易得多,譬如Smalltalk、Lisp或者Snobol,它們都有垃圾收集機(jī)制和可擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)。排除掉Smalltalk是很容易的:因為它不能在我們的機(jī)器上運行!Lisp和Snobol也有這個問題,只不過沒那么嚴(yán)重:盡管我寫ASD那會兒的機(jī)器能支持它們,但無法確保在以后的機(jī)器上也能用。實際上,在我們的環(huán)境中,C是唯一確定可移植的語言。退一步,即使有其他的語言可用,我也需要一個高效的操作系統(tǒng)接口。ASD在文件系統(tǒng)上做了很多工作,而這些工作必須既快又穩(wěn)定。人們會同時發(fā)送成百上千的文件,可能有數(shù)百萬個字節(jié),他們希望系統(tǒng)盡可能快,而且一次成功。\h1.3.3應(yīng)付快速增長我開始開發(fā)ASD的時候,我們的網(wǎng)絡(luò)還只是個原型:有時會失效,不能與每臺機(jī)器都連通。所以我用uucp作傳輸工具——我別無選擇。然而,一段時間后,網(wǎng)絡(luò)第一次變得穩(wěn)定,然后成為了不可或缺的部分。隨著網(wǎng)絡(luò)的改善,使用ASD的機(jī)器數(shù)目也在增加。到了大概25臺機(jī)器的時候,uucp已經(jīng)慢得不能輕松應(yīng)付這樣的負(fù)載了。是時候了,我們必須跨過uucp,開始直接使用網(wǎng)絡(luò)。對于使用網(wǎng)絡(luò)進(jìn)行軟件發(fā)布,我有一個好主意:我可以寫一個spooler來協(xié)調(diào)數(shù)臺機(jī)器上的發(fā)布工作。這個spooler需要一個在磁盤上的數(shù)據(jù)結(jié)構(gòu)來跟蹤哪臺機(jī)器成功地接收和安裝了軟件包,以便人們在操作失敗時可以找到出錯的地方。這個機(jī)制必須十分強(qiáng)健,可以在無人干預(yù)的情況下長時間運行。然而,我遲疑了好一陣,ASD最初版本中那些曾經(jīng)困擾過我的瑣碎細(xì)節(jié)搞得我泄了氣。我知道我希望解決的問題,但是想不出來在滿足我的限制條件的前提下,應(yīng)該如何用C來解決這些問題。一個成功的spooler必須:·有與盡量多的操作系統(tǒng)工具的接口?!け苊鉀]有道理的限制?!に俣壬媳仨毐扰f版本有本質(zhì)的提高?!と匀粯O為可靠。我可以解決所有這些問題,除了最后一個。寫一個spooler本身就很難,寫一個可靠的spooler就更難。一個spooler必須能夠?qū)Ω陡鞣N可能的奇異失敗,而且始終讓系統(tǒng)保持可以恢復(fù)的狀態(tài)。我在排除uucp中的bug上面花了數(shù)年的功夫,然而我仍然認(rèn)為,對于我新的spooler來說,要想成功,就必須立刻做到真正的bugfree。\h1.4進(jìn)入C++在那種情況下,我決定來看看能否用C++來解決我的問題。盡管我已經(jīng)非常熟悉C++了,但還沒有用它做過任何嚴(yán)肅的工作。不過BjarneStroustrup的辦公室離我不遠(yuǎn),在C++演化的過程中,我們曾經(jīng)在一起討論。當(dāng)時,我想C++有這么幾個特點對我有幫助。第一個就是抽象數(shù)據(jù)類型的觀念。比如,我知道我需要將向每臺計算機(jī)發(fā)送軟件的申請狀態(tài)存儲起來。我得想法把這些狀態(tài)用一種可讀的文件保存起來,然后在必要的時候取出來,在與機(jī)器會話時應(yīng)請求更新狀態(tài),并能最終改變標(biāo)識狀態(tài)的信息。所有這一切都要求能夠靈活進(jìn)行內(nèi)存的分配:我要存儲的機(jī)器狀態(tài)信息中,有一部分是在機(jī)器上所執(zhí)行的任何命令的輸出,而這輸出的長度是沒有限定的。另一個優(yōu)勢是JonathanShopiro最近寫的一個組件包,用于處理字符串和鏈表。這個組件包使得我能夠擁有真正的動態(tài)字符串,而不必在簿記操作的細(xì)節(jié)上戰(zhàn)戰(zhàn)兢兢。該組件包同時還支持可容納用戶對象的可變長鏈表。有了它,我一旦定義了一個抽象數(shù)據(jù)類型,比如說叫machine_status,就可以馬上利用Shopiro的組件包定義另一個類型——由machine_status對象組成的鏈表。為了把設(shè)計說得更具體一些,下面列出一些從C++版的ASDspooler中選出來的代碼片斷。這里變量m的類型是machine_status:\h[1]structmachine_status{Stringp;//機(jī)器名List<String>q;//存放可能的輸出Strings;//錯誤信息,如果成功則為空}//...m.s=domach(m.p,dfile,m.q);//發(fā)送文件if(m.s.length()==0){//工作正常否?sendfile=1;//成功——別忘了,我們是在發(fā)送一個文件if(m.q.length()==0)//是否有輸出?mli.remove();//沒有,這臺機(jī)器的事情已經(jīng)搞定elsemli.replace(m);//有,保存輸出}else{keepfile=1;//失敗,提起注意,稍后再試deadmach+=m.p;//加到失敗機(jī)器鏈表中mli.replace(m);//將其狀態(tài)放回鏈表}這個代碼片斷對于我們傳送文件的每臺目標(biāo)機(jī)器都執(zhí)行一遍。結(jié)構(gòu)體m將發(fā)送文件嘗試的執(zhí)行結(jié)果保存在自己的3個域當(dāng)中:p是一個String,保存機(jī)器的名字;q是一個String鏈表,保存執(zhí)行時可能的輸出;s是一個String,嘗試成功時為空,失敗時標(biāo)明原因。函數(shù)domach試圖將數(shù)據(jù)發(fā)送到另一臺機(jī)器上。它返回兩個值:一個是顯式的;另一個是隱式的,通過修改第三個參數(shù)返回。我們調(diào)用domach之后,m.s反映了發(fā)送嘗試是否成功的信息,而m.q則包含了可能的輸出。然后,我們通過將m.s.length()與0比較來檢查m.s是否為空。如果m.s確實為空,那么我們將sendfile置1,表示我們至少成功地把文件發(fā)送到了一臺機(jī)器上,然后我們來看看是否有什么輸出。如果沒有,那么我們可以把這臺機(jī)器從需要處理的機(jī)器鏈表中刪除。如果有輸出,則將狀態(tài)存儲在List中。變量mli就是一個指向該List內(nèi)部元素的指針(mli代表“machinelistiterator”,機(jī)器鏈表迭代器)。如果嘗試失敗,未能有效地與遠(yuǎn)程機(jī)器對話,那么我們將keepfile置為1,提醒我們必須保留該數(shù)據(jù)文件,以便下次再試,然后將當(dāng)前狀態(tài)存到List中。這個程序片斷中沒什么高深的東西。這里的每一行代碼都直接針對其試圖解決的問題。跟相應(yīng)的C代碼不同,這里沒有什么隱藏的簿記工作。這就是問題所在。所有的簿記工作都可以在庫里被單獨考慮,調(diào)試一次,然后徹底忘記。程序的其余部分可以集中精力解決實際問題。這個解決方案是成功的,ASD每年要在50臺機(jī)器上進(jìn)行4000次軟件更新。典型的例子包括更新編譯器的版本,甚至是操作系統(tǒng)內(nèi)核本身。較之C,C++使我得以從根本上在程序里更精確地表達(dá)我的意圖。我們已經(jīng)看到了一個C代碼片斷的例子,它展示了一些隱秘的細(xì)枝末節(jié)?,F(xiàn)在,我們來研究一下,為什么C必須考慮這些細(xì)枝末節(jié),再來看一看C++程序員怎樣才可能避免它們。C中隱藏的約定盡管C有字符串文本量,但它實際上沒有真正的字符串概念。字符串常量實際上是未命名的字符數(shù)組的簡寫(由編譯器在尾部插入空字符來標(biāo)識串尾),程序員負(fù)責(zé)決定如何處理這些字符。因此,比方說,盡管下面的語句是合法的;charhello[]="hello";但是這樣就不對了:charhello[5];hello="hello";因為C沒有復(fù)制數(shù)組的內(nèi)建方法。第一個例子中用6個元素聲明了一個字符數(shù)組,元素的初值分別是‘h’、‘e’、‘l’、‘l’、‘o’和‘\0’(一個空字符)。第二個例子是不合法的,因為C沒有數(shù)組的賦值,最接近的方法是:char*hello;hello="hello";這里的變量hello是一個指針,而不是數(shù)組:它指向包含了字符串常量“hello”的內(nèi)存。假設(shè)我們定義并初始化了兩個字符“串”:charhello[]="hello";charworld[]="world";并且希望把它們連接起來。我們希望庫可以提供一個concatenate函數(shù),這樣我們就可以寫成這樣:charhelloworld[];//錯誤concatenate(helloworld,hello,world);可惜的是,這樣并不奏效,因為我們不知道helloworld數(shù)組應(yīng)該占用多大內(nèi)存。通過寫成charhelloworld[12];//危險concatenate(helloworld,hello,world);可以將它們連接起來,但是我們連接字符串時并不想去數(shù)字符的個數(shù)。當(dāng)然,通過下面的語句,我們可以分配絕對夠用的內(nèi)存:charhelloworld[1000];//浪費而且仍然危險concatenate(helloworld,hello,world);但是到底多少才夠用?只要我們必須預(yù)先指定字符數(shù)組的大小為常量,我們就要接受猜錯許多次的事實。避免猜錯的唯一辦法就是動態(tài)決定串的大小。因此,譬如我們希望可以這樣寫:char*helloworld;helloworld=concatenate(hello,world);//有陷阱讓concatenate函數(shù)負(fù)責(zé)判斷包含變量hello和world的連接所需內(nèi)存的大小、分配這樣大小的內(nèi)存、形成連接以及返回一個指向該內(nèi)存的指針等所有這些工作。實際上,這正是我在ASD的最初的C版本中所做的事情:我采用了一個約定,即所有串以及類似串的值的大小都是動態(tài)決定的,相應(yīng)的內(nèi)存也是動態(tài)分配的。然而什么時候釋放內(nèi)存呢?對于C的串庫來說無法得知程序員何時不再使用串了。因此,庫必須要讓程序員負(fù)責(zé)決定何時釋放內(nèi)存。一旦這樣做了,我們就會有很多方法來用C實現(xiàn)動態(tài)串。對于ASD,我采用了3個約定。前兩個在C程序中是很普遍的,第三個則不是:1.串由一個指向它的首字符的指針來表示。2.串的結(jié)尾用一個空字符標(biāo)識。3.生成串的函數(shù)不遵循用于這些串的生命期的約定。例如,有些函數(shù)返回指向靜態(tài)緩沖區(qū)的指針,這些靜態(tài)緩沖區(qū)要保持到這些函數(shù)的下一次調(diào)用;而其他函數(shù)則返回指向調(diào)用者要釋放的內(nèi)存的指針。這些串的使用者需要考慮這些各不相同的生命周期,要在必要的時候使用free來釋放不再需要的串,還要注意不要釋放那些將在別的地方自動釋放的串。類似“hello”的字符串常量的生命周期是沒有限制的,因此,寫:char*hello;hello="hello";后不必釋放變量hello。前面的concatenate函數(shù)也返回一個無限存在的值,但是由于這個值保存在自動分配的內(nèi)存區(qū),所以使用完后應(yīng)該將它釋放。最后,有些類似getfield的函數(shù)返回一個生存期經(jīng)過精心定義的但是有限的值。甚至不應(yīng)該釋放getfield的值,但是如果想要將它返回的值保存一段很長的時間,我就必須記得將它復(fù)制到時間稍長的存儲區(qū)中。為什么要處理3種不同的存儲期?我無法選擇字符串常量:它們的語義是C的一部分,我不能改變。但是我可以使所有其他的字符串函數(shù)都返回一個指向剛分配的內(nèi)存的指針。那么就不必決定要不要釋放這樣的內(nèi)存了:使用完后就釋放內(nèi)存通常都是對的。不讓所有這些字符串函數(shù)都在每次調(diào)用時分配新內(nèi)存的主要原因是,這樣做會使我的程序十分巨大。例如,我將不得不像下面這樣重寫C程序代碼段(見1.3.1節(jié)):/*讀取八進(jìn)制文件*/param=getfield(tf);mode=cvlong(param,strlen(param),8);free(param);/*讀入用戶號*/s=getfield(tf);uid=numuid(s);free(s);/*讀入小組號*/s=getfield(tf);gid=numgid(s);free(s);/*讀入文件名(路徑)*/s=getfield(tf);path=transname(s);free(s);/*直到行尾*/geteol(tf);看來我還應(yīng)該有一些其他的可選工具來減小我所寫程序的大小。使用C++修改ASD與用C修改相比較,前者得到的程序更簡短,而所依賴的常規(guī)更少。作為例子,讓我們回顧C(jī)++ASD程序。該程序的第一句是為m.s賦值:m.s=domach(m.p,dfile,m.q);當(dāng)然,m.s是結(jié)構(gòu)體m的一個元素,m.s也可以是更大的結(jié)構(gòu)體的組成部分,等等。如果我必須自己記住要釋放m.s的位置,就必然對兩件事情有充分的心理準(zhǔn)備。第一,我不會一次正確得到所有的位置;要清除所有bug肯定要經(jīng)過多次嘗試。第二,每次明顯地改變某個東西的時候肯定會產(chǎn)生新的bug。我發(fā)現(xiàn)使用C++就不必再擔(dān)心所有這些細(xì)節(jié)。實際上,我在寫C++ASD時,沒有找到任何一個與內(nèi)存分配有關(guān)的錯誤。\h1.5重復(fù)利用的軟件盡管ASD的C版本里有許多用來處理字符串的函數(shù),我卻從沒有想過要把它們封裝成通用的包。向人們解釋使用這些函數(shù)要遵循哪些規(guī)則實在是太麻煩了。而且,根據(jù)多年和計算機(jī)用戶打交道的經(jīng)驗,我知道了一件事,那就是:在使用你的程序時,如果因為不遵守規(guī)則而導(dǎo)致工作失敗,大部分人不會反躬自省,反而會怪罪到你頭上。C可以做好很多事情,但不能處理靈活多變的字符串。C++版本的ASDspooler也使用字符—字符串函數(shù),已經(jīng)有人寫過這些函數(shù),所以我不用寫了。和我當(dāng)初發(fā)布C字符串規(guī)則比起來,編寫這些函數(shù)的人更愿意讓其他人來使用這些C++字符串例程,因為他不需要用戶記住那些隱匿的規(guī)定。同樣的,我使用串庫作為例程的基礎(chǔ)來實現(xiàn)分析文件名所需的指定的模式匹配,而這些例程又可抽取出來用于別的工作。此后我用C++編程時,還有過幾次類似的經(jīng)歷。我考慮問題的本質(zhì)是什么,再定義一個類來抓住這個本質(zhì),并確保這個類能獨立地工作。然后在遇到符合這個本質(zhì)的問題時就使用這個類。令人驚訝的是,解決方法通常只用編譯一次就能工作了。我的C++程序之所以可靠,是因為我在定義C++類時運用的思想比用C做任何事情時都多得多。只要類定義正確,我就只能按照我編寫它的初衷那樣去用它。因此,我認(rèn)為C++有助于直接表達(dá)我的思想并實現(xiàn)我的目的。\h1.6后記這章內(nèi)容基于一篇專欄文章,從我寫那篇文章到現(xiàn)在已經(jīng)過去很多年了。在這段時間里,我很欣慰地看到一整套C++類庫逐漸形成了。C庫到處都是,但是,可以肯定至少我所見過的C庫都有一定的問題。而C++則相反,它能實現(xiàn)真正的針對通用目的的庫,編寫這些庫的程序員甚至根本不必了解他們的庫會用于何處。這正是抽象的優(yōu)點。\h第2章為什么用C++工作在第1章中,我解釋了C++吸引我的地方,以及為什么要在編程中使用它。本章將對這一點進(jìn)行補(bǔ)充說明。過去的10年時間,我都用在了開發(fā)C++編程工具,理解怎樣使用它們,編寫教授C++的資料,以及修改優(yōu)化C++標(biāo)準(zhǔn)等工作上。C++有何魅力讓我如此癡迷呢?本章中,我將做出解答。這些問題的跨度很大,就像開車上班和設(shè)計汽車之間的差距。\h2.1小項目的成功我們很容易就會注意到:很多最成功的、最有名的軟件最初是由少數(shù)人開發(fā)出來的。這些軟件后來可能逐漸成長,然而,令人吃驚的是許多真正的贏家都是從小系統(tǒng)做起的。UNIX操作系統(tǒng)就是最好的例子,C編程語言也是。其他的例子還包括:電子表格、Basic和FORTRAN編程語言、MS-DOS和IBM的VM/370操作系統(tǒng)。VM/370尤其有趣,因為它完全是在IBM正規(guī)生產(chǎn)線之外發(fā)展起來的。盡管IBM多年來一直不提倡客戶使用VM/370,但該操作系統(tǒng)仍牢牢占據(jù)IBM大型機(jī)的主流市場。同樣令人吃驚的是,很多大項目的最終結(jié)果卻表現(xiàn)平平。我實在不愿意在公共場合指手畫腳,但是我想你自己也應(yīng)該能舉出大量的例子來。到底是什么使得大項目難以成功呢?我認(rèn)為原因在于軟件行業(yè)和其他很多行業(yè)不一樣,軟件制造的規(guī)模和經(jīng)濟(jì)效益不成正比。絕大多數(shù)稱職的程序員能在一兩個小時內(nèi)寫完一個100行的程序,而在大項目中通常每個程序員每天平均只寫10行代碼。\h2.1.1開銷有些負(fù)面的經(jīng)濟(jì)效益是由于項目組成員之間相互交流需要大量時間。一旦項目組的成員多到不能同時坐在一張餐桌旁,交流上的開銷問題就相當(dāng)嚴(yán)重了?;谶@一點,就必須要有某種正規(guī)的機(jī)制,保證每個項目成員對于其他人在做什么都了解得足夠清楚,這樣才能確保所有的部分最終能拼在一起。隨著項目的擴(kuò)大,這種機(jī)制將占用每個人更多的時間,同時每個人要了解的東西也會更多。我們只需要看一下項目組成員是如何利用時間的,就會發(fā)現(xiàn)這些開銷是多么明顯:管理錯誤報告數(shù)據(jù)庫;閱讀、編寫和回顧需求報告;參加會議;處理規(guī)范以及做除編程外的任何事情。\h2.1.2質(zhì)疑軟件工廠由于這些開銷是有目共睹的,所以很多人正在尋找減少它的途徑。起碼到目前為止,我還沒有見過什么有效的方法。這是個難題,我們可能沒有辦法解決。當(dāng)項目達(dá)到一定規(guī)模時,盡管作了百般努力,所有的一切好像還是老出錯;塔科馬海峽大橋和“挑戰(zhàn)者號”航天飛機(jī)災(zāi)難至今仍然歷歷在目。有些人認(rèn)為大項目的開銷是在所難免的。這種態(tài)度的結(jié)果就是產(chǎn)生了有著過多管理開銷的復(fù)雜系統(tǒng)。然而,更常見的情況是,這些所謂的管理最終不過是另一種經(jīng)過精心組織的開銷。開銷還在,只是被放進(jìn)干凈的盒子和圖表中,因此也更易于理解。有些人沉迷于這種開銷。他們心安理得地那么做,就好像它是件“好事”——就好像這種開銷真地能促進(jìn)而不是阻礙高效的軟件開發(fā)。畢竟,如果一定的管理和組織是有效的,那么更多的管理和組織就應(yīng)該更有效。我猜想,這個想法給程序項目引進(jìn)的紀(jì)律和組織,與為工廠廠房引進(jìn)生產(chǎn)流水線一樣。我希望這些人錯了。實際上我所接觸過的軟件工廠給我的感覺很不愉快。每個單獨的功能都是一個巨大機(jī)器的一部分,“系統(tǒng)”控制一切,人也要遵從它。正是這種強(qiáng)硬的控制導(dǎo)致生產(chǎn)線成為勞資雙方眾多矛盾的焦點。所幸的是,我并不認(rèn)為軟件只能朝這個方向發(fā)展。軟件工廠忽視了編程和生產(chǎn)之間的本質(zhì)區(qū)別。工廠是制造大量相同(或者基本相同)產(chǎn)品的地方。它講求規(guī)模效益,在生產(chǎn)過程中充分利用了分工的優(yōu)勢。最近,它的目標(biāo)已經(jīng)變成了要完全消除人力勞動。相反,軟件開發(fā)主要是要生產(chǎn)數(shù)目相對較少的、彼此完全不同的人造產(chǎn)品。這些產(chǎn)品可能在很多方面相似,但是如果太相似,開發(fā)工作就變成了機(jī)械的復(fù)制過程了,這可能用程序就能完成。因此,軟件開發(fā)的理想環(huán)境應(yīng)該不像工廠,而更像機(jī)械修理廠——在那里,熟練的技術(shù)工人可以利用手邊所有可用的精密工具來盡可能地提高工作效率。實際上,只要在能控制的范圍內(nèi),程序員(當(dāng)然指稱職的)就總是爭取讓他們的機(jī)器代替自己做它們所能完成的機(jī)械工作。畢竟,機(jī)器擅長干這樣的活兒,而人很容易產(chǎn)生厭倦情緒。隨著項目規(guī)模越來越大,越來越難以描述,這種把程序員看成是手工藝人的觀點也漸漸變得難以支持了。因此,我曾嘗試描述應(yīng)該如何將一個龐大的編程問題當(dāng)作一系列較小的、相互獨立的編程問題看待。為了做到這一點,我們首先必須把大系統(tǒng)中各個小項目之間存在的關(guān)系理順,使得相關(guān)人員不必反復(fù)互相核查。換言之,我們需要項目之間有接口,這樣,每個項目的成員幾乎不需要關(guān)心接口之外的東西。這些接口應(yīng)該像那些常用的子程序和數(shù)據(jù)結(jié)構(gòu)的抽象一樣成為程序員開發(fā)工具中的重要組成部分。\h2.2抽象自從25年前開始編程以來,我一直癡迷于那些能擴(kuò)展程序員能力的工具。這些工具可以是編程語言、操作系統(tǒng),甚至可以是關(guān)于某個問題的獨特思維方式。我知道有一天我將能夠輕松解決問題,這些問題是我在剛開始編程時想都不敢想的——我也知道,我不是獨自前行。我最鐘情的工具有一個共性,那就是抽象的概念。當(dāng)我在處理大問題的時候,這樣的工具總是能幫助我將問題分解成獨立的子問題,并能確保它們相互獨立。也就是說,當(dāng)我處理問題的某個部分的時候,完全不必?fù)?dān)心其他部分。例如,假設(shè)我正在用匯編語言寫一個程序,我必須時??紤]機(jī)器的狀態(tài)。我可以支配的工具是寄存器、內(nèi)存,以及運行于這些寄存器、內(nèi)存上的指令。要用匯編語言做成任何一件有用的事情,就必須把我的問題用這些特定概念表達(dá)出來。即使是匯編語言也包含了一些有用的抽象。首先是編寫的程序在機(jī)器執(zhí)行之前先被解釋了。這就是用匯編語言寫程序和直接在機(jī)器上寫程序的區(qū)別。更難以察覺的是,對于機(jī)器設(shè)計者來說,“內(nèi)存”和“寄存器”的概念本身就是一種抽象。如果拋開抽象不用,則程序的運行就要表示成處理器內(nèi)無數(shù)個門電路的狀態(tài)變換。如果你的想象力夠豐富的話,就可以看到除此之外還有更多層次的抽象。高級語言提供了更復(fù)雜的抽象。甚至用表達(dá)式替代一連串單獨的算術(shù)指令的想法,也是非常重大的。這種想法在20世紀(jì)50年代首次被提出時顯得很不同凡響,以至于后來成了FORTRAN命名的基礎(chǔ):FormulaTranslation。抽象如此有用,因此程序員們不斷發(fā)明新的抽象,并且運用到他們的程序中。結(jié)果幾乎所有重要的程序都給用戶提供了一套抽象。\h2.2.1有些抽象不是語言的一部分考慮一下文件的概念。事實上每種操作系統(tǒng)都以某種方式使文件能為用戶所用。每個程序員都知道文件是什么。但是,在大多數(shù)情況下,文件根本不是物理存在的!文件只是組織長期存儲的數(shù)據(jù)的一種方式,并由程序和數(shù)據(jù)結(jié)構(gòu)的集合提供支持來實現(xiàn)這個抽象。要使用文件做任何一件有意義的事情,程序員必須知道程序是通過什么訪問文件的,以及需要什么樣的請求隊列。對于典型的操作系統(tǒng)來說,必須確保提出不合理請求的程序得到相應(yīng)的錯誤提示,而不能造成系統(tǒng)本身崩潰或者文件系統(tǒng)破壞。實際上,現(xiàn)代的操作系統(tǒng)已經(jīng)就一個目的達(dá)成了共識,就是要在文件之間構(gòu)筑“防火墻”,以便增加程序在無意中修改數(shù)據(jù)的難度。\h2.2.2抽象和規(guī)范操作系統(tǒng)提供了一定程度的保護(hù)措施,而編程語言通常沒有。那些編寫新的抽象給其他程序員用的程序員,往往不得不依靠用戶自己去遵守編程語言技術(shù)上的限制。這些用戶不僅要遵守語言的規(guī)則,還要遵守其他程序員制定的規(guī)范。例如,由malloc函數(shù)實現(xiàn)的動態(tài)內(nèi)存的概念就是C庫中經(jīng)常使用的抽象。你可以用一個數(shù)字作參數(shù)來調(diào)用malloc,然后它在內(nèi)存中分配空間,并給出地址。當(dāng)你不再需要這塊內(nèi)存時,就用這個地址作參數(shù)來調(diào)用free函數(shù),這塊內(nèi)存就返回給系統(tǒng)留作它用。在很多情況下,這個簡單的抽象都相當(dāng)有用。不論規(guī)模大小,很難想象一個實際的C程序不使用malloc或者free。但是,要成功地使用抽象,必須遵循一些規(guī)范。要成功地使用動態(tài)內(nèi)存,程序員必須:·知道要分配多大內(nèi)存?!げ皇褂贸龇峙涞膬?nèi)存范圍外的內(nèi)存?!げ辉傩枰獣r釋放內(nèi)存?!ぶ挥胁辉傩枰獣r,才釋放內(nèi)存?!ぶ会尫欧峙涞膬?nèi)存?!で杏洐z查每個分配請求,以確保成功。要記住的東西很多,而且一不留神就會出錯。那么有多少可以做成自動實現(xiàn)的呢?用C的話,沒有多少。如果你正在編寫一個使用了動態(tài)內(nèi)存的程序,就難免要允許你的用戶釋放掉任何由他們分配的內(nèi)存,這些內(nèi)存的分配是他們對程序調(diào)用請求的一部分。\h2.2.3抽象和內(nèi)存管理有些語言通過垃圾收集(garbagecollection)來解決這個問題,這是一種當(dāng)內(nèi)存空間不再需要時自動回收內(nèi)存的技術(shù)。垃圾收集使得編寫程序時能更方便地采用靈活的數(shù)據(jù)結(jié)構(gòu),但要求系統(tǒng)在運行速度、編譯器和運行時系統(tǒng)復(fù)雜度方面付出代價。另外,垃圾收集只回收內(nèi)存,不管理其他資源。C++采用了另外一種更不同尋常的方法:如果某種數(shù)據(jù)結(jié)構(gòu)需要動態(tài)分配資源,則數(shù)據(jù)結(jié)構(gòu)的設(shè)計者可以在構(gòu)造函數(shù)和析構(gòu)函數(shù)中精確定義如何釋放該結(jié)構(gòu)所對應(yīng)的資源。這種機(jī)制不是總像垃圾收集那樣靈活,但是在實踐中,它與許多應(yīng)用更接近。另外,與垃圾收集比起來它有一個明顯的優(yōu)勢,就是對環(huán)境要求低得多:內(nèi)存一旦不用了就會被釋放,而不是等待垃圾收集機(jī)制發(fā)現(xiàn)之后才釋放。僅僅這些還不夠,要想名正言順地放棄自動垃圾收集,還應(yīng)該有一些好的理由。但是構(gòu)造函數(shù)和析構(gòu)函數(shù)的概念在其他方面也有很好的意義。用抽象的眼光看待數(shù)據(jù)結(jié)構(gòu),它們中的許多都有關(guān)于初始化和終止的概念,而不是單純地只有內(nèi)存分配。例如,一個代表緩沖輸出文件的數(shù)據(jù)結(jié)構(gòu)必須體現(xiàn)一個思想,就是緩沖區(qū)必須在文件關(guān)閉前釋放。這種約定總是在一些讓人意想不到的細(xì)節(jié)地方出現(xiàn),而由此產(chǎn)生的bug也總是非常隱蔽、難覓其蹤。我曾經(jīng)寫過一個程序,整整3年后才發(fā)現(xiàn)里面隱藏了一個bug!\h[2]在C++中,緩沖輸出文件類的定義必須包括一個釋放該緩沖區(qū)的析構(gòu)函數(shù)。這樣就不容易犯錯了。垃圾收集對此無能為力。同理,C++的很多地方也都用到了抽象和接口。其間的關(guān)鍵就是要能夠把問題分解為完全獨立的小塊。這些小塊不是通過規(guī)則相互聯(lián)系的,而是通過類定義和對成員函數(shù)和友元函數(shù)的調(diào)用聯(lián)系起來的。不遵守規(guī)則,就會馬上收到由編譯器而不是由異常征兆的出錯程序發(fā)出的診斷消息。\h2.3機(jī)器應(yīng)該為人服務(wù)為什么我要關(guān)注語言和抽象?因為我認(rèn)為大項目是無法高效地、順利地投入使用的,也不可能加以管理。我從沒見過,也不能想象,會有一種方法使得一個龐大的項目能夠?qū)顾羞@些問題。但是,如果我能找到把大項目化解為眾多小問題的方法,就能引入個體優(yōu)于混亂的整體、人類優(yōu)于機(jī)器的因素。我們必須做工具的主人,而不是其他任何角色。\h第3章生活在現(xiàn)實世界中在本章的開始,我將講述兩件看上去與C++毫無關(guān)系的軼事。千萬不要煩。波士頓一個成功的企業(yè)執(zhí)行官剛買了一輛非常昂貴的英國豪華轎車,結(jié)果在一個不期而至的寒冷天氣里,她驚訝地發(fā)現(xiàn)車子根本不能發(fā)動。她怒氣沖沖地向汽車廠的服務(wù)部門抱怨出現(xiàn)的問題?!笆裁矗俊笨头?jīng)理非常驚訝:“你沒有把它停在有暖氣的車庫里?”。這輛車確實豪華,儀表盤采用手工雕刻的木制面板,車內(nèi)到處是真皮皮革,設(shè)計者對舒適性和風(fēng)格的追求可謂精益求精了。但是波士頓的冬天比英格蘭冷得多,汽車設(shè)計者可能沒有意識到在寒冷的天氣里還會有客戶把車停在戶外。我于1967年開始學(xué)習(xí)APL。APL是一門非常優(yōu)秀的語言。它只有兩種數(shù)據(jù)類型——字符和數(shù)字——而數(shù)組是它唯一的數(shù)據(jù)結(jié)構(gòu)。變量和表達(dá)式的類型是動態(tài)定義的。APL程序員從不為內(nèi)存分配操心。執(zhí)行過程檢查所有的操作,所以APL程序員只能看到APL的值和程序。APL不僅僅是一門語言——它的第一個實現(xiàn)版本還包含了一個完整的操作系統(tǒng)。按照現(xiàn)在的標(biāo)準(zhǔn)來看,這個系統(tǒng)的效率之高出人意料:在當(dāng)時那種CPU功能和內(nèi)存只與現(xiàn)在的普通筆記本電腦相當(dāng)?shù)臋C(jī)器上\h[3],它可以同時支持30個分時用戶。到我能自如運用APL時,我發(fā)現(xiàn),解決相差不多的問題時,自己的程序大小僅僅是用其他常規(guī)語言(如FORTRAN和PL/I)所寫的程序的五分之一。這些程序并不因為小就功能簡單。內(nèi)建的APL操作符大多數(shù)只是一個字符,而不是標(biāo)識符,這也促使APL程序員總是為他們的變量起很短的名字。我認(rèn)為把APL看成一門不可讀的語言有點言過其實,這只不過是因為APL如此緊湊罷了。20世紀(jì)70年代早期,我還是哥倫比亞大學(xué)的一名學(xué)生。學(xué)校里隨處可見圖書館:大多數(shù)數(shù)學(xué)書在數(shù)學(xué)大樓里,大多數(shù)工程類書籍在工程學(xué)院,依此類推。每天圖書館都會列出一份書籍流通情況的清單,標(biāo)明哪些書被借走了、哪些還有庫存等情況。清單大概有100000行,分開打印在與圖書館最大的幾個分部對應(yīng)起來的6個表中。打印機(jī)每分鐘大約能打印900行,這樣每天要花兩個小時的打印機(jī)時來打印流通清單。根據(jù)墨菲法則\h[4],每當(dāng)我想打印自己的東西時,打印機(jī)就被那份該死的流通清單所占據(jù),害得我老得等著。因此,我考慮了一段時間,看是否能夠把圖書館流通系統(tǒng)轉(zhuǎn)換成APL。我認(rèn)為只需要在有流通清單副本的地方安裝終端就行了;這些終端可以直接和數(shù)據(jù)庫連接起來,從而一步就跨過了每天要耗費的堆積如山的紙堆。僅從紙張節(jié)省下來的開支就能很快填補(bǔ)開發(fā)項目所需的經(jīng)費。但是,這個項目還無法開展,因為存在下面這些問題:·如果有人要查詢流通列表,而APL系統(tǒng)又down掉了,這時該怎么辦?·誰負(fù)責(zé)維護(hù)出了故障的終端?·我們的程序員不了解APL,也不想學(xué)。怎么說服他們?·怎么才能使APL訪問數(shù)據(jù)庫?·怎樣處理已有的COBOL(PL/I,匯編語言)程序?我確實不知道能否找到所有這些問題的令人滿意的答案。但是,我知道這都無關(guān)緊要。假設(shè)有可能解決這些問題,并且用APL重寫了這個系統(tǒng),那么這個系統(tǒng)也不會是原來所想的那樣。類似于流通系統(tǒng)的事物都不是孤立的。有一整套經(jīng)年累月才完成的復(fù)雜的程序,用于跟蹤和處理擁有上百萬冊圖書的圖書館的情況。要想替換掉其中任何一個程序,都必須保證替換后的程序運行起來和原來的一模一樣,尤其是提供給系統(tǒng)其他部分的接口要一樣。因此,至
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 二零二五年空調(diào)銷售渠道拓展與品牌建設(shè)服務(wù)合同3篇
- 二零二五版合租房屋租賃合同-含裝修保證金條款3篇
- 二零二五版建筑設(shè)備租賃合同書范例2篇
- 二零二五版法院判決指導(dǎo)下的債務(wù)償還與再融資合同3篇
- 二零二五版第5章第5節(jié)合同擔(dān)保及供應(yīng)鏈金融合作協(xié)議3篇
- 二零二五版合同部合同合規(guī)性審查與風(fēng)險預(yù)警合同3篇
- 二零二五年度酒店物業(yè)服務(wù)質(zhì)量持續(xù)改進(jìn)合同3篇
- 二零二五年青少年體育賽事服裝贊助合同3篇
- 二零二五版安防監(jiān)控設(shè)備研發(fā)與生產(chǎn)合同3篇
- 二零二五年度物流行業(yè)集體合同協(xié)議范本3篇
- 2024年08月云南省農(nóng)村信用社秋季校園招考750名工作人員筆試歷年參考題庫附帶答案詳解
- 防詐騙安全知識培訓(xùn)課件
- 心肺復(fù)蘇課件2024
- 2024年股東股權(quán)繼承轉(zhuǎn)讓協(xié)議3篇
- 2024-2025學(xué)年江蘇省南京市高二上冊期末數(shù)學(xué)檢測試卷(含解析)
- 四川省名校2025屆高三第二次模擬考試英語試卷含解析
- 湖南財政經(jīng)濟(jì)學(xué)院專升本管理學(xué)真題
- 2024年認(rèn)證行業(yè)法律法規(guī)及認(rèn)證基礎(chǔ)知識
- 江蘇省建筑與裝飾工程計價定額(2014)電子表格版
- Proud-of-you中英文歌詞
- 基因的表達(dá)與調(diào)控.ppt
評論
0/150
提交評論