最近在工作當中遇到了一點小問題,關于C語言頭檔案的應用問題,主要還是關于全局變量的定義和聲明問題.
學習C語言已經有好幾年了,工作使用也近半年了,但是對于這部分的東西的确還沒有深入的思考過.概念上還是比較模糊的,隻是之前的使用大多比較簡單,并沒有牽涉到太複雜的工程,是以定義和聲明還是比較簡單而明了了的.但是最近的大工程讓我在這方面吃到了一點點苦頭,雖然看了别人的代碼能夠很快的改正,但是這些改正背後的原因卻不知道.我想大多數喜歡C語言的程式員應該是和我一樣的,總喜歡去追究程式問題背後的底層原因,而這也恰恰是我喜歡C語言的最根本的原因.
今天看過janders老兄在csdn上的一篇文章後,了解的确加深了很多,而且還學到一些以前不怎麼知道的知識.
現将文章轉載過來,并對文章當中的一些拼寫錯誤做了簡單的糾正,同時對文字及布局做了少許修改.
C語言中的.h檔案和我認識由來已久,其使用方法雖不十分複雜,但我卻是經過了幾個月的“不懂”時期,幾年的“一知半解”時期才逐漸認識清楚他的本來面目。揪其原因,我的驽鈍和好學而不求甚解固然是原因之一,但另外還有其他原因。原因一:對于較小的項目,其作用不易被充分開發,換句話說就是即使不知道他的詳細使用方法,項目照樣進行,程式在計算機上照樣跑。原因二:現在的各種C語言書籍都是隻對C語言的文法進行詳細的不能再詳細的說明,但對于整個程式的檔案組織構架卻隻字不提,找了好幾本比較著名的C語言著作,卻沒有一個把.h檔案的用法寫的比較透徹的。下面我就鬥膽提筆,來按照我對.h的認識思路,向大家介紹一下。
讓我們的思緒乘着時間機器回到大學一年級。C原來老師正在講台上講着我們的第一個C語言程式: Hello world!
檔案名 First.c
main()
{
printf(“Hello world!”);
}
例程-1
看看上面的程式,沒有.h檔案。是的,就是沒有,世界上的萬物都是經曆從沒有到有的過程的,我們對.h的認識,我想也需要從這個步驟開始。這時确實不需要.h檔案,因為這個程式太簡單了,根本就不需要。那麼如何才能需要呢?讓我們把這個程式變得稍微複雜些,請看下面這個,
檔案名 First.c
printStr()
printStr();
例程-2
還是沒有, 那就讓我們把這個程式再稍微改動一下.
例程-3
等等,不就是改變了個順序嘛, 但結果确是十分不同的. 讓我們編譯一下例程-2和例程-3,你會發現例程-3是編譯不過的.這時需要我們來認識一下另一個C語言中的概念:作用域.
我們在這裡隻講述與.h檔案相關的頂層作用域,頂層作用域就是從聲明點延伸到源程式文本結束, 就printStr()這個函數來說,他沒有單獨的聲明,隻有定義,那麼就從他定義的行開始,到first.c檔案結束,
也就是說,在在例程-2的main()函數的引用點上,已經是他的作用域. 例程-3的main()函數的引用點上,還不是他的作用域,是以會編譯出錯. 這種情況怎麼辦呢?有兩種方法 ,一個就是讓我們回到例程-2, 順序對我們來說沒什麼, 誰先誰後不一樣呢,隻要能編譯通過,程式能運作, 就讓main()檔案總是放到最後吧. 那就讓我們來看另一個例程,讓我們看看這個方法是不是在任何時候都會起作用.
play2()
{
……………….
play1();
………………..
}
play1(){
……………..
play2();
……………………
play1();
例程-4
也許大部分都會看出來了,這就是經常用到的一種算法, 函數嵌套, 那麼讓我們看看, play1和play2這兩個函數哪個放到前面呢?
這時就需要我們來使用第二種方法,使用聲明.
play2();
play2()
}
play1()
{
…………………….
play2();
……………………
經曆了我的半天的唠叨, 加上四個例程的說明,我們終于開始了用量變引起的質變, 這篇文章的主題.h檔案快要出現了。
一個大型的軟體項目,可能有幾千個,上萬個play, 而不隻是play1,play2這麼簡單, 這樣就可能有N個類似
play1(); play2(); 這樣的聲明, 這個時候就需要我們想辦法把這樣的play1(); play2();也另行管理, 而不是把他放在.c檔案中, 于是.h檔案出現了.
檔案名 First.h
檔案名 First.C
#include “first.h”
play1();
……………………..
play2();
……………………
各位有可能會說,這位janders大蝦也太羅嗦了,上面這些我也知道, 你還講了這麼半天, 請原諒, 如果說上面的内容80%的人都知道的話,那麼我保證,下面的内容,80%的人都不完全知道. 而且這也是我講述一件事的一貫作風,我總是想把一個東西說明白,讓那些剛剛接觸C的人也一樣明白.
上面是.h檔案的最基本的功能, 那麼.h檔案還有什麼别的功能呢? 讓我來描述一下我手頭的一個項目吧.
這個項目已經做了有10年以上了,具體多少年我們部門的人誰都說不太準确,況且時間并不是最主要的,不再詳查了。是一個通訊裝置的前台軟體, 源檔案大小共 51.6M, 大小共1601個檔案, 編譯後大約10M, 其龐大可想而知, 在這裡充斥着錯綜複雜的調用關系,如在second.c中還有一個函數需要調用first.c檔案中的play1函數, 如何實作呢?
Sencond.h 檔案
sencond.c檔案
***()
…………….
Play();
……………….
例程-5
在sencond.h檔案内聲明play1函數,怎麼能調用到first.c檔案中的哪個play1函數中呢? 是不是搞錯了,沒有搞錯, 這裡涉及到c語言的另一個特性:存儲類說明符.
C語言的存儲類說明符有以下幾個, 我來清單說明一下
說明符
用 法
Auto
隻在塊内變量聲明中被允許,表示變量具有本地生存期.
Extern
出現在頂層或塊的外部變量函數與變量聲明中,表示聲明的對象具有靜态生存期,連接配接程式知道其名字.
Static
可以放在函數與變量聲明中,在函數定義時,隻用于指定函數名,而不将函數導出到連結程式,在函數聲明中,表示其後邊會有定義聲明的函數,存儲類型static.在資料聲明中,總是表示定義的聲明不導出到連接配接程式.
無疑, 在例程-5中的second.h和first.h中,需要我們用extern标志符來修飾play1函數的聲明,這樣,play1()函數就可以被導出到連接配接程式, 也就是實作了無論在first.c檔案中調用,還是在second.c檔案中調用,連接配接程式都會很聰明的按照我們的意願,把他連接配接到first.c檔案中的play1函數的定義上去, 而不必我們在second.c檔案中也要再寫一個一樣的play1函數.
但随之有一個小問題, 在例程-5中,我們并沒有用extern标志符來修飾play1啊, 這裡涉及到另一個問題, C語言中有預設的存儲類标志符.C99中規定,所有頂層的預設存儲類标志符都是extern
.原來如此啊, 哈哈. 回想一下例程-4, 也是好險, 我們在無知的情況下, 竟然也誤打誤撞,用到了extern修飾符, 否則在first.h中聲明的play1函數如果不被連接配接程式導出,那麼我們在在play2()中調用他時, 是找不到其實際定義位置的 .
那麼我們如何來區分哪個頭檔案中的聲明在其對應的.c檔案中有定義,而哪個又沒有呢?這也許不是必須的,因為無論在哪個檔案中定義,聰明的連接配接程式都會義無返顧的幫我們找到,并導出到連接配接程式,
但我覺得他确實必要的. 因為我們需要知道這個函數的具體内容是什麼,有什麼功能, 有了新需求後我也許要修改他,我需要在短時間内能找到這個函數的定義, 那麼我來介紹一下在C語言中一個人為的規範:
在.h檔案中聲明的函數,如果在其對應的.c檔案中有定義,那麼我們在聲明這個函數時,不使用extern修飾符,如果反之,則必須顯示使用extern修飾符.
這樣,在C語言的.h檔案中,我們會看到兩種類型的函數聲明. 帶extern的,還不帶extern的, 簡單明了,一個是引用外部函數,一個是自己生命并定義的函數.
最終如下:
Extern play1();
上面洋洋灑灑寫了那麼多都是針對函數的,而實際上.h檔案卻不是為函數所禦用的. 打開我們項目的一個.h檔案我們發現除了函數外,還有其他的東西, 那就是全局變量.
在大型項目中,對全局變量的使用不可避免, 比如,在first.c中需要使用一個全局變量G_test, 那麼我們可以在first.h中,定義 TPYE G_test. 與對函數的使用類似, 在second.c中我們的開發人員發現他也需要使用這個全局變量, 而且要與first.c中一樣的那個, 如何處理? 對,我們可以仿照函數中的處理方法, 在second.h中再次聲明TPYE G_test, 根據extern的用法,以及c語言中預設的存儲類型, 在兩個頭檔案中聲明的TPYE G_test,其實其存儲類型都是extern,
也就是說不必我們操心, 連接配接程式會幫助我們處理一切. 但我們又如何區分全局變量哪個是定義聲明,哪個是引用聲明呢?這個比函數要複雜一些, 一般在C語言中有如下幾種模型來區分:
1、初始化語句模型
頂層聲明中,存在初始化語句是,表示這個聲明是定義聲明,其他聲明是引用聲明。C語言的所有檔案之中,隻能有一個定義聲明。
按照這個模型,我們可以在first.h中定義如下TPYE G_test=1;那麼就确定在first中的是定義聲明,在其他的所有聲明都是引用聲明。
2、省略存儲類型說明
在這個模型中,所有引用聲明要顯示的包括存儲類extern,而每個外部變量的唯一定義聲明中省略存儲類說明符。
這個與我們對函數的處理方法類似,不再舉例說明。
這裡還有一個需要說明,本來與本文并不十分相關,但前一段有個朋友遇到此問題,相信很多人都會遇到,那就是數組全局變量。
他遇到的問題如下:
在聲明定義時,定義數組如下:
int G_glob[100];
在另一個檔案中引用聲明如下:
int * G_glob;
在vc中,是可以編譯通過的,這種情況大家都比較模糊并且需要注意,數組與指針類似,但并不等于說對數組的聲明起變量就是指針。上面所說的的程式在運作時發現了問題,在引用聲明的那個檔案中,使用這個指針時總是提示記憶體通路錯誤,原來我們的連接配接程式并不把指針與數組等同,連接配接時,也不把他們當做同一個定義,而是認為是不相關的兩個定義,當然會出現錯誤。正确的使用方法是在引用聲明中聲明如下:
并且最好再加上一個extern,更加明了。
extern int G_glob[100];
另外需要說明的是,在引用聲明中由于不需要涉及到記憶體配置設定,可以簡化如下,這樣在需要對全局變量的長度進行修改時,不用把所有的引用聲明也全部修改了。
extern int G_glob[];
C語言是現今為止在底層核心程式設計中,使用最廣泛的語言,以前是,以後也不會有太大改變,雖然現在java,.net等語言和工具對c有了一定沖擊,但我們看到在計算機最為核心的地方,其他語言是無論如何也代替不了的,而這個領域也正是我們對計算機癡迷的程式員所向往的。
好了,看完文章,對與C語言頭檔案的作用應該有了跟多的了解吧,如果這些你原本都知道了,那麼僅當是溫習一下而已,如果原本不知道,那麼恭喜你,現在又學到一些技巧和知識.
對于全局變量的定義和聲明,其實還有另外一個解決的方法,聰明的你可能早已經猜到了:),沒錯,就是用宏定義的技巧實作.比如a.h檔案當中有:
#ifdef AAA
int i=0;
#else
int i;
#endif
那麼,在a.c檔案當中,有如下語句:
......
#define AAA
#include "a.h"
而對于其他的任何包含a.h檔案的頭檔案或者.c源檔案,隻需要直接包含a.h就行了
這樣就可以達到在a.c檔案當中定義變量一次,而在其他的檔案當中聲明該變量的目的.
當然了,你完全可以根據自己的需要來決定在哪個需要包含a.h的檔案當中定義宏AAA,但是我要說的是
在同一個工程的不同的需要包含a.h的檔案當中,你隻能定義AAA一次,否則在連接配接這些目标檔案時會出現
重複定義的錯誤,即使你的單獨目标檔案編譯沒有任何的問題.
當然,這裡說的僅僅是對全局變量的聲明技巧,強烈的推介大家在頭檔案中使用宏定義實作對整個頭檔案的防止重複包含,當然了,這個技巧大多數的c語言程式員都懂.
#ifndef XXX
#define XXX
這樣做會讓你的程式更加穩健,很大程度上減少了不必要的麻煩...
最後給出一點點全局變量使用需要注意的問題,這也僅僅是個建議,或者說一種程式設計習慣 ;)
1) 所有全局變量全部以g_開頭,并且盡可能聲明成static類型.
2) 盡量杜絕跨檔案通路全局變量.如果的确需要在多個檔案内通路同一變量,應該由該變量定義所在檔案内提供GET/PUT函數實作.
3) 全局變量必須要有一個初始值,全局變量盡量放在一個專門的函數内初始化.
4) 如調用的函數少于三個,請考慮改為局部變量實作.