




版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
C++沉思錄
(中文版)
目錄
第0章序幕
0.1第一次嘗試
0.1.1改進
0.1.2另一種改進
0.2不用類來實現
0.3為什么用C++更簡單
0.4一個更大的例子
0.5結論
第一篇動機
第1章為什么我用C++
1.1問題
1.2歷史背景
1.3自動軟件發布
1.3.1可靠性與通用性
1.3.2為什么用C
1.3.3應付快速增長
1.4進入C++
1.5重復利用的軟件
1.6后記
第2章為什么用C++工作
2.1小項目的成功
2.1.1開銷
2.1.2質疑軟件工廠
2.2抽象
2.2.1有些抽象不是語言的一部分
2.2.2抽象和規范
2.2.3抽象和內存管理
2.3機器應該為人服務
第3章生活在現實世界中
第二篇類和繼承
第4章類設計者的核查表
第5章代理類
5.1問題
5.2經典解決方案
5.3虛復制函數
5.4定義代理類
5.5小結
第6章句柄:第一部分
6.1問題
6.2一個簡單的類
6.3綁定到句柄
6.4獲取對象
6.5簡單的實現
6.6引用計數型句柄
6.7寫時復制
6.8討論
第7章句柄:第二部分
7.1回顧
7.2分離引用計數
7.3對引用計數的抽象
7.4存取函數和寫時復制
7.5討論
第8章一個面向對象程序范例
8.1問題描述
8.2面向對象的解決方案
8.3句柄類
8.4擴展1:新操作
8.5擴展2:增加新的節點類型
8.6反思
第9章一個課堂練習的分析(上)
9.1問題描述
9.2接口設計
9.3補遺
9.4測試接口
9.5策略
9.6方案
9.7圖像的組合
9.8結論
第10章一個課堂練習的分析(下)
10.1策略
10.1.1方案
10.1.2內存分配
10.1.3結構構造
10.1.4顯示圖像
10.2體驗設計的靈活性
10.3結論
第11章什么時候不應當使用虛函數
11.1適用的情況
11.2不適用的情況
11.2.1效率
11.2.2你想要什么樣的行為
11.2.3不是所有的類都是通用的
11.3析構函數很特殊
11.4小結
第三篇模板
第12章設計容器類
12.1包含什么
12.2復制容器意味著什么
12.3怎樣獲取容器的元素
12.4怎樣區分讀和寫
12.5怎樣處理容器的增長
12.6容器支持哪些操作
12.7怎樣設想容器元素的類型
12.8容器和繼承
12.9設計一個類似數組的類
第13章訪問容器中的元素
13.1模擬指針
13.2獲取數據
13.3遺留問題
13.4指向constArray的Pointer
13.5有用的增強操作
第14章迭代器
14.1完成Pointer類
14.2什么是迭代器
14.3刪除元素
14.4刪除容器
M.5其他設計考慮
14.6討論
第15章序列
15.1技術狀況
15.2基本的傳統觀點
15.3增加一些額外操作
15.4使用范例
15.5再增加一些
15.6請你思考
第16章作為接口的模板
16.1問題
16.2第一個例子
16.3分離迭代方式
16.4遍歷任意類型
16.5增加其他類型
16.6將存儲技術抽象化
16.7實證
16.8小結
第17章模板和泛型算法
17.1一個特例
17.2泛型化元素類型
17.3推遲計數
17.4地址獨立性
17.5查找非數組
17.6討論
第18章泛型迭代器
18.1一個不同的算法
18.2需求的分類
18.3輸入迭代器
18.4輸出迭代器
18.5前向迭代器
18.6雙向迭代器
18.7隨機存取迭代器
18.8是繼承嗎
18.9性能
18.10小結
第19章使用泛型迭代器
19.1迭代器類型
19.2虛擬序列
19.3輸出流迭代器
19.4輸入流迭代器
19.5討論
第20章迭代器配接器
20.1一個例子
20.2方向不對稱性
20.3一致性和不對稱性
20.4自動反向
20.5討論
第21章函數對象
21.1一個例子
21.2函數指針
21.3函數對象
21.4函數對象模板
21.5隱藏中間類型
21.6一種類型包羅萬象
21.7實現
21.8討論
第22章函數配接器
22.1為什么是函數對象
22.2用于內建操作符的函數對象
22.3綁定者(Binders)
22.4更深入地探討
22.5接口繼承
22.6使用這些類
22.7討論
第四篇庫
第23章口常使用的庫
23.1問題
23.2理解問題:第1部分
23.3實現:第1部分
23.4理解問題:第2部分
23.5實現:第2部分
23.6討論
第24章一個庫接口設計實例
24.1復雜問題
24.2優化接口
24.3溫故知新
24.4編寫代碼
24.5結論
第25章庫設計就是語言設計
25.1字符串
25.2內存耗盡
25.3復制
25.4隱藏實現
25.5缺省構造函數
25.6其他操作
25.7子字符串
25.8結論
第26章語言設計就是庫設計
26.1抽象數據類型
26.1.1構造函數與析構函數
26.1.2成員函數及可見度控制
26.2庫和抽象數據類型
26.2.1類型安全的鏈接(linkage)
26.2.2命名空間
26.3內存分配
26.4按成員賦值(mcnbcrwiis。assignment)和初始化
26.5異常處理
26.6小結
第五篇技術
第27章自己跟蹤自己的類
27.1設計一個跟蹤類
27.2創建死代碼
27.3生成對象的審計跟蹤
27.4驗證容器行為
27.5小結
第28章在簇中分配對象
28.1問題
28.2設計方案
28.3實現
28.4加入繼承
28.5小結
第29章應用器、操縱器和函數對象
29.1問題
29.2一種解決方案
29.3另一種不同的解決方案
29.4多個參數
29.5一個例子
29.6簡化
29.7思考
29.8歷史記錄、參考資料和致謝
第30章將應用程序庫從輸入輸出中分離出來
30.1問題
30.2解決方案1:技巧加蠻力
30.3解決方案2:抽象輸出
30.4解決方案3:技巧而無蠻力
30.5評論
第六篇總結
第31章通過復雜性獲取簡單性
31.1世界是復雜的
31.2復雜性變得隱蔽
31.3計算機也是一樣
31.4計算機解決實際問題
31.5類庫和語言語義
31.6很難使事情變得容易
31.7抽象和接口
31.8復雜度的守恒
第32章說了Helloworld后再做什么
32.1找當地的專家
32.2選一種工具包并適應它
32.3C的某些部分是必需的
32.4C的其他部分不是必需的
32.5給自己設一些問題
32.6結論
附錄Koenig和Moo夫婦訪談
索引
第0章序幕
有一次,我遇到一個人,他曾經用各種語言寫過程序,唯獨沒用過C和C++。他提了一
個問題:“你能說服我去學習C++,而不是C嗎?",這個問題還真讓我想了一會兒。我
給許許多多人講過C++,可是突然間我發現他們全都是C程序員出身。到底該如何向從沒
用過C的人解釋C++呢?
于是,我首先問他使用過什么與c相近的語言。他曾用Ada[1]編寫過大量程序-
-但這對我毫無用處,我不了解Ada。還好他知道Pascal,我也知道。于是我打算在我
們兩個之間有限的共通點之上找到一個例子。
下面看看我是如何向他解釋什么事情是C++可以做好而C做不好的。
0.1第一次嘗試
C++的核心概念就是類,所以我一開始就定義了一個類。我想寫一個完整的類定義,它
要盡量小,要足夠說明問題,而且要有用。另外,我還想在例子中展示數據隱藏(data
hiding),因此希望它有公有數據(publicdata)和私有數據(privatedata)0經過幾分
鐘的思索,我寫下這樣的代碼:
#include<stdio.h>
classTrace{
public:
voidprint(char*s:{printf(繪s〃,s);}
};
我解釋了這段代碼是如何定義一個名叫Trace的新類,以及如何用Trace對象來打印
輸出消息:
intmain()
(
Tracet;
t.print(,zbeginmain()\n〃);
//main函數的主體
t.print("endmain()\nz/);
)
到目前為止,我所做的一切都和其他語言很相似。實際上,即使是C++,直接使用
printf也是很不錯的,這種先定義類,然后創建類的對象,再來打印這些消息的方法,
簡直舍近求遠。然而,當我繼續解釋類Trace定義的工作方式時,我意識到,即便是如此
簡單的例子,也已經觸及到某些重要的因素,正是這些因素使得C++如此強大而靈活。
0.1.1改進
例如,一旦我開始使用Trace類,就會發現,如果能夠在必要時關閉跟蹤輸出(trace
output),這將會是個有用的功能。小意思,只要改一下類的定義就行:
#include<stdio.h>
classTrace(
public:
Trace(){noisy=0;}
voidprint(char*s){if(noisy)printfs);}
voidon(){noisy=1;}
voidoff(){noisy=0;}
private:
intnoisy;
};
此時類定義包括了兩個公有成員函數on和off,它們影響私有成員noisy的狀態。只
有noisy為on(非零)才可以輸出。因此,
t.off();
會關閉t的對外輸出,直到我們通過下面的語句恢復t的輸出能力:
t.on();
我還指出,由于這些成員函數定義在Trace類自身的定義內,C++會內聯(inline)擴展
它們,所以就使得即使在不進行跟蹤的情況下,在程序中保留Trace對象也不必付出許多
代價。我立刻想到,只要讓print函數不做任何事情,然后重新編譯程序,就可以有效地
關閉所有Trace對象的輸出。
0.1.2另一種改進
當我問自己“如果用戶想要修改這樣的類,將會如何?”時,我獲得了更深層的理解。
用戶總是要求修改程序。通常,這些修改是一般性的,例如“你能讓它隨時關閉嗎?”
或者“你能讓它打印到標準輸出設備以外的東西上嗎?"我剛才已經回答了第一個問題。
接下來著手解決第二個問題,后來證明這個問題在C+-里可以輕而易舉地解決,而在C里
卻得大動干戈。
我當然可以通過繼承來創建一種新的Trace類。但是,我還是決定盡量讓示例簡單,避
免介紹新的概念。所以,我修改了Trace類,用一個私有數據來存儲輸出文件的標識,并
提供了構造函數,讓用戶指定輸出文件:
ttinclude<stdio.h>
classTrace{
public:
Trace()(noisy=0;f=stdout;}
Trace(FILE*ff){noisy=0;f=ff;}
voidprint(char*s)
{if(noisy)fprintf(f,s);}
voidon(){noisy=1;}
voidoff(){noisy=0;}
private:
intnoisy;
FILE*f;
);
這樣改動,基于一個事實:
printf(args);
等價于:
fprintf(stdout,args);
創建一個沒有特殊要求的Trace類,則其對象的成員f為stdout。因此,調用fprintf
所做的工作與調用前一個版本的printf是一樣的。
類Trace有兩個構造函數:一個是無參構造函數,跟上例一樣輸出到stdout;另一個
構造函數允許明確指定輸出文件。因此,上面那個使用了Trace類的示例程序可以繼續工
作,但也可以將輸出定向到比如說stderr上:
intmain()
(
Tracet(stderr);
t.print("beginmain()\n〃);
//main函數的主體
t.print("endmain()\n");
}
簡而言之,我運用C++類的特殊方式,使得對程序的改進變得輕而易舉,而且不會影響
使用這些類的代碼。
0.2不用類來實現
此時,我又開始想,對于這個問題,典型的c解決方案會是怎樣的。它可能會從一個類
似于函數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_off0{noisy=0;}
這個方法是著效的,但與C++方法比較起來有3個明顯的缺點。
首先,函數trace不是內聯的,因此即使當跟蹤關閉時,它還保持著函數調用的開銷
[2]o在很多C的實現中,這個額外負擔都是無法避免的。
第二,C版本引入了3個全局名字:trace>tracc_on和tracc_off,而C++只引入了1
個。
第三,也是最重要的一點,我們很難將這個例子一般化,使之能輸出到一個以二的文件
中。為什么呢?考慮一下我們會怎樣使用這個trace函數:
intmain()
(〃“
trace(^beginmain(八n");
//main函數主體
trace("endmain()\nv);
}
采用C++,可以只在創建Trace對象時一次性指定文件名。而在C版本中,情況相反,
沒有合適的位置指定文件名。一個顯而易見的辦法就是給函數trace增加一個參數,但是
需要找到所有對trace函數的調用,并插入這個新噌的參數。另一種辦法是引入名為
trace_out的第4個函數,用來將跟蹤輸出轉向到其他文件。這當然也得要求判斷和記錄
跟蹤輸出是打開還是關閉。考慮一下,譬如,main調用的一個函數恰好利用了trace_out
向另一個文件輸出,則何時切換輸出的開關狀態呢?顯然,要想使結果正確需要花費相當
的精力。
0.3為什么用C++更簡單
為什么在c方案中進行擴展會如此困難呢?難就難在沒有一個合適的位置來存儲輔助的
狀態信息一一在本例中是文件名和“noisy”標記。在這里,這個問題尤其讓人惱火,因
為在原來的情況下根本就不需要狀態信息,只是到后來才知道需要存儲狀態。
往原本沒有考慮存儲狀態信息的設計中添加這項能力是很難的。在C中,最常見的做法
就是找個地方把它藏起來,就像我這里采用“noisy”標記一樣。但是這種技術也只能做
到這樣;如果同時出現多個輸出文件來攪局,就很難有效控制了。C++版本則更簡單,因
為C++鼓勵采用類來表示類似于輸出流的事物,而類就提供了?個理想的位置來放置狀態
信息。
“淳果是,C傾向于不存儲狀態信息,除非事先己經規劃妥當。因此,C程序員趨向于假
設有這樣一個“環境”:存在一個位置集合,他們可以在其中找到系統的當前狀態。如果
只有一個環境和一個系統,這樣考慮毫無問題。但是,系統在不斷增長的過程中往往需要
引入某些獨一無二的東西,并且創建更多這類東西。
0.4一個更大的例子
我的客人認為這個例子很有說服力。他走后,我意識到剛剛所揭示的東西跟我認識的另
一個人在一個非常大的項E里得到的經驗非常相似。
他們開發交互式事務處理系統:屏幕上顯示著紙樣表單的電子版木,一群人圍坐在跟前。
人們填寫表單,表單的內容用于更新數據庫,等等。在項目接近尾聲的時候,客戶要求做
些改動:劃分屏幕以同時顯示兩個無關的表單。
這樣的改動是很恐怖的。這種程序通常充滿了各種庫函數調用,都假設知道“屏幕”在
哪里和如何更新。這種改變通常要求查找出每一條用到了“屏幕”的代碼,并要把它們替
換為表示“屏幕的當前部分”的代碼。
當然,這些概念就是我僅在前面的例子中看到的隱藏狀態(hiddenstate)的一種。因此,
如果說在C++版本中修改這類應用程序比在C版本中容易,就不足為奇了。所需要做的事
就是改變屏幕顯示程序本身。相關的狀態信息已經包含在類中,這樣在類的多個對象中復
制它們只是小事一樁。
0.5結論
是什么使得對系統的改變如此容易?關鍵在于,一項計算的狀態作為對象的一部分應當
是顯式可用的,而不是某些隱藏在幕后的東西。實際上,將一項計算的狀態顯式化,這個
理念對于整個面向對象編程思想來說,都是一個基礎[3]o
小例子里可能還看不出這些考慮的重要性,但在大程序中它們就對程序的可理解性和可
修改性產生很大的影響。如果我們看到如下的代碼:
push(x);
push(y);
add();
z=pop();
我們可以理所當然地猜測存在一個被操作的堆棧.并設置z為x和y的和.但是我們還
必須知道應該到何處去找這個堆棧。反之,如果我們看到
s.push(x);
s.push(y);
s.add();
z=s.pop();
猜想堆棧就是s準沒錯。確實,即使在C中,我們也可能會看到
push(s,x);
push(s,y);
add(s);
z=pop(s);
但是C程序員對這樣的編程風格通常不以為然,以至于在實踐中很少采用這種方式-
除非他們發現確實需要更多的堆棧。原因就是C++采用類將狀態和動作綁在?起,而C則
不然。C不贊成上述最后一個例子的風格,因為要使例子運行起來,就要在函數push、
add和pop之外單獨定義一個s類型。C++提供了單個地方來描述所有這些東西,表明所
有東西都是相互關聯的。通過把有關系的事物聯系起來,我們就能更加清晰地用C++來表
達自己的意圖。
[1].Ada語言是在美國國防部組織下于20世紀70年代末開發的基于對象的高級語言,
特別適合于高可靠性、實時的大型嵌入式系統軟件,在1998年之前是美國國防部唯一準
許的軍用軟件開發語言,至今仍然是最重要的軍用系統軟件開發語言。一一譯者注
[2].DagBrtlck指出,首先考慮效率問題,是C/C++文化的“商標”。我在寫這段文字時,
不由自主地首先把效率問題提出來,可見這種文化對我的影響有多深!
[3].關于面向對象程序設計和函數式程序設計(functionalprogramming)之間的區別,
下面的這種說法可能算是無傷大雅的:在面向對象程序設計中,某項計算的結果狀態將取
代先前的狀態,而在函數式程序設計中,并非如此。
第一篇動機
抽象是有選擇的忽略。比如你要駕駛一輛汽車,但你又必須時時關注每樣東西是如何
運行的:發動機、傳動裝置、方向盤和車輪之間的連接等;那么你要么永遠沒法開動這輛
車,要么一上路就馬上發生事故。與此類似,編程也依賴于一種選擇,選擇忽略什么和何
時忽略。也就是說,編程就是通過建立抽象來忽略那些我們此刻并不重視的因素。C++很
有趣,它允許我們進行范圍極其寬廣的抽象。C++使我們更容易把程序看作抽象的集合,
同時也隱藏了那些用戶無須關心的抽象工作細節。
C++之所以有趣的第二個原因是,它設計時考慮了特殊用戶群的需求。許多語言被設計
用于探索特定的理論原理,還有些是面向特定的應用種類。C++不然,它使程序員可以以
一種更抽象的風格來編程,與此同時,又保留了C中那些有用的和已經深入人心的特色。
因此,C++保留了不少C的優點,比如偏重執行速度快、可移植性強、與硬件和其他軟件
系統的接口簡單等。
C++是為那些信奉實用主義的用戶群準備的。C和C++程序員通常都要處理雜亂而現實的
問題;他們需要能夠解決這些問題的工具。這種實用主義在某種程度上體現了C++語言及
其使用者的靈活性。例如,C++程序員總是為r特定的目的編寫不完整的抽象:他們會為
了解決特定問題設計一個很小的類,而不在乎這個類是否提供所有用戶希望的所有功能。
如果這個類夠用了,則他們可以對那些不盡如人意的地方視而不見。有的情況下,現在的
折衷方案比未來的理想方案好得多。
但是,實用主義和懶惰是有區別的。雖然很可能把C++程序寫得極其難以維護,但是也
可以用C++把問題精心劃分為分割良好的模塊,使模塊與模塊之間的信息得到良好的隱藏。
本書堅持以兩個思想為核心:實用和抽象。在這一篇中我們開始探討C++如何支持這些
思想,后面幾篇將探索C++允許我們使用的各種抽象機制。
第1章為什么我用C++
本章介紹一些個人經歷:我會談到那些使我第一次對使用C++產生興趣的事情以及學習
過程中的心得體會。因此,我不會去說哪些東西是C++最重要的部分,相反會講講我是如
何在特定情況下發現了C++的優點。
這些情形很有意思,因為它們是真實的歷史。我的問題不屬于類似于圖形、交互式用戶
界面等“典型面向對象的問題”,而是屬于一類復雜問題:人們最初用匯編語言來解決這
些問題,后來多用C來解決。系統必須能在許多不同的機器上高效地運行,要與一大堆已
有的系統軟件實現交互,還要足夠可靠,以滿足用戶群的苛刻要求。
1.1問題
我想做的事情是,使程序員們能更簡單地把自己的工作發布到不斷增加的機器中。解決
方案必須可移植,還要使用一些操作系統提供的機制。當時還沒有C++,所以對于那些特
定的機器來說,C基本上就是唯一的選擇。我的第一個方案效果不錯,但實現之困難令人
咋舌,主要是因為要在程序中避免武斷的限制。
機器的數目迅速增加,終于超過負荷,到了必須對程序進行大幅度修改的時候了。但是
程序已經夠復雜了,既要保證可靠性,又要保證正確性,如果讓我用C語言來擴展這個程
序,我真擔心搞不定。
于是我決定嘗試用C++進行改進工作。結果是成功的:重寫后的版本較之老版本在效率
上有了極大的提高,同時可靠性絲毫不打折扣。盡管C++程序天生不如相應的C程序快,
但是C++使我能在自己的智力所及的范圍內使用一些高超的技術,而對我來說,用C來實
現這些技術太困難了。
我被C++吸引住,很大程度上是由于數據抽象,而不是面向對象編程。C++允許我定義
數據結構的屬性,還允許我在用到這些數據結構時,把它們當作“黑匣子”使用。這些特
性用C實現起來將困難許多。而且,其他的語言都不能把我所需的效率和可靠性結合起來,
同時還允許我對付已有的系統(和用戶)。
1.2歷史背景
1980年,當時我還是AT&T貝爾實驗室計算科學研究中心的一名成員。早期的局域網原
型剛剛作為試驗運行,管理方希望能鼓勵人們更多地利用這種新技術。為了達到這個目的,
我們打算增加5臺機器,這超過了我們現有機器數目的兩倍。此外,根據硬件行情的趨勢
來看,我們最終還會擁有多得多的機器(實際上,他們承諾使中心的網絡擁有5C臺左右
的機器)。這樣一來,我們將不得不應對由此引發的軟件系統維護問題。
維護問題肯定比你想象的還要困難得多。另外,類似于編譯器這樣的關鍵程序總在不斷
變化。這些程序需要仔細安裝;磁盤空間不夠或者安裝時遇到硬件故障,都可能導致整臺
機器報廢。而且,我們不具備計算中心站的優越條件:所有的機器都由使用的人共同合作
負責維護。因此,一個新程序要想運行到另一臺機器上,唯一的方法就是有人自愿負責把
它放到上面。當然,程序的設計者通常是不愿意做這件事的。所以,我們需要一個全局性
的方法來解決維護問題。
MikeLesk多年前就意識到了這個問題,并用一個名叫uucp的程序“部分地”加以解
決,這個程序此后很有名氣。我說“部分地”,是因為Mike故意忽略了安全性問題。另
外,uucp一次只允許傳遞一個文件,而且發送者無法確定傳輸是否成功。
1.3自動軟件發布
我決定扛著Mike的大旗繼續往下走。我采用uucp作為傳輸工具,通過編寫一個名叫
ASD(AutomaticSoftwareDistribution,自動軟件發布)的軟件包來為程序員提供一個安
全的方法,使他們能夠把自己的作品移植到其他機器上,我預料這些機器的數量會很快變
得非常巨大。我決定采用兩種方式來增強uucp:更新完成后通知發送者,允許同時在不
同的位置安裝一組文件。
這些功能理論上都不是很困難,但是由于可靠性和通用性這兩個需求相互沖突,所以實
現起來特別困難。我想讓那些與系統管理無關的人用ASDo為了這個目的,我應該恰當地
滿足他們的需求,而且沒有任何瑣碎的限制。因此,我不想對文件名的長度、文件大小、
一次運行所能傳遞的文件數目等問題作任何限制。而且一旦ASD里出現了bug,導致錯誤
的軟件版本被發布,那就是ASD的末日,我決不會再有第二次機會。
L3.1可靠性與通用性
C沒有內建的可變長數組:編譯時修改數組大小的唯一方法就是動態分配內存。因此,
我想避免任何限制,就不得不導致大量的動態內存分配和由此帶來的復雜性,復雜性又讓
我擔心可靠性。例如,下面給出ASD中的一個典型的代碼段:
/*讀取八進制文件*/
param=getfield(tf);
mode=cvlong(param,strion(param),8);
/*讀入用戶號*/
uid=numuid(getfield(tf));
/*讀入小組號*/
gid=numgid(getfield(tf));
/*讀入文件名(路徑)*/
path=transname(getfield(tf));
/*直到行尾*/
geteol(tf);
這段代碼讀入文件中用tf標識的一行的連續字段。為了實現這一點,它反復調用了兒
次getfield,把結果傳遞到不同的會話程序中。
代碼看上去簡單直觀,但是外表具有欺騙性:這個例子忽略了一個重要的細節。想知道
嗎?那就想想getfield的返回類型是什么。由于getfield的值表示的是輸入行的一部分,
所以顯然應該返回一個字符串。但是C沒有字符串;最接近的做法是使用字符指針。指針
必須指到某個地方;應該什么時候用什么方法回收內存?
C里有一些解決這類問題的方法,但是都比較困難。一種辦法就是讓getfield每次都
返回一個指針,這個指針指向調用它的新分配的內存,調用者負責釋放內存。由于我們的
程序先后4次調用了getfield,所以也需要先后4次在適當場合調用free。我可不愿意
使用這種解決方法,寫這么多的調用真是很討厭,我肯定會漏掉一兩個。
所以,我再一次想,假如我能承受漏寫一兩個調用的后果,也就能承受漏寫所有調用的
后果。所以另一種解決方法應該完全無需回收內存,每次調用時,讓getfield分配內存,
然后永遠不釋放。我也不能接受這種方法,因為它會導致內存的過量消耗,而實際上,通
過仔細地設計完全可以避免內存不足的問題。
我選擇的方法是讓getfield所返回內存塊的有效期保持到下次調用getfield為止。這
樣,總體來說,我不用老是記著要回收getfield傳回的內存。作為代價,我必須記住,
如果打算把getfield傳回的結果保留下來,那么每次調用后就必須將結果復制一份(并
且記住要回收用于存放復制值的那塊內存)。當然,對于上述的程序片斷來說,付出這個
代價是值得的,事實上,對于整個ASD系統來說,也是合適的。但是跟完全無需回收內存
的情況相比,使用這種策略顯然還是使得編寫程序的難度增大。結果,我為了使程序沒有
這種局限性所付出的努力,大部分都花在進行簿記工俏的程序上,而不是解決實際問題的
程序上。而且由于在簿記工作方面進行了大量的手工編碼,我經常擔心這方面的錯誤會使
ASD不夠可靠。
1.3.2為什么用C
此時,你可能會問自己:“他為什么要用C來做呢?”。畢竟我所描述的簿記工作用其
他的語言來寫會容易得多,譬如Smalllalk>Lisp或者Snob。1,它們都有垃圾收集機制
和可擴展的數據結構。
排除掉Smalltalk是很容易的:因為它不能在我們的機器上運行!Lisp和Snobol也有
這個問題,只不過沒那么嚴重:盡管我寫ASD那會兒的機器能支持它們,但無法確保在以
后的機器上也能用。實際上,在我們的環境中,C是唯一確定可移植的語言。
退一步,即使有其他的語言可用,我也需要一個高效的操作系統接口。ASD在文件系統
上做了很多工作,而這些工作必須既快又穩定。人們會同時發送成百上千的文件,可能有
數百萬個字節,他們希望系統盡可能快,而且一次成功。
1.3.3應付快速增長
我開始開發ASD的時候,我們的網絡還只是個原型:有時會失效,不能與每臺機器都連
通。所以我用uucp作傳輸工具一一我別無選擇。然而,一段時間后,網絡第一次變得穩
定,然后成為了不可或缺的部分。隨著網絡的改善,使用ASD的機器數目也在增加。到了
大概25臺機器的時候,uucp已經慢得不能輕松應付這樣的負載了。是時候了,我們必須
跨過uucp,開始直接使用網絡。
對于使用網絡進行軟件發布,我有一個好主意:我瓦以寫一個spooler來協調數臺機器
上的發布工作。這個spooler需要一個在磁盤上的數據結構來跟蹤哪臺機器成功地接收和
安裝了軟件包,以便人們在操作失敗時可以找到出錯的地方。這個機制必須十分強健,可
以在無人干預的情況下長時間運行。
然而,我遲疑了好一陣,ASD最初版本中那些曾經困擾過我的瑣碎細節搞得我泄了氣。
我知道我希望解決的問題,但是想不出來在滿足我的限制條件的前提下,應該如何用C來
解決這些問題。一個成功的spooler必須:
?有與盡量多的操作系統工具的接口。
?避免沒有道理的限制。
?速度上必須比舊版本有本質的提高。
?仍然極為可靠。
我可以解決所有這些問題,除了最后一個。寫一個spooler本身就很難,寫一個可靠的
spooler就更難。一個spooler必須能夠對付各種可能的奇異失敗,而且始終讓系統保持
可以恢復的狀態。
我在排除UUCP中的bug上面花了數年的功夫,然而我仍然認為,對于我新的spooler
來說,要想成功,就必須立刻做到真正的bugfree。
1.4進入C++
在那種情況下,我決定來看看能否用C++來解決我的問題。盡管我已經非常熟悉C++了,
但還沒有用它做過任何嚴肅的工作。不過BjarneStroustrup的辦公室離我不遠,在C++
演化的過程中,我們曾經在一起討論。
當時,我想C++有這么幾個特點對我有幫助。
第一個就是抽象數據類型的觀念。比如,我知道我需要將向每臺計算機發送軟件的申請
狀態存儲起來。我得想法把這些狀態用一種可讀的文件保存起來,然后在必要的時候取出
來,在與機器會話時應請求更新狀態,并能最終改變標識狀態的信息。所有這一切都要求
能夠靈活進行內存的分配:我耍存儲的機器狀態信息中,有一部分是在機器上所執行的任
何命令的輸出,而這輸出的長度是沒有限定的。
另一個優勢是JonathanShopiro最近寫的一個組件包,用于處理字符串和鏈表。這個
組件包使得我能夠擁有真正的動態字符串,而不必在簿記操作的細節上戰戰兢兢。該組件
包同時還支持可容納用戶市象的可變長鏈表。有了它,我一旦定義了一個抽象數據類型,
比如說叫machine_status,就可以馬上利用Shopiro的組件包定義另一個類型一一由
machinestatus對象組成的鏈表。
為了把設計說得更具體一些,下面列出一些從C++版的ASDspooler中選出來的代碼片
斷。這里變量m的類型是machine_status:[1]
structmachinestatus{
Stringp;//機器名
List<String>q;//存放可能的輸出
Strings;//錯誤信息,如果成功則為空
}
//...
m.s=domach(m.p,dfile,m.q);//發送文件
if(m.s.length()=0){//工作正常否?
sendfile=1;//成功——別忘了,我們是在發送一個文件
if(m.q.length()==0)//是否有輸出?
mli.remove();//沒有,這臺機器的事情已經搞定
else
mli.replace(m);//有,保存輸出
}else{
keepfile=1;//失敗,提起注意,稍后再試
deadmach+=m.p;//加到失敗機器鏈表中
mli.replace(in);//將其狀態放回鏈表
)
這個代碼片斷對于我們傳送文件的每臺目標機器都執行一遍。結構體m將發送文件嘗試
的執行結果保存在自己的3個域當中:p是一個String,保存機器的名字;q是一個
String鏈表,保存執行時可能的輸出;s是一個String,嘗試成功時為空,失敗時標明
原因。
函數domach試圖將數據發送到另一臺機器上。它返回兩個值:一個是顯式的;另一個
是隱式的,通過修改第三個參數返回。我們調用domach之后,m.s反映了發送嘗試是否
成功的信息,而m.q則包含了可能的輸出。
然后,我們通過將m.s.length。與0比較來檢查m.s是否為空。如果m.s確實為空,
那么我們將sendfile置b表示我們至少成功地把文件發送到了一臺機器上,然后我們
來看看是否有什么輸出。如果沒有,那么我們可以把這臺機器從需要處理的機器鏈表中刪
除。如果有輸出,則將狀態存儲在List中。變量mli就是一個指向該List內部元素的指
針(mli代表"machinelistiterator”,機器鏈表迭代器)。
如果嘗試失敗,未能有效地與遠程機器對話,那么我們將keepfile置為1,提醒我們
必須保留該數據文件,以便下次再試,然后將當前狀態存到List中。
這個程序片斷中沒什么高深的東西。這里的每?行代碼都直接針對其試圖解決的問題。
跟相應的C代碼不同,這里沒有什么隱藏的簿記工作。這就是問題所在。所有的簿記工作
都可以在庫里被單獨考慮,調試一次,然后徹底忘記。程序的其余部分可以集中精力解決
實際問題。
這個解決方案是成功的,ASD每年要在50臺機器上進行4000次軟件更新。典型的例子
包括更新編譯器的版本,甚至是操作系統內核本身。較之C,C++使我得以從根本上在程
序里更精確地表達我的意圖。
我們已經看到了一個C代碼片斷的例子,它展示了一些隱秘的細枝末節。現在,我們來
研究一下,為什么C必須考慮這些細枝末節,再來看一看C++程序員怎樣才可能避免它們。
Q中^的約
盡管C有三符串文本量,但它實際上沒有真正的字符串概念。字符串常量實際上是未命
名的字符數組的簡寫(由編譯器在尾部插入空字符來標識串尾),程序員負責決定如何處
理這些字符。因此,比方說,盡管下面的語句是合法的;
charhello[]="hello”;
但是這樣就不對了:
charhello[5];
hello="hello”;
因為C沒有復制數組的內建方法。第一個例子中用6個元素聲明了一個字符數組,元素
的初值分別是'h,、",、’1,、’1,、’0"和(一個空字符)。第二個例
子是不合法的,因為C沒有數組的賦值,最接近的方法是:
char*hello;
hello="hello";
這里的變量hello是一個指針,而不是數組:它指向包含了字符串常量"hello”的內
存。
假設我們定義并初始化了兩個字符“串”:
charhelloLJ="hello”;
charworld[]="world”;
并且希望把它們連接起來。我們希望庫可以提供一個concatenate函數,這樣我們就可
以寫成這樣:
charhelloworld[];〃錯誤
concatenate(helloworld,hello,world);
可惜的是,這樣并不奏效,因為我們不知道helloworld數組應該占用多大內存。通過
寫成
charhelloworld[12];〃危險
concatenate(helloworld,hello,world);
可以將它們連接起來,但是我們連接字符串時并不想去數字符的個數。當然,通過下面
的語句,我們可以分配絕對夠用的內存:
charhelloworld[1000];〃浪費而且仍然危險
concatenate(helloworld,hello,world);
但是到底多少才夠用?只要我們必須預先指定字符數組的大小為常量,我們就要接受猜
錯許多次的事實。
避免猜錯的唯?辦法就是動態決定串的大小。因此,譬如我們希望可以這樣寫:
char*helloworld;
helloworld-coiicalenale(hello,world);〃有陷阱
讓concatenate函數負責判斷包含變量hello和world的連接所需內存的大小、分配這
樣大小的內存、形成連接以及返回一個指向該內存的指針等所有這些工作。實際上,這正
是我在ASD的最初的C版本中所做的事情:我采用了一個約定,即所有串以及類似串的值
的大小都是動態決定的,相應的內存也是動態分配的。然而什么時候釋放內存呢?
對于C的串庫來說無法得知程序員何時不再使用串了。因此,庫必須要讓程序員負責決
定何時釋放內存。一旦這樣做了,我們就會有很多方法來用C實現動態串。
對于ASD,我采用了3個約定。前兩個在C程序中是很普遍的,第三個則不是:
1.串由一個指向它的首字符的指針來表示。
2.串的結尾用一個空字符標識。
3.生成串的函數不遵循用于這些串的生命期的約定。例如,有些函數返回指向靜態緩沖
區的指針,這些靜態緩沖區要保持到這些函數的下一次調用;而其他函數則返回指向調用
者要釋放的內存的指針。這些串的使用者需要考慮這些各不相同的生命周期,要在必要的
時候使用free來釋放不再需要的串,還要注意不要釋放那些將在別的地方自動釋放的串。
類似“hello”的字符串常量的生命周期是沒有限制的,因此,寫:
char*hello;
hello=〃hel】o〃;
后不必釋放變量hello0前面的concatenate函數也返回一個無限存在的值,但是由于
這個值保存在自動分配的內存區,所以使用完后應該將它釋放。
最后,有些類似getfield的函數返回一個生存期經過精心定義的但是有限的值。甚至
不應該釋放getfield的值,但是如果想要將它返回的值保存一段很長的時間,我就必須
記得將它復制到時間稍長的存儲區中。
為什么要處理3種不同的存儲期?我無法選擇字符串常量:它們的語義是C的一部分,
我不能改變。但是我可以使所有其他的字符串函數都返回一個指向剛分配的內存的指針。
那么就不必決定要不要釋放這樣的內存了:使用完后就釋放內存通常都是對的。
不讓所有這些字符串函數都在每次調用時分配新內存的主要原因是,這樣做會使我的程
序十分巨大。例如,我將不得不像下面這樣重寫C程序代碼段(見1.3.1節):
/*讀取八進制文件*/
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);
palh-Irafisname(s);
free(s);
/*直到行尾*/
geteol(tf);
看來我還應該有一些其他的可選工具來減小我所寫程序的大小。
使用C++修改ASD與用C修改相比較,前者得到的程序更簡短,而所依賴的常規更少。
作為例子,讓我們回顧C++ASD程序。該程序的第一句是為m.s賦值:
m.s=domach(m.p,dfi1c,m.q);
當然,m.s是結構體m的一個元素,m.s也可以是更大的結構體的組成部分,等等。如
果我必須自己記住要釋放ns的位置,就必然對兩件事情有充分的心理準備。第一,我不
會一次正確得到所有的位置;要清除所有bug肯定要經過多次嘗試。第二,每次明顯地改
變某個東西的時候肯定會產生新的bugo
我發現使用C++就不必再擔心所有這些細節。實際上,我在寫C++ASD時,沒有找到任
何一個與內存分配有關的錯誤。
1.5重復利用的軟件
盡管ASD的C版本里有許多用來處理字符串的函數,我卻從沒有想過要把它們封裝成通
用的包。向人們解釋使用這些函數要遵循哪些規則實在是太麻煩了。而且,根據多年和計
算機用戶打交道的經驗,我知道了一件事,那就是:在使用你的程序時,如果因為不遵守
規則而導致工作失敗,大部分人不會反躬自省,反而會怪罪到你頭上。C可以做好很多事
情,但不能處理靈活多變的字符串。
C++版本的ASDspooler也使用字符一字符串函數,已經有人寫過這些函數,所以我不
用寫了。和我當初發布C字符串規則比起來,編寫這些函數的人更愿意讓其他人來使用這
些C++字符串例程,因為他不需要用戶記住那些隱匿的規定。同樣的,我使用串庠作為例
程的基礎來實現分析文件名所需的指定的模式匹配,而這些例程又可抽取出來用于別的工
作。
此后我用C++編程時,還有過幾次類似的經歷。我考慮問題的本質是什么,再定義一個
類來抓住這個本質,并確保這個類能獨立地工作。然后在遇到符合這個本質的問題時就使
用這個類。令人驚訝的是,解決方法通常只用編譯一次就能工作了。
我的C++程序之所以可靠,是因為我在定義C++類時運用的思想比用C做任何事情時都
多得多。只要類定義正確,我就只能按照我編寫它的初衷那樣去用它。因此,我認為C++
有助于直接表達我的思想并實現我的目的。
1.6后記
這章內容基于一篇專欄文章,從我寫那篇文章到現在已經過去很多年了。在這段時間里,
我很欣慰地看到一整套C+-類庫逐漸形成了。C庫到處都是,但是,可以肯定至少我所見
過的C庫都有一定的問題。而C++則相反,它能實現真正的針對通用目的的庫,編寫這些
庫的程序員甚至根本不必了解他們的庫會用于何處。
這正是抽象的優點。
第2章為什么用C++工作
在第1章中,我解釋了C++吸引我的地方,以及為什么要在編程中使用它。本章將對這
一點進行補充說明。過去的10年時間,我都用在了開發C++編程工具,理解怎樣使用它
們,編寫教授C++的資料,以及修改優化C++標準等工作C++有何魅力讓我如此癡迷呢?
本章中,我將做出解答。這些問題的跨度很大,就像開車上班和設計汽車之間的差距。
2.1小項目的成功
我們很容易就會注意到:很多最成功的、最有名的軟件最初是由少數人開發出來的。這
些軟件后來可能逐漸成長,然而,令人吃驚的是許多真正的贏家都是從小系統做起的。
UNIX操作系統就是最好的例子,C編程語言也是。其他的例子還包括:電子表格、Basic
和FORTRAN編程語言、MS-DOS和IBM的VM/370操作系統。VM/370尤其有趣,因為它完全
是在IBM正規生產線之外發展起來的。盡管IBM多年來一直不提倡客戶使用VM/37O,但
該操作系統仍牢牢占據IBM大型機的主流市場。
同樣令人吃驚的是,很多大項目的最終結果卻表現平平。我實在不愿意在公共場合指手
畫腳,但是我想你自己也應該能舉出大量的例子來。
到底是什么使得大項目難以成功呢?我認為原因在于軟件行業和其他很多行業不一樣,
軟件制造的規模和經濟效益不成正比。絕大多數稱職的程序員能在一兩個小時內寫完一個
100行的程序,而在大項目中通常每個程序員每天平均只寫10行代碼。
2.1.1開銷
有些負面的經濟效益是由于項目組成員之間相互交流需要大量時間。一旦項目組的成員
多到不能同時坐在一張餐桌旁,交流上的開銷問題就相當嚴重了。基于這一點,就必須要
有某種正規的機制,保證每個項目成員對于其他人在做什么都了解得足夠清楚,這樣才能
確保所有的部分最終能拼在一起。隨著項目的擴大,這種機制將占用每個人更多的時間,
同時每個人要了解的東西也會更多。
我們只需要看一下項目組成員是如何利用時間的,就會發現這些開銷是多么明顯:管理
錯誤報告數據庫;閱讀、編寫和回顧需求報告;參加會議;處理規范以及做除編程外的任
何事情。
2.1.2質疑軟件工廠
由于這些開銷是有目共睹的,所以很多人正在尋找減少它的途徑。起碼到目前為止,我
還沒有見過什么有效的方法。這是個難題,我們可能沒有辦法解決。當項目達到一定規模
時,盡管作了百般努力,所有的一切好像還是老出錯;塔科馬海峽大橋和“挑戰者號”航
天飛機災難至今仍然歷歷在目。
有些人認為大項目的開銷是在所難免的。這種態度的結果就是產生了有著過多管理開銷
的復雜系統。然而,更常見的情況是,這些所謂的管理最終不過是另一種經過精心組織的
開銷。開銷還在,只是被放進干凈的盒子和圖表中,因此也更易于理解。有些人沉迷于這
種開銷。他們心安理得地那么做,就好像它是件“好事”一一就好像這種開銷真地能促進
而不是阻礙高效的軟件開發。畢竟,如果一定的管理和組織是有效的,那么更多的管理和
組織就應該更有效。我猜想,這個想法給程序項目引進的紀律和組織,與為工廠廠房引進
生產流水線一樣。
我希望這些人錯了。實際上我所接觸過的軟件工廠給我的感覺很不愉快。每個單獨的功
能都是一個巨大機器的一部分,“系統”控制一切,人也要遵從它。正是這種強硬的控制
導致生產線成為勞資雙方眾多矛盾的焦點。
所幸的是,我并不認為軟件只能朝這個方向發展。軟件工廠忽視了編程和生產之間的本
質區別。工廠是制造大量相同(或者基本相同)產品的地方。它講求規模效益,在生產過
程中充分利用了分工的優勢。最近,它的目標已經變成了要完全消除人力勞動。相反,軟
件開發主要是要生產數目相對較少的、彼此完全不同的人造產品。這些產品可能在很多方
面相似,但是如果太相似,開發工作就變成了機械的復制過程了,這可能用程序就能完成。
因此,軟件開發的理想環境應該不像工廠,而更像機械修理廠一一在那里,熟練的技術工
人可以利用手邊所有可用的精密工具來盡可能地提高工作效率。
實際上,只要在能控制的范圍內,程序員(當然指稱職的)就總是爭取讓他們的機器代
替自己做它們所能完成的機械工作。畢竟,機器擅長干這樣的活兒,而人很容易產生厭倦
情緒。
隨著項目規模越來越大,越來越難以描述,這種把程序員看成是手工藝人的觀點也漸漸
變得難以支持了。因此,我曾嘗試描述應該如何將一個龐大的編程問題當作一系列較小的、
相互獨立的編程問題看待。為了做到這一點,我們首先必須把大系統中各個小項目之間存
在的關系理順,使得相關人員不必反復互相核查。換言之,我們需要項目之間有接口,這
樣,每個項目的成員幾乎不需要關心接口之外的東西。這些接口應該像那些常用的子程序
和數據結構的抽象一樣成為程序員開發工具中的重要組成部分。
2.2抽象
自從25年前開始編程以來,我一直癡迷于那些能擴展程序員能力的工具。這些工具可
以是編程語言、操作系統,甚至可以是關于某個問題的獨特思維方式。我知道有一天我將
能夠輕松解決問題,這些問題是我在剛開始編程時想都不敢想的一一我也知道,我不是獨
自前行。
我最鐘情的工具有一個共性,那就是抽象的概念。當我在處理大問題的時候,這樣的工
具總是能幫助我將問題分解成獨立的子問題,并能確保它們相互獨立。也就是說,當我處
理問題的某個部分的時候,完全不必擔心其他部分。
例如,假設我正在用匯編語言寫一個程序,我必須時常考慮機器的狀態。我可以支配的
工具是寄存器、內存,以及運行于這些寄存器、內存上的指令。要用匯編語言做成任何一
件有用的事情,就必須把我的問題用這些特定概念表達出來。
即使是匯編語言也包含了一些有用的抽象。首先是編寫的程序在機器執行之前先被解釋
了。這就是用匯編語言寫程序和直接在機器上寫程序的區別。更難以察覺的是,對于機器
設計者來說,“內存”和“寄存器”的概念本身就是一種抽象。如果拋開抽象不用,則程
序的運行就要表示成處理器內無數個門電路的狀態變換。如果你的想象力夠豐富的話,就
可以看到除此之外還有更多層次的抽象。
高級語言提供了更復雜的抽象。甚至用表達式替代一連串單獨的算術指令的想法,也是
非常重大的。這種想法在20世紀50年代首次被提出時顯得很不同凡響,以至于后來成了
FORTRAN命名的基礎:FormulaTranslation。抽象如此有用,因此程序員們不斷發明新
的抽象,并且運用到他們的程序中。結果幾乎所有重要的程序都給用戶提供了一套抽象。
2.2.1有些抽象不是語言的一部分
考慮一下文件的概念。事實上每種操作系統都以某種方式使文件能為用戶所用。每個程
序員都知道文件是什么。但是,在大多數情況下,文件根本不是物理存在的!文件只是組
織長期存儲的數據的一種方式,并由程序和數據結構的集合提供支持來實現這個抽象。
要使用文件做任何一件有意義的事情,程序員必須知道程序是通過什么訪問文件的,以
及需要什么樣的請求隊列。對于典型的操作系統來說,必須確保提出不合理請求的程序得
到相應的錯誤提示,而不能造成系統本身崩潰或者文件系統破壞。實際上,現代的操作系
統已經就一個目的達成了共識,就是要在文件之間構筑“防火墻”,以便增加程序在無意
中修改數據的難度。
2.2.2抽象和規范
操作系統提供了一定程度的保護措施,而編程語言通常沒有。那些編寫新的抽象給其他
程序員用的程序員,往往不得不依靠用戶自己去遵守編程語言技術上的限制。這些用戶不
僅要遵守語言的規則,還要遵守其他程序員制定的規范。
例如,由malloc函數實現的動態內存的概念就是C庫中經常使用的抽象。你可以用一
個數字作參數來調用malloc,然后它在內存中分配空間,并給出地址。當你不再需要這
塊內存時,就用這個地址作參數來調用free函數,這塊內存就返回給系統留作它用。
在很多情況下,這個簡單的抽象都相當有用。不論規模大小,很難想象一個實際的C程
序不使用malloc或者free。但是,要成功地使用抽象,必須遵循一些規范。要成功地使
用動態內存,程序員必須:
?知道要分配多大內存。
?不使用超出分配的內存范圍外的內存。
?不再需要時釋放內存。
?只有不再需要時,才釋放內存。
?只釋放分配的內存。
?切記檢查每個分配請求,以確保成功。
要記住的東西很多,而且一不留神就會出錯。那么有多少可以做成自動實現的呢?用
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
- 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 建筑照明設計案例分析
- 中華優傳統文化 課件 第一章 中國傳統文化概述
- 創建平安年終工作總結
- 2025西安交通大學輔導員考試試題及答案
- 2025遼寧建筑職業學院輔導員考試試題及答案
- 中國美食教案設計
- 2025福建農林大學金山學院輔導員考試試題及答案
- 幼兒園天氣主題活動設計
- 江西報業傳媒集團有限責任公司招聘筆試題庫2025
- 字母ABC基礎教學設計
- 附件3:微創介入中心評審實施細則2024年修訂版
- 信創的基礎知識培訓課件
- 全國國道大全(包括里程及路過城市)
- 化學品作業場所安全警示標志大全
- T-QGCML 3384-2024 無人值守地磅收驗貨系統配置規范
- AQ/T 2061-2018 金屬非金屬地下礦山防治水安全技術規范(正式版)
- 道路提升改造、公路工程 投標方案(技術標)
- 《筵席設計與制作》考試復習題庫(含答案)
- DZ/T 0462.6-2023 礦產資源“三率”指標要求 第6部分:石墨等26種非金屬礦產(正式版)
- 交通出行車費報銷單模板
- 中國民族鋼琴藝術鑒賞智慧樹知到期末考試答案章節答案2024年西安交通大學
評論
0/150
提交評論