轉(zhuǎn)載自CSDN: https://blog.csdn.net/st19890625/article/details/38977029
文字稍許修改
關(guān)鍵字: C語言、.h文件亦镶、extern倔约、作用域
C語言中的.h文件和我認(rèn)識由來已久,其使用方法雖不十分復(fù)雜姥份,但我卻是經(jīng)過了幾個月的“不懂”時期绑嘹,幾年的“一知半解”時期才逐漸認(rèn)識清楚他的本來面目稽荧。揪其原因,我的 駑鈍和好學(xué)而不求甚解固然是原因之一,但另外還有其他原因姨丈。原因一:對于較小的項目畅卓,其作用不易被充分開發(fā),換句話說就是即使不知道他的詳細(xì)使用方法蟋恬,項 目照樣進行翁潘,程序在計算機上照樣跑。原因二:現(xiàn)在的各種C語言書籍都是只對C語言的語法進行詳細(xì)的不能再詳細(xì)的說明歼争,但對于整個程序的文件組織構(gòu)架卻只字不提拜马,找了好幾本比較著名的C語言著作,卻沒有一個把.h文件的用法寫的比較透徹的沐绒。下面我就斗膽提筆俩莽,來按照我對.h的認(rèn)識思路,向大家介紹一下乔遮。
讓我們的思緒乘著時間機器回到大學(xué)一年級扮超。C原來老師正在講臺上講著我們的第一個C語言程序: Hello world!
/***************************
例程-1
****************************/
文件名 First.c
main()
{
printf(“Hello world!”);
}
看看上面的程序,沒有.h文件蹋肮。
是的出刷,就是沒有,世界上的萬物都是經(jīng)歷從沒有到有的過程的坯辩,我們對.h的認(rèn)識馁龟,我想也需要從這個步驟開始。這時確實不需要.h文件漆魔,因為這個程序太簡單了屁柏,根本就不需要。那么如何才能需要呢有送?讓我們把這個程序變得稍微復(fù)雜些,請看下面這個:
/***************************
例程-2
****************************/
文件名 First.c
printStr()
{
printf(“Hello world!”);
}
main()
{
printStr();
}
還是沒有, 那就讓我們把這個程序再稍微改動一下.
/***************************
例程-3
****************************/
文件名 First.c
main()
{
printStr();
}
printStr()
{
printf(“Hello world!”);
}
等等僧家,不就是改變了個順序嘛, 但結(jié)果確是十分不同的雀摘。
讓我們編譯一下例程-2和例程-3,你會發(fā)現(xiàn)例程-3是編譯不過的;這時需要我們來認(rèn)識一下另一個C語言中的概念:作用域八拱。
我們在這里只講述與.h文件相關(guān)的頂層作用域, 頂層作用域就是從聲明點延伸到源程序文本結(jié)束, 就printStr()這個函數(shù)來說阵赠,他沒有單獨的聲明,只有定義肌稻,那么就從他定義的行開始清蚀,到first.c文件結(jié)束, 也就是說:在在例程-2的main()函數(shù)的引用點上,已經(jīng)是他的作用域爹谭;例程-3的main()函數(shù)的引用點上枷邪,還不是他的作用域,所以會編譯出錯诺凡。
這種情況怎么辦呢? 有兩種方法 东揣,一個就是讓我們回到例程-2, 順序?qū)ξ覀儊碚f沒什么, 誰先誰后不一樣呢践惑,只要能編譯通過,程序能運行嘶卧,就讓main()文件總是放到最后吧尔觉。
那就讓我們來看另一個例程,讓我們看看這個方法是不是在任何時候都會起作用芥吟。
文件名 First.c
play2()
{
……………….
play1();
………………..
}
play1(){
……………..
play2();
……………………
}
main()
{
play1();
}
*************************
例程-4
也許大部分都會看出來了侦铜,這就是經(jīng)常用到的一種算法, 函數(shù)嵌套, 那么讓我們看看, play1和play2這兩個函數(shù)哪個放到前面呢?
這時就需要我們來使用第二種方法,使用聲明.
文件名 First.c
play1();
play2();
play2()
{
……………….
play1();
………………..
}
play1()
{
…………………….
play2();
……………………
}
main()
{
play1();
}
例程-4
經(jīng)歷了我的半天的嘮叨, 加上四個例程的說明,我們終于開始了用量變引起的質(zhì)變, 這篇文章的主題.h文件快要出現(xiàn)了。
一個大型的軟件項目钟鸵,可能有幾千個钉稍,上萬個play,而不只是play1携添,play2這么簡單嫁盲,這樣就可能有N個類似 play1(); play2(); 這樣的聲明,這個時候就需要我們想辦法把這樣的play1(); play2(); 也另行管理, 而不是把他放在.c文件中, 于是.h文件出現(xiàn)了.
文件名 First.h
play1();
play2();
文件名 First.C
#include “first.h”
play2()
{
……………….
play1();
………………..
}
play1();
{
……………………..
play2();
……………………
}
main()
{
play1();
}
例程-4
各位有可能會說,這位janders大蝦也太羅嗦了,上面這些我也知道, 你還講了這么半天, 請原諒, 如果說上面的內(nèi)容80%的人都知道的話,那么我保證,下面的內(nèi)容烈掠,80%的人都不完全知道羞秤。而且這也是我講述一件事的一貫作風(fēng),我總是想把一個東西說明白,讓那些剛剛接觸C的人也一樣明白左敌。
上面是.h文件的最基本的功能瘾蛋,那么.h文件還有什么別的功能呢?讓我來描述一下我手頭的一個項目吧矫限。
這個項目已經(jīng)做了有10年以上了哺哼,具體多少年我們部門的人誰都說不太準(zhǔn)確,況且時間并不是最主要的叼风,不再詳查了取董。是一個通訊設(shè)備的前臺軟件, 源文件大小共 51.6M, 大小共1601個文件,編譯后大約10M无宿,其龐大可想而知茵汰,在這里充斥著錯綜復(fù)雜的調(diào)用關(guān)系,如在second.c中還有一個函數(shù)需要調(diào)用first.c文件中的play1函數(shù), 如何實現(xiàn)呢?
Second.h 文件
play1();
second.c文件
***()
{
…………….
Play();
……………….
}
例程-5
在second.h文件內(nèi)聲明play1函數(shù)孽鸡,怎么能調(diào)用到first.c文件中的哪個play1函數(shù)中呢? 是不是搞錯了蹂午,沒有搞錯, 這里涉及到c語言的另一個特性:
-
存儲類說明符。
C語言的存儲類說明符有以下幾個, 我來列表說明一下:
|說明符| 用 法|
Auto 只在塊內(nèi)變量聲明中被允許, 表示變量具有本地生存期彬碱。
Extern 出現(xiàn)在頂層或塊的外部變量函數(shù)與變量聲明中豆胸,表示聲明的對象具有靜態(tài)生存期, 連接程序知道其名字。
Static 可以放在函數(shù)與變量聲明中巷疼,在函數(shù)定義時,只用于指定函數(shù)名晚胡,而不將函數(shù)導(dǎo)出到鏈接程序,在函數(shù)聲明中,表示其后邊會有定義聲明的函數(shù),存儲類型static搬泥。在數(shù)據(jù)聲明中,總是表示定義的聲明不導(dǎo)出到連接程序桑寨。
無疑,在例程-5中的second.h和first.h中忿檩,需要我們用extern標(biāo)志符來修飾play1函數(shù)的聲明尉尾,這樣,play1()函數(shù)就可以被導(dǎo)出到連接程序燥透,也就是實現(xiàn)了無論在first.c文件中調(diào)用沙咏,還是在second.c文件中調(diào)用,連接程序都會很聰明的按照我們的意愿班套,把他連接到first.c文件中的play1函數(shù)的定義上去肢藐,而不必我們在second.c文件中也要再寫一個一樣的play1函數(shù)。
但隨之有一個小問題吱韭,在例程-5中吆豹,我們并沒有用extern標(biāo)志符來修飾play1啊, 這里涉及到另一個問題, C語言中有默認(rèn)的存儲類標(biāo)志符。
C99中規(guī)定 :所有頂層的默認(rèn)存儲類標(biāo)志符都是extern理盆。
原來如此啊痘煤,哈哈,回想一下例程-4猿规,也是好險衷快,我們在無知的情況下,竟然也誤打誤撞姨俩,用到了extern修飾符蘸拔,否則在first.h中聲明的play1函數(shù)如果不被連接程序?qū)С觯敲次覀冊谠趐lay2()中調(diào)用他時环葵,是找不到其實際定義位置的调窍。
那么我們?nèi)绾蝸韰^(qū)分哪個頭文件中的聲明在其對應(yīng)的.c文件中有定義,而哪個又沒有呢张遭?
這也許不是必須的陨晶,因為無論在哪個文件中定義,聰明的連接程序都會義無返顧的幫我們找到帝璧,并導(dǎo)出到連接程序,但我覺得他確實必要的湿刽。
因為我們需要知道這個函數(shù)的具體內(nèi)容是什么的烁,有什么功能,有了新需求后我也許要修改他诈闺,我需要在短時間內(nèi)能找到這個函數(shù)的定義渴庆,那么我來介紹一下在C語言中一個人為的規(guī)范:
在.h文件中聲明的函數(shù),如果在其對應(yīng)的.c文件中有定義,那么我們在聲明這個函數(shù)時襟雷,不使用extern修飾符刃滓,反之,則必須顯示使用extern修飾符耸弄。
這樣咧虎,在C語言的.h文件中,我們會看到兩種類型的函數(shù)聲明。帶extern的计呈,和不帶extern的砰诵,簡單明了。
一個是引用外部函數(shù)捌显,一個是自己申明并定義的函數(shù)茁彭。
最終如下:
Second.h 文件
Extern play1();
上面洋洋灑灑寫了那么多都是針對函數(shù)的,而實際上.h文件卻不是為函數(shù)所御用的. 打開我們項目的一個.h文件我們發(fā)現(xiàn)除了函數(shù)外,還有其他的東西, 那就是全局變量.
在大型項目中扶歪,對全局變量的使用不可避免, 比如,在first.c中需要使用一個全局變量G_test, 那么我們可以在first.h中,定義 TPYE G_test. 與對函數(shù)的使用類似, 在second.c中我們的開發(fā)人員發(fā)現(xiàn)他也需要使用這個全局變量, 而且要與first.c中一樣的那個, 如何處理? 對,我們可以仿照函數(shù)中的處理方法, 在second.h中再次聲明TPYE G_test, 根據(jù)extern的用法,以及c語言中默認(rèn)的存儲類型, 在兩個頭文件中聲明的TPYE G_test,其實其存儲類型都是extern, 也就是說不必我們操心, 連接程序會幫助我們處理一切. 但我們又如何區(qū)分全局變量哪個是定義聲明,哪個是引用聲明呢?這個比函數(shù)要復(fù)雜一些, 一般在C語言中有如下幾種模型來區(qū)分:
1理肺、初始化語句模型
頂層聲明中,存在初始化語句是善镰,表示這個聲明是定義聲明妹萨,其他聲明是引用聲明。C語言的所有文件之中媳禁,只能有一個定義聲明眠副。
按照這個模型,我們可以在first.h中定義如下TPYE G_test=1竣稽;那么就確定在first中的是定義聲明囱怕,在其他的所有聲明都是引用聲明。
2毫别、省略存儲類型說明
在這個模型中娃弓,所有引用聲明要顯示的包括存儲類extern,而每個外部變量的唯一定義聲明中省略存儲類說明符岛宦。
這個與我們對函數(shù)的處理方法類似台丛,不再舉例說明。
這里還有一個需要說明砾肺,本來與本文并不十分相關(guān)挽霉,但前一段有個朋友遇到此問題,相信很多人都會遇到变汪,那就是數(shù)組全局變量侠坎。
他遇到的問題如下:
在聲明定義時,定義數(shù)組如下:
int G_glob[100];
在另一個文件中引用聲明如下:
int * G_glob;
在vc中裙盾,是可以編譯通過的实胸,這種情況大家都比較模糊并且需要注意他嫡,數(shù)組與指針類似,但并不等于說對數(shù)組的聲明起變量就是指針庐完。上面所說的的程序在運行時發(fā)現(xiàn)了問題钢属,在引用聲明的那個文件中,使用這個指針時總是提示內(nèi)存訪問錯誤门躯,原來我們的連接程序并不把指針與數(shù)組等同淆党,連接時,也不把他們當(dāng)做同一個定義生音,而是認(rèn)為是不相關(guān)的兩個定義宁否,當(dāng)然會出現(xiàn)錯誤。正確的使用方法是在引用聲明中聲明如下:
int G_glob[100];
并且最好再加上一個extern缀遍,更加明了慕匠。
extern int G_glob[100];
另外需要說明的是,在引用聲明中由于不需要涉及到內(nèi)存分配域醇,可以簡化如下台谊,這樣在需要對全局變量的長度進行修改時,不用把所有的引用聲明也全部修改了譬挚。
extern int G_glob[];
C語言是現(xiàn)今為止在底層核心編程中锅铅,使用最廣泛的語言,以前是减宣,以后也不會有太大改變盐须,雖然現(xiàn)在java,.net等語言和工具對c有了一定沖擊,但我們看到在計算機最為核心的地方漆腌,其他語言是無論如何也代替不了的贼邓,而這個領(lǐng)域也正是我們對計算機癡迷的程序員所向往的。