一.概要
對於語言初學者,開始的時候總是把所有的代碼寫在一個源檔案中。當程式碼非常龐大時候,這樣的做法往往很難維護和修改。而真正的工程項目都是由多人共同完成的,因此劃分模組,組織良好清晰的檔案結構顯得非常重要。本文主要針對C/C++語言初學者在組織工程檔案結構時所遇到的眾多問題和概念給予總結。
二.認識編譯單元
開始時,自己寫的小工程只有一個源檔案時,通過編譯連結就可產生可執行檔。這個過程往往忽視了連結的存在。連結的主要工作就是將多個編譯單元各自產生的目標檔案彼此相串連,也即將在一個檔案中引用的符號同該符號在另外一個檔案中的定義串連起來,使得所有的這些目標檔案成為一個能夠被作業系統裝入執行的統一整體。而編譯單元就是一個源檔案,當我們只有一個源檔案時,連結的作用往往體現不出來。
編譯就是把文本形式原始碼翻譯為機器語言形式的目標檔案的過程。一般情況一個源檔案.c/.cpp就是一個編譯單元,如下例:兩個編譯單元分別獨立編譯,編譯所做的一件重要事情就是看是否有未定義的東西。在main.cpp我們開始聲明的f()就是告訴編譯器:有f()這個函數。編譯器就按照聲明的格式去編譯後續調用f()的代碼,然後首先會在本檔案末尋找f()的具體定義,如果找不到就等到連結的時候看看其他編譯單元是否有,如果還找不到就會報連結錯誤了!
因此我們可以再一個編譯單元A中定義一個函數f,而只要在其他編譯單元聲明f,就可以使用它了。通常以標頭檔的形式展現這個過程:
而在編譯器的眼裡,沒有標頭檔。因為在先行編譯階段,所有”#include”,都只是一段代碼的插入。那麼所有的.h標頭檔全部都被插入到眾多.c/.cpp源檔案中。此時.h檔案的作用就終結了。編譯器只看到了兩個編譯單元:
編譯f.cpp單元時一切OK,編譯main.cpp時main函數外面聲明的f()格式去調用f()函數,編譯器也未在自己的編譯單元中找到f()的定義,那麼之後將在連結的時候去其他編譯單元尋找f()的定義。
三.連結那些事兒3.1 error: undefined reference
此例,兩個編譯單元各自通過編譯,main.cpp在編譯時間沒有在自己內部找到聲明的f()函數的定義。在連結過程中就會去其他編譯單元找,也沒找到就會報undefined reference錯誤了。那對於每個編譯單元中未找到的定義,在連結的時候都要去其他哪些編譯單元中找呢?這就看makefile的寫法了。如本例:gcc -o object f.o main.o 顯示的將main.o和f.o連結。那麼連結時候main.o和f.o就會互相尋找是否有自己需要的定義。
3.2 error: mutiple definition
3.1中的例子我們說了未定義的錯誤,現在討論多重定義的錯誤,如上面這三個編譯單元,各自編譯OK。連結的時候main.o就得去其他編譯單元找f()的定義,可以發現有兩個編譯單元提供了f()的定義,此時就會報mutiple definition了。
3.3不得不說的聲明和定義
如前所述,我們發現對於函數有定義和聲明。在一個編譯單元裡面定義,在別的編譯單元只要聲明就可以使用。這種特性叫做外串連。
函數是外串連的,那麼變數呢?局部變數有它限制的範圍,不會牽扯到連結問題。只有全域變數才會牽扯到連結。
連結這兩個編譯單元gcc a1.o main.o 就會報mutiple definition錯誤,說明全域變數也是外串連的。也就是你在自己模組定義的全域變數將會影響到所有連結單元。因此使用全域變數要小心。有的初學者甚至把全域變數定義在標頭檔中,那樣若多個編譯單元include此標頭檔,那麼這些編譯單元都有一個同名全域變數,連結必然出錯。如果想使用其他編譯單元的全域變數怎麼辦呢?使用extern關鍵字聲明。正確的使用如下:
用法和函數幾乎一樣,在a1.cpp定義,而在main.cpp中聲明一下就可以使用了,就是告訴編譯器,你先用著,看看咱自己編譯單元有沒有定義,沒有的話等連結的時候再看看其他編譯單元吧。
因此只有函數和全域變數屬於外串連,而連結時,主要是連結函數和全域變數。
四:全域變數的聲明和定義
從上面的學習已經知道,一個函數一般有兩部分組成:聲明部分和定義部分。函數的聲明是函數的原型,而函數的定義是函數的本身。想在任何編譯單元使用某個函數,只需聲明一個這個函數就可以。編譯器會幫你在其他編譯單元找到對應函數定義的。
而對於變數而言,對變數而言,聲明和定義的關係稍微複雜,在聲明部分出現的變數有兩種情況:一種是需要建立儲存空間的(如int a),另一種是不需要建立儲存空間的(如:extern a).前者為“定義性聲明”,或簡稱“定義”。後者稱為“引用性聲明”。廣義的說他們都是聲明。但我們通常為了敘述方便,把建立儲存空間的聲明稱定義,而把不需要建立儲存空間的聲明稱為聲明。總結一下:只有全域變數的聲明和定義不是一回事。
對全域變數用static聲明,則該變數就變成了內串連。即在某個編譯單元定義一個全域變數後,在其他編譯單元用extern聲明此全域變數,依然訪問不到此全域變數。
五.讓標頭檔扮演模組向外公開的介面
很多情況下標頭檔這個角色往往把初學者繞的很暈。需掌握以下幾點:
(1)#include一個標頭檔就是插入代碼。
(2)標頭檔A.h裡面經常放對A.c編譯單元裡面的函式宣告。因此別的編譯單元#include A.h,其實就插入這個A.c編譯單元的函式宣告。那麼可以理解為A.h通常就是A.c編譯單元往外提供的使用介面。
(3)使用內部包含衛哨#ifndef...#define..#endif來防止重複聲明或定義。