[5] 多文件工程

上一節:《函數》

接上一講,我們再看一下「掐頭去尾」程序的另一種寫法:

int main(){ int a = 123456; a = t(a); a = v(a); a = t(a); a = v(a); return a;}int v(int n){ int m = 0; while (n) { m = m * 10 + n % 10; n = n / 10; } return m;}int t(int n){ return n / 10;}

這個程序在build時會報錯,意思是調用t函數和v函數的時候,這兩個函數還沒有被定義,這樣是不行。不過我們可以稍作改動,把函數聲明放在前面,函數實現放在後面:

int v(int a);int t(int);int main(){ int a = 123456; a = t(a); a = v(a); a = t(a); a = v(a); return a;}int v(int n){ int m = 0; while (n) { m = m * 10 + n % 10; n = n / 10; } return m;}int t(int n){ return n / 10;}

這個程序可以正常編譯和運行,說明:

  • 函數的聲明實現是可以分離的
  • 在調用函數之前,程序必須聲明函數
  • 函數聲明語句中,參數列表中的變數名沒有實際作用,所以變數名可以有也可以沒有
  • 函數聲明語句末尾必須帶分號,函數實現的大括弧末尾不能帶分號

在實際項目中,往往都是多個人一起編寫程序,例如v函數、t函數和main函數可以是三個不同的程序員實現的。他們會各自編輯自己負責的C語言代碼。假設現在有vv.c, tt.c, main.c三個源代碼文件,內容分別是:

vv.c內容:

int v(int n){ int m = 0; while (n) { m = m * 10 + n % 10; n = n / 10; } return m;}

tt.c內容:

int t(int n){ return n / 10;}

main.c內容:

#include "vv.c"#include "tt.c"int main(){ int a = 123456; a = t(a); a = v(a); a = t(a); a = v(a); return a;}

現在我們要做的是,把這三個文件放在同一個目錄下(或者都放在桌面上),用Code::Blocks打開main.c,就可以正常地編譯和運行程序了。

這裡解釋一下main.c的寫法,特別是#include,它的作用通過文件名導入文件內容,因此這個main.c在編譯開始的時候,內容跟上一講最後一個程序是完全一樣的,當然就可以運行了。


這種直接導入別人的源代碼的方法雖然有用,但卻有不小的問題。這主要是因為編譯的過程是很費時間的,初學階段遇到的程序代碼量都比較少,但實際項目中代碼動輒上萬行,如果我們只為了對main函數作一些小修改,就不得不把其它上萬行的程序導入進來重新編譯,這樣是很浪費時間和資源的。

那怎樣才能既引入別人的代碼,又減少不必要的編譯呢。解決方法見下面的main.c:

int v(int a);int t(int);int main(){ int a = 123456; a = t(a); a = v(a); a = t(a); a = v(a); return a;}

現在我們要讓這三個文件成為一個整體,這就需要新建工程(Project)了。先自己做桌面新建一個文件夾,在Code::Blocks的左上角點擊New → Project,然後選Empty Project,然後輸入項目的名稱(隨便給項目起一個名字),項目文件夾選剛才新建的文件夾。然後右鍵點擊左邊的項目名,點Add File,把vv.c, tt.c, main.c加入都項目中。

這個代碼有點似曾相似,並且我們點Build和Run,發現它真的可以編譯和運行。這是因為它們已經被加入到了同一個項目中,所以互相之間已經可見了。main函數聲明了v函數和t函數,並且調用方法也是合法的,就可以被編譯成main.o文件,與此同時另外兩個.c文件也分別被編譯成了vv.o和tt.o文件。由於main.o裡面缺乏函數的實現,因此它只是一個空殼。編譯器會把這三個.o文件合併在一起,最終就變成了既有入口、又有實現、擁有完整邏輯的可執行程序(main.exe)了。這個過程就叫做鏈接(Link)

這樣做為什麼能減少不必要的編譯呢?因為如果我們只修改了main.c文件,那麼編譯器就只會做main.c→main.o的編譯,其它的.o文件則保持不變,最後再做一次鏈接操作就能生成新的可執行程序了。


在實際項目中,負責編寫vv.c和tt.c的人一般都會再編寫vv.h和tt.h,並加入到項目中,

vv.h的內容:

int v(int);

tt.h的內容:

int t(int);

這樣main.c的寫法就可以變成:

#include "vv.h"#include "tt.h"int main(){ int a = 123456; a = t(a); a = v(a); a = t(a); a = v(a); return a;}

這種寫法的優點是,當vv.h和tt.h裡面聲明了很多函數的時候,能夠幫main.c的編寫者節省不少敲代碼的時間,而且這樣寫也有利於整個項目代碼的日常維護。


回到「入門問題」一講,最後那個顯示字母的程序:

int putchar(int);int main(){ putchar(65); putchar(97); return 0;}

putchar函數的實現是開發環境自帶的,因此不需要我們自己實現putchar函數,在鏈接階段就能自動補全putchar的實現邏輯,我們就能無憂無慮地調用putchar函數。

有個開發環境自帶的stdio.h文件聲明了putchar函數,同時還聲明了一系列跟輸入輸出有關的函數,所以上面的程序還可以寫成:

#include <stdio.h>int main(){ putchar(65); putchar(97); return 0;}

因為stdio.h是系統自帶的,不是我們自己編寫並加入到項目中的,因此導入它的內容要用<stdio.h>而不是"stdio.h"


進入下一講之前,我們可以先想想怎樣才能把一個整數顯示在屏幕上。

下一節:《分支、循環與遞歸》


推薦閱讀:

Teach Yourself Programming in Ten Years - 用十年的時間自學編程

TAG:編程入門 | 編程學習 |