C語言頭文件的真正作用

C語言頭文件的作用
 

最近在工作當中遇到了一點小問題,關於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()

{

     printf(“Hello world!”);

}

main()

{

printStr();

}

     例程-2

 

還是沒有, 那就讓我們把這個程序再稍微改動一下.

 

文件名 First.c

main()

{

printStr();

}

 

 

 printStr()

{

     printf(“Hello world!”);

}

     例程-3

 

等等,不就是改變了個順序嘛, 但結果確是十分不同的. 讓我們編譯一下例程-2和例程-3,你會發現例程-3是編譯不過的.這時需要我們來認識一下另一個C語言中的概念:作用域.

我們在這裏只講述與.h文件相關的頂層作用域, 頂層作用域就是從聲明點延伸到源程序文本結束, 就printStr()這個函數來說,他沒有單獨的聲明,只有定義,那麼就從他定義的行開始,到first.c文件結束, 也就是說,在在例程-2的main()函數的引用點上,已經是他的作用域. 例程-3的main()函數的引用點上,還不是他的作用域,所以會編譯出錯. 這種情況怎麼辦呢? 有兩種方法 ,一個就是讓我們回到例程-2, 順序對我們來說沒什麼, 誰先誰後都可以,只要能編譯通過,程序能運行, 就讓main()文件總是放到最後吧. 那就讓我們來看另一個例程,讓我們看看這個方法是不是在任何時候都會起作用.

文件名 First.c
   play2()
 {

 ……………….

           play1();

 ………………..

 
 }


   play1()

{
    ……………..

           play2();       
    ………………
 }

 

 

main()

{

    play1();

}

例程-4

 

也許大部分都會看出來了,這就是經常用到的一種算法, 函數嵌套, 那麼讓我們看看, play1和play2這兩個函數哪個放到前面呢?

 

這時就需要我們來使用第二種方法,使用聲明.

文件名 First.c

play1();

play2();

play2()

{

 ……………….

 play1();

 ………………..
}


    play1()
 {
     …………………….

 play2();

 ……………………
 }

 

main()

{

play1();

}

例程-4

 

經歷了我的半天的嘮叨, 加上四個例程的說明,我們終於開始了從量變引起的質變, 這篇文章的主題.h文件快要出現了。

一個大型的軟件項目,可能有幾千個,上萬個play, 而不只是play1,play2這麼簡單, 這樣就可能有N個類似 play1(); play2(); 這樣的聲明, 這個時候就需要我們想辦法把這樣的play1(); play2(); 也另行管理, 而不是把他放在.c文件中, 於是.h文件出現了.

 

文件名 First.h

play1();

play2();

 

文件名 First.C

#include “first.h”

play2()

{

 ……………….

 play1();

 ………………..

}


    play1();

{

……………………..

     play2();

  ……………………

}

 

 

main()

{

play1();

}

例程-4

 

各位有可能會說,這位janders大蝦也太羅嗦了,上面這些我也知道, 你還講了這麼半天, 請原諒, 如果說上面的內容80%的人都知道的話,那麼我保證,下面的內容,80%的人都不完全知道. 而且這也是我講述一件事的一貫作風,我總是想把一個東西說明白,讓那些剛剛接觸C的人也一樣明白.

上面是.h文件的最基本的功能, 那麼.h文件還有什麼別的功能呢? 讓我來描述一下我手頭的一個項目吧.

 

這個項目已經做了有10年以上了,具體多少年我們部門的人誰都說不太準確,況且時間並不是最主要的,不再詳查了。是一個通訊設備的前臺軟件, 源文件大小共 51.6M, 大大小小共1601個文件, 編譯後大約10M, 其龐大可想而知, 在這裏充斥着錯綜複雜的調用關係,如在second.c中還有一個函數需要調用first.c文件中的play1函數, 如何實現呢?

 

Second.h 文件

 

play1();

 

 

second.c文件

 

***()

{

…………….

Play();

……………….

}

例程-5

 

在second.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的, 簡單明瞭,一個是引用外部函數,一個是自己生命並定義的函數.

最終如下:

Second.h 文件

 

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中,是可以編譯通過的,這種情況大家都比較模糊並且需要注意,數組與指針類似,但並不等於說對數組的聲明起變量就是指針。上面所說的的程序在運行時發現了問題,在引用聲明的那個文件中,使用這個指針時總是提示內存訪問錯誤,原來我們的連接程序並不把指針與數組等同,連接時,也不把他們當做同一個定義,而是認爲是不相關的兩個定義,當然會出現錯誤。正確的使用方法是在引用聲明中聲明如下:

 

int G_glob[100];

 

並且最好再加上一個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就行了
......
#include "a.h"
......
這樣就可以達到在a.c文件當中定義變量一次,而在其他的文件當中聲明該變量的目的.
當然了,你完全可以根據自己的需要來決定在哪個需要包含a.h的文件當中定義宏AAA,但是我要說的是
在同一個工程的不同的需要包含a.h的文件當中,你只能定義AAA一次,否則在連接這些目標文件時會出現
重複定義的錯誤,即使你的單獨目標文件編譯沒有任何的問題.

當然,這裏說的僅僅是對全局變量的聲明技巧,強烈的推介大家在頭文件中使用宏定義實現對整個頭文件的防止重複包含,當然了,這個技巧大多數的c語言程序員都懂.
#ifndef XXX
#define XXX

#endif
這樣做會讓你的程序更加穩健,很大程度上減少了不必要的麻煩...

最後給出一點點全局變量使用需要注意的問題,這也僅僅是個建議,或者說一種編程習慣 ;)
1) 所有全局變量全部以g_開頭,並且儘可能聲明成static類型. 
2) 儘量杜絕跨文件訪問全局變量.如果的確需要在多個文件內訪問同一變量,應該由該變量定義所在文件內提供GET/PUT函數實現. 
3) 全局變量必須要有一個初始值,全局變量儘量放在一個專門的函數內初始化. 
4) 如調用的函數少於三個,請考慮改爲局部變量實現. 
 

 

 

 

 

 

 

 

 

簡單辦法,先寫完整程序,再把一部分抽出去,抽出去的存到 自己的頭文件裏,在抽出的地方寫 #include ...

 

例如,完整程序(計算平均值):

#include<stdio.h>

 

double mean(double *y, int N){

int i;

double s=0.0;

for (i=0;i<N;i++) s=s+y[i];

s = s / (double) N;

return s;

}

void main()

{

double x[10]={1,2,3,4,5,6,7,8,9,10};

printf("mean = %lf\n", mean(x,10));

}

----------------------------------------------

抽出部分 存入 a_x.h :

double mean(double *y, int N){

int i;

double s=0.0;

for (i=0;i<N;i++) s=s+y[i];

s = s / (double) N;

return s;

}

--------------------------------

 

 在c/c++語言中的頭文件其實是爲了搜尋對應的類型和函數信息的

比如在頭文件中可以聲明一個函數,但這個函數可能定義在任何地方

比如一個靜態庫或者動態導入庫lib中,或者可以直接以原代碼的方式寫在cpp/c文件中頭文件提供的服務叫做類型映射(phototype)

 

函數在c/c++語言中也是一種類型函數在聲明的時候其實僅僅是說明了對應的函數調用協議,函數名稱和參數類型這樣就可以明確的指導編譯器如何創建這個函數對應的調用代碼而尋找這個函數的工作交給了鏈接器注意 編譯器在vc裏邊叫做cl,鏈接器在vc裏邊叫做link。

 

cl負責生成obj,每一個cpp/c文件會生成一個obj文件

這個obj裏邊包含了直接由c/cpp源程序所生成的彙編代碼,這個c/cpp文件需要查找的符號(這個後面再說)

 

link其實和咱們上微機原理彙編課的時候用的Link很像

他負責將每一個obj中的符號查找表中的東西轉換爲一個地址

這個地址就是最後編譯完成後的exe文件的函數對應這個函數的入口地址。

 

符號,就是這個函數或者對象通過編譯器後所產生的名稱

在c語言中一個符號由這個函數或者這個對象的名稱和這個函數的調用協議組成

這也是爲什麼c語言不支持重載的原因

(還記得重載吧?重載就是參數類型或個數不同,

參數和個數都相同的叫做重寫,重寫只能用在類的函數的繼承中)

而c++會在每一個函數的前後添加一堆用於表示這個函數的調用方法所屬類型,名字空間和調用參數類型等的大量字符

用來區分每一個函數

比如一個函數

int Test(); 根據不同的命名方法可能被不同的命名

如果到定義了extern "C" 如下:

extern "C" int Test();

這個函數就被vc編譯器按c語言的方式命名爲 _Test

 其中的前劃線 _表示這個函數的調用協議爲__cdecl

 Test就是這個函數的名稱

如果使用vc編譯器直接編譯這個函數int Test();

他就被當作int __cdecl Test(void); 編譯成 ?Test@@YAHXZ

 其中的? 和 @@YAHXZ都是編譯器加上去的

 ? 和 @@YAH 是用來表示調用協議的

 其中的H爲返回值是int

 X表示沒有參數。

 Z是函數名稱結束脩飾

 

調用協議

指函數的參數傳遞使用的方式

有__cdecl __fastcall __stdcall __thiscall 等

__cdecl 是c語言的調用方式

__fastcall 使用寄存器傳遞參數

__stdcall 使用棧傳遞參數,並且其壓棧順序爲從右到左,由被調用函數來清除棧

__thiscall 是類對象的方法調用方式,這種調用方式不能直接由程序指定

 

如果一個函數的是被實現在cpp或者c文件中的時候,就必須保證這個cpp或者c文件所產生的符號與這個函數的聲明所產生的

符號相同,否則鏈接器在鏈接的時候就會發生無法找到符號的錯誤

 

如何保證這個符號是相同的呢?

只要在cpp或者c文件中包含這個頭文件

或者如果能保證所生成的符號相同則根本就不用h文件也能工作

比如如下程序

 

// 這是main.cpp文件的東西

int Test();

 

int main()

{

 Test();

}

 

// 這是test.cpp文件的東西

int Test()

{

 return 1;

}

這裏就完全沒有用頭文件

 

注意事項:

1。如果是使用了c文件作爲函數的載體而編譯器爲微軟的vc,

需要在h文件中添加extern "C"通知編譯器使用c的命名規則

最好使用判斷

#ifdef __cplusplus

extern "C" {

#endif

 

...

函數聲明

...

 

#ifdef __cplusplus

}

#endif

這樣在c編譯器上也能使用這個頭文件了

2。c/cpp文件與頭文件名稱無關

3。如果一個頭文件中定義了一個結構或者類,那麼在h文件中最好使用防止多次包含的

#ifndef __XXX__H__

#define __XXX__H__

 

...

//類或結構定義

class a

{

 ...

};

...

 

#endif

這裏的XXX就是這個頭文件的名稱,這個名稱是可以隨便起的,只要不起重。

但c/cpp文件中不需要添加這些東西

 

或者如果使用了vc6作爲編譯器它支持

#pragma once

在h文件的最前面寫上這句話就可以保證這個頭文件只會被處理一遍

 

 

程序變:

#include<stdio.h>

#include "a_x.h"

void main()

{

double x[10]={1,2,3,4,5,6,7,8,9,10};

printf("mean = %lf\n", mean(x,10));

}

=============================================

你要是願意隨便抽一塊也可以,例如抽出(也叫 a_x.h):

double mean(double *y, int N){

int i;

double s=0.0;

for (i=0;i<N;i++) s=s+y[i];

s = s / (double) N;

return s;

}

void main()

{

------------------------

程序變:

#include<stdio.h>

#include "a_x.h"

double x[10]={1,2,3,4,5,6,7,8,9,10};

printf("mean = %lf\n", mean(x,10));

}

==============================

語法上,功能上,兩種抽法都可以。但第一種方法較好--程序可讀性好,不易出錯。

 

一般情況下,頭文件裏放 函數原型,全局量聲明 和 函數定義。

 

 

 

 

C語言中的頭文件可以自己寫嗎?

電腦圈圈 發表於 2006-3-17 22:57:00

 

2

推薦

一些初學C語言的人,不知道頭文件(*.h文件)原來還可以自己寫的。只知道調用系統庫函數時,要使用#i nclude語句將某些頭文件包含進去。其實,頭文件跟.C文件一樣,是可以自己寫的。頭文件是一種文本文件,使用文本編輯器將代碼編寫好之後,以擴展名.h保存就行了。頭文件中一般放一些重複使用的代碼,例如函數聲明,變量聲明,常數定義,宏的定義等等。當使用#i nclude語句將頭文件引用時,相當於將頭文件中所有內容,複製到#i nclude處。爲了避免因爲重複引用而導致的編譯錯誤,頭文件常具有

#ifndef   LABEL

#define   LABEL

   //代碼部分

#endif

的格式。其中,LABEL爲一個唯一的標號,命名規則跟變量的命名規則一樣。常根據它所在的頭文件名來命名,例如,如果頭文件的文件名叫做hardware.h,

那麼可以這樣使用:

#ifndef   __HARDWARE_H__

#define   __HARDWARE_H__

 //代碼部分

#endif

這樣寫的意思就是,如果沒有定義__HARDWARE_H__,則定義__HARDWARE_H__,並編譯下面的代碼部分,直到遇到#endif。這樣,當重複引用時,由於__HARDWARE_H__已經被定義,則下面的代碼部分就不會被編譯了,這樣就避免了重複定義。

另外一個地方就是使用include時,使用引號與尖括號的意思是不一樣的。使用引號(“”)時,首先搜索工程文件所在目錄,然後再搜索編譯器頭文件所在目錄。而使用尖括號(<>)時,剛好是相反的搜索順序。假設我們有兩個文件名一樣的頭文件hardware.h,但內容卻是不一樣的。一個保存在編譯器指定的頭文件目錄下,我們把它叫做文件I;另一個則保存在當前工程的目錄下,我們把它叫做文件II。如果我們使用的是#i nclude <hardware.h>,則我們引用到的是文件I。如果我們使用的是#i nclude “hardware.h”,則我們引用的將是文件II。筆者以前就遇到過一個同事問,爲什麼他修改了那個頭文件裏面的內容,好象跟沒有修改一樣?就是因爲他有兩個一樣的頭文件(就像我們剛描述的那樣),他是使用#i nclude<hardware.h>引用的,而他修改時,卻是當前工程所在的目錄下的那個文件。

 

 

 

 

#include 的本質就是把對應的文件直接拷貝到這一行裏面

要理解頭文件,主要是要理解“聲明”

C/C++中,所有使用到得變量、函數、類都要是聲明過得,就是說,要有一行語句來告訴編譯器,我有一個名字叫XXX的???類型的變量(函數、類)。

然後還有一個因素就是,在編譯的時候,程序是按照每個.C或.CPP文件單獨編譯的。

也就是說,對於每個C文件中,如果都用到了同一個函數(比如printf),那麼,我在每個對應文件中寫一遍printf的聲明明顯是很麻煩的。所以我把這個聲明單獨寫了一個文件,爲了區別,我把擴展名記做.h,在需要使用對應的函數(類)的時候,我就不需要去拷貝函數的聲明,而只需要#include對應頭文件就可以了,系統自動幫你拷貝進來——C語言提供的頭文件,按照函數功能分類好了,比如數學函數就都寫在了math.h裏面,一包含就全包含,不管你用沒用到cos()這個函數或者其他什麼。

 

當然,由於.h文件中也可以包含其他.h文件,所以爲了不重複聲明或定義,需要用宏做相應的處理,這個不是要理解的東西,而是照着寫。

 

看到你的補充,在這裏我也補充下答案

對於自己定義的函數,首先,肯定的是,你至少需要在一個C文件中定義它,否則鏈接會出錯。當你想在任何一個文件中使用的時候,你只需要讓這個文件包函數聲明所在的頭文件即可。

具體來說:

a.h中聲明瞭了

int a( int x);

a.c中實現這個函數,需要有類似代碼

#include "a.h"

//.....其他代碼

int a(int x)

{

return x*x;

}

如果在b.c中想使用這個,則只要在b.c中這樣就可以:

#include "a.h"

//....其他代碼

x = a(x);

//...其他代碼


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章