在C語言的應用領域,如通訊領域和嵌入式系統領域,一個的軟件項目通常包含很多復雜的功能,實現這個項目不是一個程序員單槍匹馬可以勝任的,往往需要一個 團隊的有效合作,另外,在一個以C代碼為主的完整的項目中,經常也需要加入一些其他語言的代碼,例如,C代碼和匯編代碼的混合使用,C文件和C++的同時 使用。這些都增加了一個軟件項目的復雜程度,為了提高軟件質量,合理組織的各種代碼和文件是非常重要的。
組織代碼和文件的目的是為了使團隊合作更加有效,使軟件項目有良好的可擴展性、可維護性、可移植性、可裁減、可測試性,防止錯誤發生,提高軟件的穩定性。 通常情況下,軟件項目采用層次化結構和模塊化開發的方法,例如,一個嵌入式軟件項目可能有驅動層,操作系統層,功能層,應用程序層,每一個層使用它的下層 提供的接口,並為它的上層提供調用接口,模塊則是每一個層中完成一個功能的單元,例如驅動層的每一個設備的驅動就是一個模塊,應用層的每個應用程序就是一 個模塊,模塊使用下層提供的接口和同層其他模塊提供的接口,完成特定功能,為上層和同層的其他模塊提供調用接口。
這裡的接口是指一個功能模塊暴露出來的,提供給其他模塊的訪問具體功能的方法。根據C語言的特點,使用*.c文件實現模塊的功能,使用*.h文件暴露單元 的接口,在*.h文件裡聲明外部其他模塊可能是用的函數,數據類型,全局變量,類型定義,宏定義和常量定義.外部模塊只需包含*.h文件就可以使用相應的 功能.當然,模塊可以在細化為子模塊.雖然我們這裡說的接口和COM(通用組件模型)裡定義的接口不同,但是,根據COM裡對接口的討論,為了使軟件在修 改時,一個模塊的修改不會影響到其他模塊的一個模塊的修改不會導致其他模塊也需要修改,所以,接口第一次發布後,修改*.h文件不能導致使用這個接口的其 他模塊需要重新編寫.
根據C語言的特點,並借鑒一些成熟軟件項目代碼,總結C項目中代碼文件組織的基本建議:
使用層次化和模塊化的軟件開發模型.每一個模塊只能使用所在層和下一層模塊提供的接口.
每個模塊的文件包存在獨立的一個文件夾中.通常情況下,實現一個模塊的文件不止一個,這些相關的文件應該保存在一個文件夾中.
用於模塊裁減的條件編譯宏保存在一個獨立的文件裡,便於軟件裁減.
硬件相關代碼和操作系統相關代碼與純C代碼相對獨立保存,以便於軟件移植.
聲明和定義分開,使用*.h文件暴露模塊需要提供給外部的函數,宏,類型,常量,全局變量,盡量做到模塊對外部透明,用戶在使用模塊功能時不需要了解具體的實現,文件一旦發布,要修改一定要很慎重,文件夾和文件命名要能夠反映出模塊的功能.
正式版本和測試版本使用統一文件,使用宏控制是否產生測試輸出。
必要的注釋不可缺少。
理想的情況下,一個可執行的模塊提供一個公開的接口,即使用一個*.h文件暴露接口,但是,有時候,一個模塊需要提供不止一個接口,這時,就要為每個定義 的接口提供一個公開的接口。在C語言的裡,每個C文件是一個模塊,頭文件為使用這個模塊的用戶提供接口,用戶只要包含相應的頭文件就可以使用在這個頭文件 中暴露的接口。所有的頭文件都建議參考以下的規則:
1.頭文件中不能有可執行代碼,也不能有數據的定義,只能有宏、類型(typedef,struct,union,menu),數據和函數的聲明。例如以下的代碼可以包含在頭文件裡:
#define NAMESTRING “name”
typedef unsign long word;
menu{
flag1;
flag2;
};
typedef struct{
int x;
int y;
}
Piont;
extent Fun(void);
extent int a;
全局變量和函數的定義不能出現在*.h文件裡。例如下面的代碼不能包含在頭文件:
int a;
void Fun1(void)
{
a++;
}
2. 頭文件中不能包本地數據(模塊自己使用的數據或函數,不被其他模塊使用)。這一點相當於面向對象程序設計裡的私有成員,即只有模塊自己使用的函數,數據, 不要用extent在頭文件裡聲明,只有模塊自己使用的宏,常量,類型也不要在頭文件裡聲明,應該在自己的*.c文件裡聲明。
3. 含一些需要使用的聲明。在頭文件裡聲明外部需要使用的數據,函數,宏,類型。
4. 防止被重復包含。使用下面的宏防止一個頭文件被重復包含。
#ifndef MY_INCLUDE_H
#define MY_INCLUDE_H
<頭文件內容>
#endif
5. 包含extern "C",使的程序可以在C++編譯器被編譯
#ifdef __cplusplus
extern "C"{
#endif
<函數聲明>
#ifdef __cplusplus
}
#enfif
被extern "C"修飾的變量和函數是按照C語言方式編譯和連接的;未加extern “C”聲明時的編譯方式,作為一種面向對象的語言,C++支持函數重載,而過程式語言C則不支持。函數被C++編譯後在符號庫中的名字與C語言的不同。
例 如,假設某個函數的原型為:void foo( int x, int y );
該函數被C編譯器編譯後在符號庫中的名字為_foo,而C++編譯器則會產生像_foo_int_int之類的名字(不同的編譯器可能生成的名字不 同,但是都采用了相同的機制,生成的新名字稱為“mangled name”)。
_foo_int_int這樣的名字包含了函數名、函數參數數量及類型信息,C++就是靠這種機制來實現函數重載的。例如,在C++中,函 數void foo( int x, int y )與void foo( int x, float y )編譯生成的符號是不相同的,後者為_foo_int_float。 同樣地,C++中的變量除支持局部變量外,還支持類成員變量和全局變量。用戶所編寫 程序的類成員變量可能與全局變量同名,我們以"."來區分。而本質上,編譯器在進行編譯時,與函數的處理相似,也為類中的變量取了一個獨一無二的名字,這 個名字與用戶程序中同名的全局變量名字不同。加extern "C"聲明後的編譯和連接,強制C++連接器按照C編譯器產生的符號_foo鏈接。
6. 保證在使用這個頭文件時,用戶不用再包含使用此頭文件的其他前提頭文件,即要使用的頭文件已經包含在此頭文件裡。例如:area.h頭文件包含了面積相關 的操作,要使用這個頭文件不需同時包含了關於點操作的頭文件piont.h。用戶在使用area.h時不需要手動包含piont.h,因為我們已經在 area.h中用#include “point.h”包含了這個頭文件。
有一些頭文件是為用戶提供調用接口,這種頭文件中聲明了模塊中需要給其他模塊使用的函數和數據,鑒於軟件質量上的考慮,處理參考以上的規則,用來暴露接口的頭文件還需要參考更多的規則:
一個模塊一個接口,不能幾個模塊用一個接口。
文件名為和實現模塊的c文件相同。abc.c--abc.h
盡量不要使用extern來聲明一些共享的數據。因為這種做法是不安全的,外部其他模塊的用戶可能不能完全理解這些變量的含義,最好提供函數訪問這些變量。
盡量避免包含其他的頭文件,除非這些頭文件是獨立存在的。這一點的意思是,在作為接口的頭文件中,盡量不要包含其他模塊的那些暴露*.C文件中內容的頭文件,但是可以包好一些不是用來暴露接口的頭文件。
不要包含那些只有在可執行文件中才使用的頭文件,這些頭文件應該在*.c文件中包含。這一點如同上一點,為了提高接口的獨立性和透明度。
接口文件要有面向用戶的充足的注釋。從應用角度描述個暴露的內容。
接口文件在發布後盡量避免修改,即使修改也要保證不影響用戶程序。
多個代碼文件使用一個接口文件:這種頭文件用於那些認為一個模塊使用一個文件太大的情況。對於這種情況對於這種情況在參考上述建議後,也要參考以下建議。
多個代碼文件組成的一個模塊只有一個接口文件。因為這些文件完成的是一個模塊。
使用模塊下文件命名<系統名><模塊名命名>
不要濫用這種文件。
有時候也會出現幾個*.c文件用於共向數據的*.h文件,這種文件的特點是在一個*.c文件裡定義全局變量,而在其他*.c文件裡使用,要將這種文件和用於暴露模塊接口的文件區別。
一個模塊如果有幾個子模塊,可以用一個*.h文件暴露接口,在這個文件裡用#include包含每個子模塊的接口文件。
還有一種頭文件,說明性頭文件,這種頭文件不需要有一個對應的代碼文件,在這種文件裡大多包含了大量的宏定義,沒有暴露的數據變量和函數。這些文件給出以下建議:
包含一些需要的概念性的東西.
命名方式,定義的功能.h
不包含任何其他的頭文件.
不定義任何類型.
不包含任何數據和函數聲明.
上面介紹了C頭文件的一些建議,下面介紹C代碼文件*.c文件的一些建議,*.c文件是C語言中生成匯編代碼和機器碼的內容,要注意以下建議:
命名方式 模塊名.c
用static修飾本地的數據和函數。
不要使用external。這是在*.h中使用的,可以被包含進來。
無論什麼時候定義內部的對象,確保獨立與其他執行文件。
這個文件裡必須包含相應功能函數。
上面介紹了一些C文件組織的建議,用於提高C語言項目的質量,在以後的C項目組織中,學習面向對象和COM的思想,將這些思想加入到C程序中,能夠寫出更 高質量的代碼。上面的建議在具體的項目裡應該靈活運用,附錄裡有*.h頭文件和*.c代碼文件的模版,在工程中可以作為參考。另外,C工程中經常有一些匯 編代碼文件,這些文件也要使有*.h頭文件暴露其中的數據和函數,以便其他*.c文件包含使用。