如何寫好C/C++程序

1. 基本認識 

1.1. 計算機科學是"人爲"的科學 

計算機科學是一門新興的科學,它不同於物理、化學、天文等自然科學,它是人爲創造的,它的研究對象也是人爲創造的,是一門人爲科學。自然科學的研究對象是客觀世界,是對未知的探索;發現自然規律,研究並利用自然規律是其根本目的。作爲工程師,要在給定條件下"做得更好",就是說要根據實際情況利用現有條件充分發揮"人爲"的優越性。 

1.2. 軟件經常是理論落後於實踐 

衆所周知,任何一門科學,都有它的理論,都應做到理論與實踐相結合,軟件理所當然應該這樣,然而,實際情況並非如此。軟件是工程,軟件理論落後於實踐的情況比比皆是。比如說,幾乎沒有人將圖靈機理論應用於實際軟件開發,計算機更多的是工程,工程是靠實踐的,工程師要始終堅持"身體力行",不要忽視寫程序,要從實際應用中去領悟軟件的真諦。 

1.3. 編程語言不是目的 

首先,語言是工具,編程語言同樣是工具;其次,語言不是目的,學習語言的目的就是爲了更好地表達人的意思,而不是爲了學習而學習。因此,對於工程師來說,不應把學習幾門編程語言作爲最終目的,而是要多快好省的"使用工具"實現自己的目的,開發出優秀的軟件。 

1.4. 馮·諾伊曼模型至今顛撲不破 

存儲程序理論至今已有五十年曆史,馮·諾伊曼模型今後仍是計算機的基本模型,非馮模型不是不可能,但卻越來越遙遠。因此,工程師要恪守"規矩成方圓"的準則,好的工程師要會用工具,要會用"尺"來衡量,這裏的"尺"就是馮·諾伊曼機的內存,做工程時一定要用"尺"量,儘量減小誤差,編寫程序不能違背計算機的基本模型,掌握語言,要做到知其然更知其所以然,利用語言操作計算機硬件,使其有效工作,纔是最終目的。 


北京科泰世紀科技有限公司 版權所有 2000年--2002年 


2. C/C++編程中應該特別注意的一些問題 

下面通過變量、棧、堆在設計中如何使用,來討論如何寫好C/C++程序(所有例題答案附在全文後) 

2.1. 全程、局部、動態變量 

例1:請回答下面程序中註釋行中提出的問題 
int a; // 內存中佔多少字節?分配在哪一區? 
int b = 1; // b 與 a 是否相連?什麼是數據區? 

void main() 

int *p; // 分配在哪一區? 
p = (int *)malloc(100); // 100 字節在哪? 
… 


2.2. 內存空間 

不清楚內存就寫不了好程序,因此對內存的理解,對工程師來說至關重要。(對於本公司的工程師,搞不清楚內存,程序不可以check in) 

下圖是內存空間的簡單邏輯結構示意圖,低(零)地址在下面,高地址在上面。 

· 用戶禁區:是從零地址開始的一段空間,嚴禁用戶使用,保護系統空間; 
· 程序代碼區:用來存儲當前運行程序的源代碼; 
· 全程數據區:存儲全程變量,大小不受限制; 
· 堆:堆是內存中可以動態分配的一片空間,用於存放程序運行過程中動態產生的變量,它是從低地址向高地址生長的,可以是鏈式結構,原則上講只要內存中有剩餘空間,堆就可以動態增長下去; 
· 棧:是從高地址向低地址方向生長的存儲區,它的實現有特殊性,是後進先出的,並且它的大小是受限的,使用中具有靈活,速度快,不需要人工釋放,不存在競爭冒險問題等優點; 
· 動態連接庫代碼;動態連接庫數據;每個動態連接庫又有它的堆、棧。 

例2:一個關於指針入棧的問題。 
void foo(char * p) {p = "世界";} 
void main() { 
char *p = "你好"; 
foo(p); 
printf("%s/n", p); // 輸出什麼? 

此題要求察看反彙編代碼,建議使用Microsoft的compiler。並且要求瞭解編譯後產生的所有文件,至少知道它們都是做什麼的,有什麼用途。 

例3:請回答下面程序中註釋行中提出的問題。 

int a[10000000]; // 是否出錯? 
void main() { 
int a[10000000]; // 是否出錯? 
int *a = (int *) malloc(10000000); // 是否出錯? 
*a = 1; 
printf("%d/n", *a); 


例4:關於堆使用的問題。 

void main() { 
char *p = (char *) malloc(10000000); 
char *q = new char[10000000]; 
…… 
free(p); // 釋放多少內存? 
delete q; // 釋放多少內存? 

2.3. C/C++ Calling Conventions(調用習慣) 
調用習慣決定編譯程序編譯源文件時,如何處理函數調用時傳遞參數的壓棧次序,由誰(調用者還是被調用者)負責彈棧等。(參見MSDN) 
· __cdecl Caller pops stack (C) 
· __stdcall Callee pops stack (C++) 
· __fastcall ECX passes this pointer (優化C) 
__cdecl 是C/C++函數調用的默認形式,由調用函數清除棧,生成的可執行代碼包含清除棧的部分,因此,對於同樣的函數,它比__stdcall形式調用的可執行代碼長。 
__stdcall通常用於Win32 API函數調用,由被調用函數清除棧,採用這種方式調用,要有函數原型。 
__fastcall用於優化的C,只要可能,就使用寄存器,速度快。 


北京科泰世紀科技有限公司 版權所有 2000年--2002年 


3. 程序運行基本概念 

3.1. 主程序/子程序(EXE) 

二進制文件,可以在機器上直接運行。 

3.2. 程序庫/模版庫(LIB) 

對於一些常用的函數,如printf、strcpy等,把他們編成庫函數,由使用者調用,減少重複勞動和出錯的可能,但編譯後代碼長度並沒有變小。 

3.3. 動態鏈接庫(DLL) 

當多個進程都需要調用某個函數時,爲了節省內存空間把這些函數編成動態連接庫,由多個進程動態共享。 
在選擇使用LIB還是DLL時,要考慮應用中具體情況,比如說多少進程共享一個DLL合適,效率如何等等,更具實際做出權衡。另外,DLL也有其缺點,例如不同版本DLL的兼容性不可能做到完美。 


北京科泰世紀科技有限公司 版權所有 2000年--2002年 


4. 面向對象程序設計 

在面向對象的程序設計中,我們常涉及到的是封裝、繼承、多態、類、映射等特性。 

4.1. 向誰最重要 

面向對象,面向誰最爲重要。這個問題可以從三個方面來考慮。首先,也是最重要的,要面向使用者,因爲軟件的最終目的是滿足實際應用;其次,要面向開發者,開發者可能會使用這個對象去構造更大的對象,實現更多的功能;最後,要面向機器,對象最終必須能在馮·諾伊曼機器上運行,設計中必須考慮運行效率、軟件模型、實際誤差等問題。 

4.2. 封裝 (Encapsulation) 

C++中有public、protected、private關鍵字,這樣就可以叫封裝了。C++中封裝是對程序員的限制,沒有實現真正意義上的封裝。 
下面的例子是兩種類的聲明。 
class A { class B { public:   
public: virtual foo(); 
   foo();    virtual bar(); 
protected:       virtual foobar(); 
bar(); } 
private: 
foobar(); 
int a; 

class B的虛表(vtable)的物理存儲結構如下圖: 

如果將上例class A中protected改爲public,比較前後兩種情況下編譯之後的結果,就會發現它們是一樣的,也就是說在源文件中的不同之處編譯後在內存中是沒有反映的,既然沒有反映,就談不上真正的區別,所以,這裏的所謂封裝只是對程序員自身的一種約束,沒有真正的意義。 

4.3. 繼承(Inheritance) 

繼承是指能夠直接獲得已有的性質和特徵,而不必重複定義。是子類自動的共享基類中定義的數據和方法的機制。 
C++ Base Class的程序複用(編譯時的靜態複用) 

_繼承的實質就是少寫代碼,節省打字時間,減少出錯可能,它只是程序複用,是一種靜態機制。因爲,代碼是自己寫的還是從別的類中繼承的,反映在內存中是一樣的,沒有區別,所以,繼承只是在工程上有意義,更多是面向開發者,工程師可以利用這個方法提高開發效率,對使用者而言,則毫無意義。 

COM Aggregation(聚合) 

組件複用(運行時的動態複用)、相對靈活構造。(參見COM編程技術) 

4.4. 多態(Polymorphism) 

多態一詞來源於希臘語,簡單說就是有許多形態,它增加了面向對象軟件系統的靈活性,C++中使用虛函數實現,是C++中最重要的概念。例5中class B的函數前加上virtual關鍵字之後,在這個數據結構中前4個字節保留下來,形成運算表(虛函數表vtbl),運算表中存放函數的入口地址,通過指針訪問,形成間址結構,如下圖所示。只要通過指針訪問這個表(通過這層間址),調用不同的函數,從前的很多程序問題都可迎刃而解。 

UNIX 的驅動程序模型就是用的這種結構,取得了巨大的成功。如: 
_open(); close(); read(); write(); ioctl(); 

4.5. 類(Class) 類廠(ClassFactory) 

類是對具有相同屬性和行爲的一個或多個對象的描述,它是建立對象時使用的"樣板"。C++中類的概念是假的,它的類在編譯之後,到內存中沒有實體與其對應,因此C++不存在真正的類的概念。而smalltalk中類是一種特殊的對象,對象是一種特殊的類,對象這種數據結構是佔內存的,所以類也是佔內存的,這樣纔是真正的類。 

類的概念有多重要性是: 

_第一,對象要有生死的概念(lifetime),對象從哪裏來,由類來控制,Microsoft則叫它類廠,強調產生類、產生對象這個概念。 

第二,類引出映射的概念。例如整數集合上的加、減、乘、除這四個運算,它們是跟整數類相對應的,按照馮·諾伊曼的存儲程序理論,程序就是數據,所以它們既是運算也是數據,作爲數據應該放在整數類中,放在類中有什麼用呢?這裏引出映射的概念,來解答這個問題。 

4.6. 映射(Reflection) 

下面 通過對加法運算的兩種不同情況闡述映射的概念。 

· 普通的32位加法,如果溢出了,怎麼辦?可不可以做64位加呢?既然是數,就能改,可以在運行過程中(動態)把一個32位數用一個PUSH語句入棧,再把另一個也壓入棧中,這樣32位加法運算轉變爲64位加法運算,問題解決了。同樣的加、減、乘、除這四個運算,可以映射出不同的運算實現來,32位運算,64位運算是兩種不同的實現,這種不同的實現就是對同一個描述的不同映射。 

· 網絡上的運算如何實現?同樣還是加、減、乘、除運算,在網上,計算可以在本地進行,也可以在遠程進行。比如說會做加法的進程在遠程,這時本地進程將運算數據打包發給遠程進程,遠程進程計算後將結果打包送回。本地和遠程,這是兩種不同的實現方式,對於設計者來說,本地計算方式是用一段代碼進行計算,遠程計算方式是用一段代碼與遠程進行通信,但對於用戶來說都是加法,除了時間上可能有差別外,其它完全一樣,這是完全透明的網絡運算。這就是映射。 
C++中不存在真正的類,更不存在映射的概念。然而,類和映射的概念至關重要,要提到一個新的高度來認識。C++源程序編譯後的目標代碼是 .obj,而JAVA源程序編譯後目標代碼是 .class,JAVA想說明運算可以在運行中改,只有有了這個前提才能真正做到完全透明的網絡運算。認識到這一點,舉一反三,JAVA還能把32位加法動態提升爲64位加法等等。如果把這種思想應用於軟件設計,那麼是不是能做到他人無法做到的事情呢? 

映射(Reflection)這個概念怎麼強調也不過分,它是程序設計的一個里程碑,從結構化設計到面向對象,有了多態這個概念,程序設計靈活了,現在又有了映射,程序設計更加靈活,人們走入了一個新境界。 


北京科泰世紀科技有限公司 版權所有 2000年--2002年 


5. C++、COM、COM+程序設計的比較 

5.1. C++ 

程序模型 
C++是面向對象編程的,它的模塊是靜態的,鏈接後不可分割,這是C++的最主要的缺點,它不能動態升級。 

模塊實現 
C++將運算與數據結合起來,放在類中,通過繼承實現程序重用,採用二進制標準,將不同模塊聯繫起來實現不同功能。 
C++最關鍵的技術要點就是它加了一個vtbl(如下圖),它帶來很多好處,這在前面已經介紹過了,與原來的程序設計語言相比已經是一個跨越式的發展。 

這裏區分兩個概念,語言和思想。C++是一種語言,是一種非常好的語言。它表達設計者的思想最容易、最直觀,但它或多或少的帶有設計者的歷史侷限性,語言雖然還是好語言,但如今思想的侷限性卻越來越明顯。 

5.2. COM 

1993年6月,Microsoft發佈了COM標準,主要是認識到了C++的不足。另外,並不是只有Microsoft這樣認爲,包括IBM發明Smalltalk、Digital發明CORBA、SUN發明JAVA,也都是認識到了這一點。在美國,所有大軟件公司的大的工程項目很少直接採用C++的編程思想,而是採用它們自己的編程思想,實現軟件設計。 

COM解決了C++做不到的不同來源的組件之間的互操作,使某個組件升級時不影響其他組件,並且獨立於編程語言,實現了組件在進程內、跨進程甚至於跨網絡運行時對用戶的透明性。 

程序模型 

COM解決了C++做不到的不同來源的組件之間的互操作,使某個組件升級時不影響其他組件,並且獨立於編程語言,實現了組件在進程內、跨進程甚至於跨網絡運行時對用戶的透明性。 

模塊實現 

在C++模型基礎上增加了Interface ptr,目的就是要實現動態升級。舉例說明這個問題,例如下圖中已經有了兩個域,如果需要增加域怎麼辦?前面講過,棧有很多好處,要充分利用它,但是,如果一個數據結構放在棧上,這塊空間一旦分配,它是不可以更改的,這個模塊也就不可升級了,所以任何語言(JAVA、COM、C#等),它的組件、構件不可以生成在棧上,這是一個基本原則;然而,"指針,放之四海而皆準",可以在棧上放一指針(Interface ptr)來解決這個問題,(這個指針在JAVA中不是一個物理指針,是一個虛的,叫handle,相當於一個標識符,能訪問到就可以了),理論上講,這是增加一層間址,解決了組件動態可替換問題。 

註冊數據庫 

C++語言中使用new在堆上動態分配內存。例如,用new操作動態生成一個class,new是知道它所要分配的內存的大小的,既然知道大小,就沒有辦法升級。這個信息new是不該知道,這也正是C++當初違背封裝原則的一個體現;即使new不知道分配空間的大小,也要有一套協議,來查詢誰來支持這個class,誰來完成new操作。要想使模塊可以升級,就不能再用new。 
在COM中,使用CoCreateInstance(),本公司的ezCOM使用NEW_COMPONENT,實際上是重載了new。爲了在"new"的時候,知道class現在的大小、在什麼位置、由誰支持,需要在運行過程中創建一個數據庫,通過間址的方式查詢這個數據庫,獲得關於class的信息,之後才能創建一個class,只有這樣才能實現動態升級。創建class時要用一層間址,調用時同樣用一層間址,這兩方面構成了COM的基本內涵。 

元數據庫(TypeLib) 

COM,93年出現的時候,它已經提出了元數據的概念,但當時沒有給予強調,隨着Internet時代的到來,94、95年出現了browsor,SUN發明了JAVA,這兩件事大大加速了人們對軟件的理解。元數據已經應用在COM、JAVA和腳本語言中,但COM並沒有將元數據提高到一個無所不能的高度來理解,JAVA的設計者則不同,他把元數據提高到一個高層次,通過 .class便可一目瞭然。如何寫程序進入了嶄新的階段。 

元數據(Metadata)是定義存儲在數據庫中數據的形式的數據,可認爲是關於數據的數據。元數據是對運算的描述,比如對整數集上的加、減、乘、除這四個運算的描述就稱爲整數集合(CLASS)上的Class Information,既爲Metadata。CDL文件對ezCOM來說就是元數據,TLB是CDL的二進制表現,二者表達的信息是一樣的。 

構件(Component) = 對象(Object) + 元數據(Metadata),構件是由兩部分組成的,一部分是對象,一部分是元數據,兩者打包在一起構成一個dll文件的構件。構件在物理上與對象是不同的,強調一點,如果在物理上不同(內存中反映不同),在現實中就會有不同的體現。正是因爲這個原因,本公司的ezCOM與C++截然不同,如果認爲ezCOM與C++完全一樣,或者就是用C++來編寫的,那就完全錯了。 

程序應該用零件來構造,強調元數據的重要性,這是新的程序設計理念。 

5.3. 關於自動化/自行化的概述 

腳本語言 

腳本語言(script),英文願意是手跡、手稿、副本的意思。腳本語言是解釋執行的,就相當於舞臺上話劇演員按着劇本的內容演出一樣,要一句一句的來。 

模型/顯示/控制 (MVC)編程方式 

本公司的圖形系統就是通過自動化來實現的,Model View Controller這個概念是smalltalk提出的,Windows、X-Windows都沒有按照面向對象、按照這套思路來做,JAVA、.NET都是按照這一套思路來做的,本公司也這樣做。 

代理組件自動生成 

自動遠程通訊主要通過自動化,通過元數據實現。(參見…) 

自描述數據結構 

雖然本公司的產品開發使用C++,但是在所有的接口函數上,參數都必須使用自描述數據結構,這也是對C++的限制,目的就是要動態生成中間件,通過映射動態生成不同的程序實現。能不能實現動態替換,很大程度上是通過元數據和自描述數據結構來完成的。 

本公司的自描述數據結構如下(參見基礎數據類型文檔): 
INT,LONG,CHAR,etc. 
EzStr,EzByteBuf,etc. 
EzIntArray,EzStrArray,etc. 
EzVariant,EzDelegate,etc. 

5.4. COM+ 

a. COM+ 程序模型 

代理(虛擬)組件 

COM+比COM更加重視元數據,有了元數據之後,達到的新境界,就是操作系統可以動態生成代理組件(由系統生成的組件就是中間件)。 

在用戶程序與組件模塊之間插入代理組件帶來很多好處。Windows2000的COM+就是在強調這一點。代理組件,"薄"的時候可以什麼都沒有,就是一層間址,速度不會受損。它的靈活性是指可以動態替換零件,完成不同的功能,比如可以把一個圖形軟件放入內核或放到其它機器上等等,對用戶沒有影響;再如使用ORACLE時,假如有100個用戶,可是隻買了10個版權,這時可以通過代理組件來給每個用戶分組件,實現動態共享;還比如說,在用戶通訊的過程中,可以通過代理組件中零件的替換實現呼叫轉移,接入Internet時,可以通過不同的零件加密、監控等等,這些都是加上代理組件的優越性。 

COM+ 模塊實現 

在COM的基礎上,再增加一層間址,定義運行環境 (Context),變爲多層間址。工程師只要理解內存,學會用"尺"後,就會發現這種多重間址的妙處,程序設計就會變得非常靈活。 

b. 組件運行環境對用戶透明 

(詳細請參見其他相關文檔) 


北京科泰世紀科技有限公司 版權所有 2000年--2002年 



6. COM技術要點及編程(詳細請參見COM編程相關書籍) 

1、 面向接口,可改變程序實現 

2、 二進制標準 (無虛擬機) 

3、 計數器控制生命週期 

4、 動態模塊構造 

5、 元數據支持解釋程序 

6、 自描述數據結構 

7、 自動遠程通訊 


北京科泰世紀科技有限公司 版權所有 2000年--2002年 


7. 例題解答 

例1: 
int a; // 佔4個字節,在全程數據區;在_BSS段,可參考cod文件 
int b=1; // 佔4個字節,在全程數據區;與 a 不相連,在_DATA段 

void main() { 
int * p; // 分配在棧上 
p = (int *) malloc (100); // 100 字節在堆上 
…… 


例2:你好 

例3: 
int a[10000000]; // 不出錯 

void main() { 
int a[10000000]; 
// 出錯,棧的大小有限制,但可通過編譯器開關修改棧的大小 
int *a = (int *) malloc(10000000); 
// 在堆上只要硬件上有足夠的內存,是不出錯的,(堆可動態增長) 
*a = 1; 
printf("%d/n", *a); 


例4:(堆) 
void main() { 
char *p = (char *) malloc(10000000); 
char *q = new char[10000000]; 
…… 
free(p); 
// 釋放10000000字節的內存,malloc在堆上分配10000000字節的內 
// 存,不會自動釋放,需要用free來釋放 
//delete q; 
// 釋放10000000字節的內存,new 在堆上分配10000000字節的內存 
發佈了0 篇原創文章 · 獲贊 0 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章