2013-08-31 132 views
18

我知道頭文件具有在.c文件中用於調用#include的各種函數,結構等的前向聲明,對吧?據我明白了, 「三權分立」 發生這樣的:C頭文件和編譯/鏈接

頭文件func.h

  • 包含的函數的前向聲明

    int func(int i); 
    

C源文件: func.c

  • 包含實際的函數定義

    #include "func.h" 
    
    int func(int i) { 
        return ++i ; 
    } 
    

C源文件source.c( 「實際」 計劃):

#include <stdio.h> 
#include "func.h" 

int main(void) { 
    int res = func(3); 
    printf("%i", res); 
} 

我的問題是:看到了#include是一個簡單的編譯器指令,副本在#include所在文件中的.h的內容,.c文件如何知道如何實際執行該功能?它所得到的是int func(int i);,所以它怎麼能實際執行該功能?它如何獲得func的實際定義?標題中是否包含某種「指針」,表示「這是我的定義,在那邊!」?

它是如何工作的?

+0

這就是'Linker'解決定義並確保你在編譯期間聲稱存在的事實際存在的魔力。 –

+0

在處理頭文件時,您可能需要閱讀[include guard](http://en.wikipedia.org/wiki/Include_guard)。 –

+0

我知道包括警衛(ifndef所有這些),但爲了簡潔省略了它們。 – Aristides

回答

20

Uchia Itachi給出了答案。這是鏈接器

使用GNU C編譯器gcc你會編的一檔節目像

gcc hello.c -o hello # generating the executable hello 

但是編譯在你的例子所描述的兩個(或更多)文件的程序,你就必須做到以下幾點:

gcc -c func.C# generates the object file func.o 
gcc -c main.C# generates the object file main.o 
gcc func.o main.o -o main # generates the executable main 

每個目標文件都有外部符號(您可能會認爲它是公共成員)。函數默認是外部的,而(全局)變量默認爲內部函數。你可以通過定義

static int func(int i) { # static linkage 
    return ++i ; 
} 

/* global variable accessible from other modules (object files) */ 
extern int global_variable = 10; 

當遇到調用一個函數,在主模塊中沒有定義更改此行爲,鏈接器搜索所有的提供的對象文件(庫)用於定義被調用函數的模塊的輸入。默認情況下,你可能有一些庫鏈接到你的程序,這就是你如何使用printf,它已經被編譯到一個庫中。

如果您真的感興趣,請嘗試一些彙編編程。這些名稱與彙編代碼中的標籤相同。

+0

因此,對於GCC,模式是:1.對每個.c(帶有定義)和.h(帶有func原型)使用-c標誌來創建每個.o 2.使用-o標誌和每個.o文件創建最終exe文件? – Aristides

+0

是的。 「-c」選項用於「編譯」,因此只需將目標代碼編譯爲目標文件即可。沒有-c的gcc認識到輸入是目標文件,所以它只是使用鏈接器將它們鏈接在一起。最後,-o標誌是可選的,它用於指定可執行文件的輸出文件名。 –

+2

這是一個非常好的答案,謝謝。 –

14

這是處理所有的鏈接器。編譯器只是在對象文件中發出一個特殊序列,表示「我有這個外部符號func,請解析它」鏈接器。然後鏈接程序會看到該內容,並搜索所有其他目標文件和庫以查找該符號。

+0

這是否意味着項目中的所有'.c'文件都將被搜索? –

+0

@LidongGuo如果你在命令行上一起編譯所有源文件,或者如果你從所有源創建目標文件並將它們鏈接在一起,那麼是的,它們將被搜索。它不是自動完成的,你必須告訴鏈接器你想鏈接哪些目標文件,只有那些將被搜索。 –

2

報頭提供不僅在同一程序中其它.c文件,但同樣的是可以以二進制形式被分發庫的訪問。一個.c文件與另一個文件的關係與依賴另一個的庫完全相同。

由於編程接口需要在文本形式無論執行的格式,頭文件是有意義的關注點分離。

正如其他人所說,解析函數調用和庫和來源(編譯單元)被稱爲連接器之間的訪問節目。

鏈接器不與接口兼容。它只是在所有翻譯單元和庫中定義的所有名稱的大表,然後將這些名稱鏈接到訪問它們的代碼行。 C的古代用法甚至允許在沒有任何實現聲明的情況下調用函數;只是假定每個未定義類型都是int

3

沒有相同的編譯單元內的定義符號的聲明告訴編譯器編譯一個佔位符,該符號的地址轉換成目標文件。

鏈接器將會看到符號的定義是必需的,並且會查找庫和其他對象文件中符號的外部定義。

如果鏈接器找到定義,原始對象文件中的佔位符將被最終可執行文件中找到的地址替換。

1

通常當你編譯這樣的文件:

gcc -o program program.c 

你真的是調用驅動程序,它具有以下功能:

  • 預處理(如果你問它是一個單獨的步驟)使用cpp
  • 彙編(可能與預處理集成)使用cc1
  • 彙編,使用as(gas,GNU彙編程序)。
  • 鏈接使用collect2,它也使用ld(GNU鏈接器)。

典型地,在第一階段3中,將創建一個簡單的對象文件(擴展名.o),其被通過編譯編譯單元創建的(即是一個.c文件,與所述的#include和替換其他指示預處理器)。

第四階段是創建最終可執行文件的階段。編譯完一個單元后,編譯器會將多段代碼標記爲需要由鏈接器解析的引用。鏈接器的工作是在許多編譯單元中搜索並解析對外部編譯單元的引用。