為什麼C語言會有標頭檔

來源:互聯網
上載者:User

標籤:編譯過程   技術分享   很多   沒有   res   聲明   根據   編譯   表示   

前段時間一個剛轉到C語言的同事問我,為什麼C會多一個標頭檔,而不是像Java和Python那樣所有的代碼都在源檔案中。我當時回答的是C是靜態語言很多東西都是需要事先定義的,所以按照慣例我們是將所有的定義都放在標頭檔中的。事後我再仔細想想,這個答案並不不能很好的說明這個問題。所以我在這將關於這個問題的相關內容寫下來,希望給大家一點提示,也算是一個總結

include語句的本質

要回答這個問題,首先需要知道C語言程式碼群組織問題,也就是我比較喜歡說的多檔案,這個不光C語言有,幾乎所有的程式設計語言都有,比如Python中使用import來匯入新的模組,而C中我們可以簡單的將include等效為import。那麼問題來了,import後面的模組名稱一般是相關類和對象的的的聲明和實現模組,而include後面只能跟一個標頭檔,只有聲明。其實這個認識是錯誤的,C語言並沒有規定include只能包含標頭檔,include的本質是一個預先處理指令它主要的工作是將它後面的相關檔案整個拷貝並替換這個include語句,比如下面一個例子

//add.cppint add(int x, int y){    return x + y;}//main.cpp#include "add.cpp"int main(){    int x = add(1, 2);    return 0;}

在這個例子中我們在add.cpp檔案中先定義一個add函數,然後在main檔案中先包含這個原始碼檔案,然後在main函數中直接調用add函數,項目的目錄結構如下:

在這裡給大家說一個技巧,在VS中右擊項目--->選擇屬性------>C++------>命令列,在編輯框中填入 /P,然後開啟對應的檔案點擊編譯(這裡不能選產生,由於/P選項只會進行預先處理並編譯這一個檔案,其餘.cpp檔案並沒有編譯,選產生一定會報錯)

點擊編譯以後它會在項目的源碼目錄下產生一個與對應cpp同名的.i檔案,這個檔案是預先處理之後產生的源檔案。這個技巧對於調試檢查和理解宏定義的代碼十分重要,我們看到預先處理之後的代碼如下:

int add(int x, int y){    return x + y;}int main(){    int x = add(1, 2);    return 0;}

這段代碼中我把注釋給刪掉了,注釋表示後面的程式碼片段都是來自於哪個檔案的,從代碼檔案來看,include被替換掉了,正是用add.cpp檔案中的代碼替換了,去掉之前添加的/P參數,再次點擊編譯,發現它報錯了,報的是add函數重複定義。因為編譯add.cpp時產生的add.obj中有函數add的定義,而在main檔案中又有add函數的定義。我們將代碼做簡單的改變就可以解決這個問題,最終的代碼如下:

//add.cppint add(int x, int y);#ifndef __ADD_H__int add(int x, int y){    return x + y;}#endif // __ADD_H__//main.cpp#define __ADD_H__#include "add.cpp"int main(){    int x = add(1, 2);    return 0;}

在這段代碼中加了一個宏定義,如果沒有定義這個宏則包含add的實現代碼,否則不包含。然後在main檔案中定義這個宏,表示在main中不包含它的實現,但是不管怎麼樣都需要在add.cpp中加上add函數的定義,否則在調用add函數時會報add函數未定義的變數或者函數

上述寫法的窘境

上面只引入一個檔案,我們來試試引入兩個, 在這個項目中新增一個mul檔案來編寫一個乘法的函數

#define __ADD_H__#include "add.cpp"int mul(int x, int y);#ifndef __MUL_H__int mul(int x, int y){    int res = 0;    for(int i =0; i < y; i++)    {        res = add(res, x);    }    return res;}#endif

上面的乘法函數利用之前的add函數,乘法是多次累加的結果,在上面的代碼中由於要使用add函數,所以先包含add.cpp檔案,並定義宏保證沒有重複定義,然後再寫對應的演算法。最後在main中引用這個函數

#define __ADD_H__#define __MUL_H__#include "add.cpp"#include "mul.cpp"int main(){    int x = add(1, 2);    x = mul(x, 2);    return 0;}

注意這裡對應宏定義和include的順序,稍有不慎就可能會報錯,一般都是報重複定義的錯誤,如果報錯還請使用之前介紹的/P選項來排錯
到這裡是不是覺得這麼寫很麻煩?其實我在準備這些例子的時候也是這樣,很多時候沒有注意相關代碼的順序導致報錯,而針對重複定義的報錯很難排查。而這還僅僅只引入了兩個檔案,一般的項目中幾時上百個檔案那就更麻煩了

標頭檔的誕生

從上面的兩個例子來看,其實我們只需要包含對應的聲明,不需要也不能包含它的實現。很自然的就想到專門編寫一個檔案來包含所有的定義,這樣要使用對應的函數或者變數的時候直接包含這個檔案就可以了,這個就是我們所說的標頭檔了。至於為什麼叫做標頭檔,這隻是一個約定俗成的叫法,而以.h來命名也只是一個約定而已,我們經常看到C++的開源項目中將標頭檔以.hpp命名。這個真的只是一個約定而已,我們也看到了上面的例子都包含的是cpp檔案,它也能編譯過。
其實針對所有的變數、類、函數可以都在統一的標頭檔中聲明,但是這麼做又帶來一個問題,如果我要看它的實現怎麼辦,那麼多個檔案我不可能一個個的找吧。所以這裡又有一條約定,每個模組都放在統一的cpp檔案中而該檔案中相關內容的聲明則放到與之同名的標頭檔中

其實我覺得這個原則在所有靜態、需要區分聲明和實現的語言應該是都適用的,像我知道的組合語言,特別是win32 的宏彙編,它也有一個標頭檔的思想。

C語言編譯過程

在上面我基本上回答了為什麼需要一個標頭檔,但是本質的問題還是沒有解決,為什麼像Python這類動態語言也有對應模組、多檔案,但是它不需要像C那樣要先聲明才能使用?
要回答這個問題需要瞭解一點C/C++的編譯過程。
C/C++編譯的時候先掃描整個檔案有沒有語法錯誤,然後將C語句轉化為彙編,當碰到不認識的變數、類、函數、對象的命名時,首先尋找它有沒有聲明,如果沒有聲明直接報錯,如果有,則根據對應的定義空出一定的儲存空間並進行相關的指令轉化:比如給變數賦值時會轉化為mov指令並將、調用函數時會使用call指令。這樣就解釋了為什麼在聲明時指定變數類型,如果編譯器不知道類型就不知道該用什麼指令來替換C代碼。同時會將對應的變數名作為符號保留。然後在符號表(這個符號表時每個代碼檔案都有一個)中填入該檔案中定義的相關內容的符號以及它所在的首地址。最終如果未發生錯誤就產生了一個對應的.obj檔案,這就是編譯的基本過程。
編譯完成之後進行連結,首先掃描所有的obj檔案,先尋找main函數,然後根據main函數中代碼的執行流程來一一組織代碼結構,當碰到之前保留的符號時,去所有的obj中的符號表中根據變數符號尋找對應的地址,當它發現找到多個地址的時候就會報重複定義的錯誤。如果未找到對應的符號就會報函數或者變數已經聲明但是未定義。找到之後會將之前obj中的符號替換為地址,比如將 mov eax num 替換成 mov eax, 0x00ff7310這樣的指令。最終產生一個PE檔案。
根據上面的編譯過程來看,它事先會掃描檔案中所有的變數定義,所以必須讓編譯器知道這個變數是什麼。而Python是邊解釋邊執行,所以事先不需要聲明,只要執行到該處能找到定義即可。它們這點區別就解釋了為什麼C/C++需要聲明而Python不用。

為什麼C語言會有標頭檔

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.