c語言鏈接問題

連接錯誤示例

    連接過程中常見的錯誤是符號未找到(undefined reference)和符號重定義(redefinition)。由於在編譯器在處理各個符號的時候,已經沒有了各個C語言源文件的概念,只有目標文件。因此對於這種錯誤,連接器在報錯的時候,只會給出錯誤的符號的名稱,而不會像編譯器報錯一樣給出錯誤程序的行號。

    符號未定義的錯誤經常發生在符號已經聲明,但是並沒有具體的定義的情況下。在C語言中,符號可以是一個函數,也可以是一個全局變量。在程序的編譯過程中,只要符號被聲明,編譯就可以通過,但是在連接的過程中符號必須具有具體的實現纔可以成功連接。

    例如:某一個源程序的文件的某一個地方調用了一個函數,如果這個函數具有聲明,這時編譯就可以通過。在連接的過程中,連接器將在各個代碼段中尋找函數,如果函數沒有在程序的任何一個位置中定義,那麼就不會有函數符號,這時連接器將發生符號未定義的連接錯誤。請閱讀如下程序:
            extern void function(void);
            int main(void) 
            {
                /* ...... */
                function ();
                /* ...... */
                return 0;
            }
    在以上的例子中函數function可以和其調用者main在同一個源文件中,也可以在其他的源文件被調用中,但是它必須被定義。

    符號重定義錯誤與符號未定義錯誤類似,如果連接器在連接的時候,發現一個符號在不同的地方有多於一個定義,這時就會產生符號重定義錯誤。對於同一個符號,如果在多個源文件中出現多次,將會產生符號重定義錯誤。

   知識點:在連接過程中,符號未定義和符號重定義是兩種最基本的錯誤。

下面以一個包含3個文件的程序爲例,說明在連接過程中的錯誤。這個程序的三個文件爲hello.h、hello.c和main.c。

     main.c文件如下所示:
            #include "hello.h"

            int main(void) 
            {
                hello();
                string[0] = ''''a''
                return 0;
            }

     hello.h文件如下所示:
            #ifndef HELLO_H
            #define HELLO_H

            void hello(void);
            extern char string [];
            #endif

     hello.c文件如下所示:
            #include 
            #include "hello.h"

            char string [1024] = "";
            void hello(void)
            {
                printf("=== hello ===\n");
            }
    以上是一個可以正常運行的程序,編譯器將對main.c和hello.c兩個源文件分別進行編譯。在main函數中,調用了函數hello,並訪問了數據string,由於包含了hello.h頭文件,hello和string具有聲明,因此main.c可以被成功編譯。hello.c中定義了一個讀寫數據段上的變量string和函數hello。在連接的過程中,目標文件main.o對string和hello符號進行訪問,hello.o提供了這兩個符號,因此可以連接成功。

1.由於無數據定義導致符號未定義錯誤
將hello.c中對data的定義去掉,這時編譯器依然可以成功編譯main.c和hello.c。但是,由於string[0] =””a””編譯後產生的代碼將對string訪問,在連接的過程中找不到string這個數據,因此會產生符號未定義連接錯誤(找不到數據)。

2.由於無函數實現導致符號未定義錯誤
將hello.c中對hello函數的定義去掉,這時編譯器還是可以成功編譯main.c,而且有這個符號,因此也會產生目標文件hello.o。但是,在連接器處理函數調用的時候,需要跳轉到hello符號,由於實際上並沒生符號而報告未定義連接錯誤(找不到函數)。

     知識點:在程序中使用只有聲明而未定義的函數或數據,可以成功編譯,但是連接時將發生符號未定義錯誤。

3.由於數據僅能在文件內部使用,導致符號未定義錯誤

     將hello.c的數組string []更改爲靜態變量:
     static char string [1024] = "";
     此時編譯依然是可以通過的,這時候由於string已經是一個靜態數據,因此它不會出現在hello.c的目標文件的符號中。也就是說,增加static修飾後,目標文件和以前將略有不同。

     連接器在處理的時候,雖然有string這個數據,但是它只有數據區而沒有符號,因此依然會出現未定義符號錯誤。

4.函數具有聲明可編譯通過,但有連接錯誤
取消hello.c中對函數的聲明,在main.c增加對該函數的聲明:
void hello(void);

     在hello.c中,將hello函數的定義改爲靜態的:
              static void hello(void)
              {
                 printf("=== hello ===\n");
              }
     這時,編譯還是能通過,由於hello成爲靜態函數,只能在文件內部使用,不會產生外部的符號。因此,雖然在main中對該函數進行了聲明,連接也會產生未定義符號錯誤。

     函數在各個源文件中的聲明不會對連接產生影響,因此,在本例中,只要hello函數不是靜態的,連接就可以通過。

     知識點:使用static的函數和變量不能給其他文件調用,否則會發生連接的符號未定義錯誤。

5.定義同名變量導致符號重定義錯誤
在hello.h文件中,取消對string的外部聲明。在main函數中增加一個定義在讀寫數據段的字符數組string[],

     char string[1024] = "main string";

     編譯通過後,在連接時將會產生符號重定義的連接錯誤。main.c和hello.c中各自有一個讀寫數據段的字符數組string[],雖然它們看似沒有直接的關係,但連接器無法處理這種情況,依然會產生連接錯誤。

     在這種情況下,即使沒有對string的引用(即沒有string[0] = ''''a''''),連接器依然無法處理兩個同名的讀寫數據段全局變量,這時還是會報告符號重定義的連接錯誤。

6.實現同名函數導致符號重定義錯誤

     在main.c文件中,增加對hello函數的定義。
              void hello(void)
              {
                 printf("+++ main hello +++\n");
              } 
     編譯通過後,連接時會產生符號重定義的錯誤。實際上,由於在hello.c和main.c的目標文件的代碼段中分別定義hello函數。連接器認爲出現重定義。與上例的程序類似,即使沒有對函數hello的調用,編譯器也不允許在代碼段中出現2個hello函數的符號,所以還是會產生符號重定義連接錯誤。

     知識點:在多個文件中定義全局的同名函數和變量,連接時將發生符號重定義錯誤。

7.靜態函數與其他文件中的函數重名,可以正常使用
將main.c文件更改成如下:
#include “hello.h”

              int main(void) 
              {
                  hello();
                  string[0] = ‘a’;
                  return 0;
              }

              static void hello(void)                        /* 靜態的函數,內部使用 */
              {
                  printf("+++ main hello +++\n");
              }
     程序主要的變化是增加了靜態的hello函數,在這種情況下,是可以成功地進行編譯連接和運行的,運行結果如下所示:

              +++ main hello +++

     從運行結果中可以看到,main中調用的hello函數是main.c文件中定義的static 的hello函數。

     值得注意的是,在這種情況下,編譯器在進行編譯的時候,main函數寫在靜態的函數hello前面,因此可以通過編譯。這是由於main.c文件中包含了hello.h文件,其中具有對hello()函數的聲明。但是,當編譯器編譯到main.c之中的hello函數的時候,由於static頭文件中聲明函數原型不同,可能出現一個編譯報警(warning)。

8.靜態變量與其他文件中的變量重名,可以正常使用
在main.c中,增加靜態(static)的讀寫數據段的字符數組string[]的定義。

              char string[1024] = "main string";
     在這種情況下,編譯連接可以成功。當連接工作完成後,可執行程序的讀寫數據段將出現兩個string[1024]數組,均佔用的空間。一個是hello.c中定義的全局的數組,一個是main.c定義的文件內部使用的數組。在數據訪問的過程中,語句string[0] = ''''a''''訪問的將是main.c中定義的數組string[]。

     知識點:如果全局變量和函數已定義,而在某個文件中另外定義靜態的同名變量和函數,可以在文件內部使用同名的靜態變量和函數。在使用的過程中,將優先使用文件內的變量和函數。

9.在頭文件中定義已經初始化數據,可能產生問題
在程序中,將string[]的定義放入hello.h的頭文件中:
#ifndef HELLO_H
#define HELLO_H

              void hello(void);
              char string[1024] = "";
              #endif
     同時,取消在hello.c中對string[]數組的定義。此時,由於hello.c和main.c同時包含了hello.h頭文件,因此string在內存中有兩份,連接的時候將產生符號重定義錯誤。

     如果將頭文件中string的定義改爲靜態的,這時不會產生連接錯誤,但是會在hello.c和main.c的目標文件中各產生一個string[1024]。最終可執行程序的讀寫數據段中也會有兩個string。

     如果在頭文件中使用如下方式定義:

     const char string_const[1024] = {"constant data"};

由於具有const屬性,string_const是一個在只讀數據段上的常量。這樣有多個文件包含該頭文件的時候,在連接過程中也會出現符號重定義的錯誤。連接器在這個問題上,對讀寫數據區和只讀數據區的處理方式是類似的。

     知識點:具有初始值的變量將被連接到讀寫數據區。在頭文件中不應該定義有初始值的全局變量。同樣,也不應該定義只讀數據段的常量。否則,在頭文件被多個文件包含的時候,將發生連接錯誤。

10.在頭文件中定義未初始化數據段,可以正常使用
在程序中,hello.h的頭文件中定義string[],但是沒有初值:
#ifndef HELLO_H
#define HELLO_H

              void hello(void);
              char string[1024];
              #endif

     同時,取消在hello.c中對string[]數組的定義。在這種情況下,編譯連接都是可以通過的,程序也可以正常運行。

     知識點:無初始化的變量將被連接到未初始化數據段,在頭文件中可以定義。當頭文件被多個文件包含時,該未初始化段在運行時也將只有一份。

     事實上,由於沒有初值,string[]將不再是讀寫數據段上的變量,而是未初始化數據段上的變量。未初始化段上的變量並不會佔用目標文件或者可執行文件中的空間,它們只是一些標識。由於不需要分配空間,編譯器允許這種做法。未初始化數據段的變量在運行的時候纔會產生,而且只會有一個。

     同理,可以將string修改爲static的未初始化變量:
              #ifndef HELLO_H
              #define HELLO_H

              void hello(void);
              static char string[1024];
              #endif
    在這種情況下,在編譯的時候將會在兩個目標文件中各自記錄一個未初始化數據段,在運行時程序將在內存上開闢兩個獨立的1024字節的數據區。

    比較以上的兩個示例(9和10),總結出以下的結論:

    首先,不應該在頭文件中使用全局的讀寫數據變量,這樣當兩個文件同時引用這個頭文件的時候,將會產生符號重定義連接錯誤。

    其次,在頭文件中也不應該使用靜態的變量,無論它有沒有初值(即在讀寫數據段或者未初始化數據段),這樣雖然不會引起連接錯誤,但是在各個源文件中各自產生變量,不但佔用更多的空間,而且在邏輯上是不對的,也違背頭文件的使用原則。

    最後,在頭文件中使用全局的沒有初始化的變量是可以的,它在程序運行的過程中,在內存中只會有一份,可以被包含該頭文件的程序訪問。

    從C語言程序設計的角度,不應該在頭文件中定義變量或者函數。對於函數,在頭文件中只是聲明,需要在源文件中定義;對於變量,無論何種性質(只讀數據段、可讀寫數據段、未初始化數據段),最好的方式是在C語言的源文件中定義,在頭文件中使用extern聲明。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章