2011-05-05 90 views
42

我已經詳細閱讀了可能的重複,但是沒有一個答案有下沉C中的頭文件和源文件如何工作?

TL;博士:在C如何相關的源文件和頭文件?項目在構建時隱式地清理聲明/定義依賴項嗎?

我試圖瞭解編譯器如何理解.c.h文件之間的關係。

鑑於這些文件:

header.h

int returnSeven(void); 

由source.c

int returnSeven(void){ 
    return 7; 
} 

的main.c

#include <stdio.h> 
#include <stdlib.h> 
#include "header.h" 
int main(void){ 
    printf("%d", returnSeven()); 
    return 0; 
} 

這混亂會編譯?我目前正在做我的工作,來自Cygwin的NetBeans 7.0gcc,它可以自動執行大部分構建任務。在編譯項目時,涉及的項目文件將根據header.h中的聲明,明確列出source.c

+1

是的,這將彙編(爲什麼你認爲這是一個「爛攤子「?)。要學習的概念是**編譯單元**和**鏈接**。 – Jesper 2011-05-05 21:56:09

+0

謝謝** Jesper **;哈哈,這不是一團糟,我想這個詞最適合描述我的大腦,可以在3本初學C級書籍之間閱讀。我肯定會研究*編譯單元*和*鏈接*,但爲了專注於學習語法,我會讓** NetBeans ** + ** gcc **爲我解決這個問題。鑑於這種情況,只要給定的頭文件在項目的其他地方存在定義的聲明,那麼包含該頭文件足以提供對定義的功能的訪問,編譯器會對這些細節進行整理? – Dan 2011-05-05 22:00:39

+1

'header.h'需要包括守衛;) – alternative 2011-05-05 22:01:22

回答

58

將C源代碼文件轉換爲可執行程序通常分兩步完成:編譯鏈接

首先,編譯器將源代碼轉換爲目標文件(*.o)。然後,鏈接程序將這些目標文件與靜態鏈接的庫一起取出並創建一個可執行程序。

在第一步驟中,編譯器需要編譯單元,這通常是一個預處理的源文件(所以,所有的標題的內容的源文件,它#include多個)並將其轉換爲一個對象文件。

在每個編譯單元中,使用的所有函數必須是,聲明爲,以便讓編譯器知道該函數存在以及它的參數是什麼。在您的示例中,函數returnSeven的聲明位於頭文件header.h中。在編譯main.c時,需要在聲明中包含頭文件,以便編譯器在編譯main.c時知道returnSeven

當鏈接器完成其工作時,它需要找到每個函數的定義。每個函數都必須在一個目標文件中定義一次 - 如果有多個包含相同函數定義的目標文件,鏈接器將停止並顯示錯誤。

您的功能returnSevensource.c中定義(並且main功能在main.c中定義)。總而言之,您有兩個編譯單元:source.cmain.c(包含它包含的頭文件)。您將它們編譯爲兩個目標文件:source.omain.o。第一個將包含returnSeven的定義,第二個爲main的定義。然後鏈接程序會將這兩個文件粘貼在一個可執行程序中。

關於聯動:

外部鏈接內部鏈接。默認情況下,函數具有外部鏈接,這意味着編譯器使這些功能對鏈接器可見。如果你創建了一個函數static,它具有內部鏈接 - 它只在定義它的編譯單元中可見(鏈接器不知道它存在)。這對於在源文件內部執行某些操作並且希望隱藏程序其餘部分的函數很有用。

+0

謝謝** Jesper **;你的回答幾乎涉及到我所困惑的所有問題。綜合迴應 – Dan 2011-05-05 23:19:30

+0

4年之後,我在回顧這個問題,我有點害怕,雖然這個答案寫得很好,內容翔實,但幾乎完全沒有回答實際問題,它幾乎沒有提到頭文件 – 2015-11-09 19:28:26

+1

幾乎完全不能回答實際問題。它幾乎沒有提到頭文件。「這是解釋整個編譯過程的最佳答案之一。 – 2016-08-04 12:54:02

23

C語言沒有源文件和頭文件的概念(編譯器也沒有)。這只是一個慣例;請記住頭文件始終是#include d到源文件中;在正確編譯開始之前,預處理器從字面上只是複製粘貼內容。

你的例子應該編譯(儘管有愚蠢的語法錯誤)。例如,使用GCC,您可能會首先執行:

gcc -c -o source.o source.c 
gcc -c -o main.o main.c 

這將分別編譯每個源文件,創建獨立的目標文件。在此階段,returnSeven()尚未在main.c內解決;編譯器只是以一種表明它必須在將來解決的方式標記目標文件。所以在這個階段,main.c看不到的定義returnSeven()不是問題。 (注意:這與main.c必須能夠看到聲明returnSeven()才能編譯的事實截然不同;它必須知道它確實是一個函數,它的原型是什麼,這就是爲什麼您必須在#include "source.h"之內。main.c

您然後執行:

gcc -o my_prog source.o main.o 

鏈接的兩個目標文件組合成一個可執行二進制文件,並執行符號的分辨率。在我們的示例中,這是可能的,因爲main.o要求returnSeven(),並且這由source.o公開。如果所有內容都不匹配,則會導致鏈接器錯誤。

+1

(注意:這與main.c必須能夠看到returnSeven()的聲明這一事實截然不同:我很迂腐,但這不完全正確。編譯器會很高興地編譯(with在C99中有一個警告),並且鏈接器解析它,通常會導致不好的結果。例如,文件ac中的 ,調用'x = bob(1,2,3,4)'和文件bc中的'void bob(char * a){}'將編譯,鏈接並運行。 – mattnz 2011-05-05 23:49:42

+0

絕對世界級的答案。愛極簡主義的GCC編譯器指令示例 – 2017-07-18 13:58:12

2

編譯器本身沒有關於源文件和頭文件之間關係的特定「知識」。這些類型的關係通常由項目文件(例如,makefile,解決方案等)定義。

給出的例子看起來好像它會正確編譯。您需要編譯這兩個源文件,然後鏈接器需要兩個目標文件來生成可執行文件。

4

頭文件用於分隔對應於源文件中實現的接口聲明。他們以其他方式受到虐待,但這是常見的情況。這不是編譯器,而是編寫代碼的人。

大多數編譯器實際上不會單獨看到這兩個文件,它們是由預處理器組合的。

10

編譯沒有什麼魔力。也不自動!

頭文件基本上向編譯器提供信息,幾乎從不代碼。
僅靠這些信息通常不足以創建完整的程序。

考慮「Hello World」程序(用簡單的puts功能):

#include <stdio.h> 
int main(void) { 
    puts("Hello, World!"); 
    return 0; 
} 

無頭,編譯器不知道如何處理puts()(它不是一個C關鍵字)。頭文讓編譯器知道如何管理參數和返回值。

但是,該函數的工作原理在此簡單代碼中的任何位置均未指定。其他人已經編寫puts()的代碼並將編譯後的代碼包含在庫中。作爲編譯過程的一部分,該庫中的代碼與源代碼的編譯代碼一起提供。

現在考慮你想你自己的puts()

int main(void) { 
    myputs("Hello, World!"); 
    return 0; 
} 

版本編譯只是因爲編譯器沒有關於功能的信息的代碼給出了一個錯誤。您可以將這些信息提供

int myputs(const char *line); 
int main(void) { 
    myputs("Hello, World!"); 
    return 0; 
} 

和代碼編譯現在---但沒有鏈接,即不產生可執行文件,因爲沒有爲myputs()沒有代碼。所以你寫myputs()代碼在一個名爲「myputs.c」

#include <stdio.h> 
int myputs(const char *line) { 
    while (*line) putchar(*line++); 
    return 0; 
} 

文件,你要記住編譯您的第一個源文件和「myputs.c」在一起。

過了一段時間,您的「myputs.c」文件已擴展爲一個完整的函數,並且您需要在要使用它們的源文件中包含有關所有函數(它們的原型)的信息。
將所有原型寫入單個文件和#include該文件更爲方便。包含你在輸入原型時不會冒犯錯誤的風險。

雖然你仍然需要編譯和鏈接所有的代碼文件。


當他們長大甚至更多,你把所有的已編譯的代碼庫中......這就是另一個故事:)