程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C語言筆記之頭文件與鏈接(一)

C語言筆記之頭文件與鏈接(一)

編輯:關於C語言

C語言筆記之頭文件與鏈接(一)


 

雖然一直在用#include命令包含頭文件,但其實一致不太明白頭文件的原理。今天就研究了一下。

 

 

首先,在大型項目中,僅僅一個源文件是不夠的,巨大的代碼量需要分別放在幾個文件中,當然分開存放的更主要的目的是便於模塊化。我們把代碼按照不同的功能或作用分隔存放在不同的文件中,那麼當其中一個功能有改動時,只需要重新編譯相關的文件,而不必編譯整個項目的所有源文件。

但是,這樣就帶來了一個問題:在一個文件中定義的變量或函數,能不能在另一個文件中使用呢?或者兩個文件中同名的變量會不會引起沖突呢?

為了回答這個問題,首先要明白C語言的源代碼如何一步步生成可執行代碼的。我們先看只有一個源文件的情況:

首先經過預處理器,替換掉文件中的宏命令;

然後經過編譯器,生成匯編代碼;

接著是匯編器,生成二進制的目標代碼,然而此時的目標代碼仍然不能執行,它還缺少啟動代碼(程序和操作系統之間的接口)和庫代碼(比如printf函數的實體代碼);

最後經過鏈接器,鏈接相關的代碼,生成最終的可執行代碼。

 

既然提到了編譯,就不得不介紹一下C語言的編譯器gcc,假設我們寫好了一個源文件first.c,那麼對應上面的步驟,gcc的命令參數如下:

預編譯: gcc -E first.c -o first_1.c (注:-o 選項用來指定生成結果的文件名)

匯編: gcc -S first.c -o first.s

編譯: gcc -c first.s -o first.o (也可以直接編譯源碼:gcc -c first.c -o first.o)

可執行: gcc first.o -o first (當然,這裡也可以一步到位:gcc first.c -o first)

 

現在我們把目光集中到鏈接過程上。從上面的分析可以知道,所謂鏈接,就是把目標代碼、啟動代碼和庫代碼結合到一起形成可執行代碼。上面是只有一個源文件的情況,如果有多個文件,則把多個目標代碼與啟動代碼和庫代碼粘合在一起。那麼問題來了:多個目標代碼真的就能隨隨便便粘合在一起嗎?

 

要回答這個問題,還是得回到對源代碼的分析上,畢竟目標代碼只是源代碼的編譯版本。雖然源代碼被分隔成幾個部分並存放到不同的文件中,但是在邏輯或者上下文中,還是必須要保持一致的。也就是說,把幾個文件中的代碼重新放回到一個文件中,它們還是要保持“兼容”的,比如變量啊、函數啊之類的,不能重復;再比如只能有一個main函數。

 

然而,我們知道,變量和函數的作用域,最大的也就是文件作用域了。???比如,如何保證一個文件中的變量也被其他的文件直接使用並且不會引起沖突呢?答案就是頭文件。頭文件,就是把各個被分割的文件在邏輯上串起來的關鍵。

現在給出一個例子,在這個例子中,我用C代碼模仿游戲“石頭剪子布”,0、1、2分別代表石頭、剪子、布。游戲過程中,程序隨機產生一個數,同時提示用戶輸入一個數,然後根據規則做出輸贏判斷。完整的代碼如下:

 

#include 
#include 
#include 

int gen_rnd(void);
void judge(int, int);

int main(void)
{
    int user, computer;

    printf("Please input a number, 0 for stone, 1 for scissors, 2 for cloth and q for quit: ");

    while(scanf("%d", &user) && user != 'q') {
        if(user > 2 || user < 0) {
            printf("Please input a number between 0 and 2: ");
            continue;
        }

        computer = gen_rnd();
        judge(user, computer);
        printf("number: ");
    }

    return 0;
}

int gen_rnd(void)
{
    int ret;
    time_t t;

    srand((unsigned)time(&t));
    ret = rand() % 3;

    return ret;
}

void judge(int user, int computer)
{
    char *name[] = {"stone", "scissors", "cloth"};
    int res = abs(user - computer);

    if(res == 0)
        printf("The computer is %s and you are %s, even\n", name[computer], name[user]);
    else if(res == 1) {
        if(user < computer)
            printf("The computer is %s and you are %s, you win\n", name[computer], name[user]);
        else
            printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]);
    }
    else {
        if(user < computer)
            printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]);
        else
            printf("The computer is %s and you are %s, you win\n", name[computer], name[user]);
    }
}

 

file.c

 

源碼中有三個函數,分別代表不同的功能:main是主函數;gen_rnd()產生隨機數用來模擬電腦;judge()用來判斷輸贏。每個函數就是一個功能模塊,現在我們把這個文件分割成三個,分別是main.c gen_rnd.c judge.c,每個文件只存放一個函數。如下:

 

#include 
#include 

int gen_rnd(void);
void judge(int, int);

int main(void)
{
    int user, computer;

    printf("Please input a number, 0 for stone, 1 for scissors, 2 for cloth and q for quit: ");

    while(scanf("%d", &user) && user != 'q') {
        if(user > 2 || user < 0) {
            printf("Please input a number between 0 and 2: ");
            continue;
        }

        computer = gen_rnd();
        judge(user, computer);
        printf("number: ");
    }

    return 0;
}
main.c

 

 

 

#include 

int gen_rnd(void)
{
    int ret;
    time_t t;

    srand((unsigned)time(&t));
    ret = rand() % 3;

    return ret;
}
gen_rnd.c

 

 

#include 

void judge(int user, int computer) { char *name[] = {"stone", "scissors", "cloth"}; int res = abs(user - computer); if(res == 0) printf("The computer is %s and you are %s, even\n", name[computer], name[user]); else if(res == 1) { if(user < computer) printf("The computer is %s and you are %s, you win\n", name[computer], name[user]); else printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]); } else { if(user < computer) printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]); else printf("The computer is %s and you are %s, you win\n", name[computer], name[user]); } }

 


judge.c

 

可以看到,由於成為了單獨的文件,judge.c必須要自己包含,否則編譯目標文件時會報錯:

 

m@sys:~/program/C_codes$ gcc -c judge.c 
judge.c: In function ‘judge’:
judge.c:8:9: warning: incompatible implicit declaration of built-in function ‘printf’ [enabled by default]
         printf("The computer is %s and you are %s, even\n", name[computer], name[user]);
         ^
同樣的道理,gen_rnd.c則要自己包含,而main.c則不需要這個頭文件了。
現在,我們分別為其生成目標文件:

 

gcc -c judge.c main.c gen_rnd.c

這會在當前目錄下自動生成gen_rnd.o judge.o main.o

接著就可以生成可執行文件了:gcc gen_rnd.o judge.o main.o -o exe

這三個目標文件之所以還能被正確的粘合在一起,是因為它們仍然存在著邏輯上的聯系:首先,只有main.c文件有一個main函數,這就提供了正確的入口;其次,各個文件都能包含需要的頭文件,從而正確的生成各自的目標代碼;再次,因為main.c要調用另外兩個函數,所以聲明了另外兩個函數的原型,雖然該文件中沒有它們的代碼,但是在鏈接階段兩個函數的代碼卻會一起組合到可執行文件中,同樣的道理,printf()等函數的代碼也會在鏈接階段被組合到可執行文件中,即所謂的鏈接庫文件。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved