C/C++中的重複定義bug

C/C++中的重複定義bug

標籤(空格分隔):c/c++


  • 使用不當很容易出現重定義的bug

    • 可以重複聲明,不可以重複定義
        在.h頭文件中,變量如果沒有初始化就是聲明;初始化了就是定義。所以很多代碼裏面,都把變量的聲明放在.h文件中當作全局變量使用,這是可以的,但如果這樣使用的話是不能進行初始化的。一旦對.h中聲明的變量進行了初始化也就相當於進行了定義,就可能出現重複定義的bug。

    • 或許可行的解決方案

      #ifndef _HEADERNAME_H
      #define _HEADERNAME_H
      
      ...//(頭文件內容)
      
      #endif
      

      該解決方案針對的是一個.c文件中存在多個 #include *.h ,且這些.h文件同時又包含很多其他的頭文件,造成重複包含。

    • 其他的重複包含

      • 情況一
        [main.c]

        #include <stdio.h>
        #include <stdlib.h>
        #include "fun.h"
        
        // int x;    //見藍色文字說明
        
        int main()
        {
            fun(0);
            g_val;
        
            system("pause");
            return 0;
        }
        

        [fun.c]

        #include <stdio.h>
        #include <stdlib.h>
        #include "fun.h"
        
        void fun(int x)
        {
            printf("iuahysiouayspi");
        }
        

        [fun.h]

        int g_val;
        void fun(int x);
        

      可以正常執行,不會報錯,通過 nm 命令列出目標文件(.o文件)的符號清單如下:

         可以看出,只是聲明但是沒有定義的 g_val ,在兩個目標文件中都位於C段,即common段,表示未初始化數據段,該數據段的數據對應的空間只有在鏈接過程纔會分配,所以不會有重定義的報錯(相當於多次聲明,但不是多次定義)。不要使用這種方式聲明全局變量!!!
        有一點需要說明的是如果我們在main.c文件中的main函數上面聲明一個全局變量 int x; 通過 nm 命令可以發現,x在ELF中的位置和頭文件中聲明g_val的位置規律是一致的,即如果未初始化就在common段,如果初始化了就在.bss/.data段(由是否初始化爲0決定)。

      • 情況二
        [main.c]

        #include <stdio.h>
        #include <stdlib.h>
        #include "fun.h"
        
        int main()
        {
            fun(0);
            g_val;
        
            system("pause");
            return 0;
        }
        

        [fun.c]

        #include <stdio.h>
        #include <stdlib.h>
        #include "fun.h"
        
        void fun(int x)
        {
            printf("iuahysiouayspi");
        }
        

        [fun.h]

        int g_val = 0;     // or int g_val = 1;
        void fun(int x);
        

      見下圖,會報錯,g_val重定義。
      【初始化爲0】

      【初始化爲1】

        可以看到,如果全局變量初始化爲0,在兩個目標文件中g_val都位於B段,即.bss段。如果全局變量初始化爲1,在兩個目標文件中g_val都位於D段,即.data段。很明顯,兩種情況下都會出現重複定義的問題(這也對應了不能出現兩個強符號的規則)。

      • 情況三
        [main.c]

        #include <stdio.h>
        #include <stdlib.h>
        #include "fun.h"
        
        int g_val = 0;
        
        int main()
        {
            fun(0);
        
            system("pause");
            return 0;
        }
        

        [fun.c]

        #include <stdio.h>
        #include <stdlib.h>
        #include "fun.h"
        
        void fun(int x)
        {
            printf("iuahysiouayspi");
        }
        

        [fun.h]

        extern int g_val;
        void fun(int x);
        

        最好的全局變量的使用方法:在相應的.c文件中定義全局變量,在對應的.h文件中使用extern關鍵字。

          因爲對fun.c而言, extern int g_val 只是聲明,而不是定義,且因爲extern關鍵字,導致在fun.o文件中沒有g_val符號的存在(在unix下使用U來標記這種情況的,U表示該符號在當前文件中未定義,即符號的定義在別的文件中)。而在main.o中,g_val被定義(且被初始化爲0),在g_val在main.o目標文件的.bss段。此時,兩個可執行文件之間不會出現全局變量的重定義。

        【再次說明】
        #ifndef #define ... #endif
          這些宏的作用只是在編譯之前就“決定”了代碼是否會參與編譯以及後面的鏈接過程。 #define 宏的使用見define和const的對比

      【補充·其他情況】
      [main.c]

      #include <stdio.h>
      #include <stdlib.h>
      #include "fun.h"
      
      g_val = 0;
      
      int main()
      {
          fun(0);
      
          system("pause");
          return 0;
      }
      

      [fun.c]

      #include <stdio.h>
      #include <stdlib.h>
      #include "fun.h"
      
      void fun(int x)
      {
          printf("iuahysiouayspi");
      }
      

      [fun.h]

      int g_val;
      void fun(int x);
      

      會報錯,但是和重定義沒有關係,報錯如下:

      error: ‘g_val’ does not name a type
      g_val = 0;
      ^

        原因是在頭文件中聲明的全局變量,其實相當於在函數之外聲明瞭全局變量(預處理過程,include內容被替換爲相應代碼),C/C++規定不能在函數外對變量賦值,但是可以初始化這與很多編譯器鏈接之前可以單獨編譯的特性沒關係…


        上述說明在c++中就不行,c++作爲一門面向對象的混合型語言,雖然形式上述的代碼是可以的,但是最好不要出現面向過程的寫法(再次我們僅僅是爲了說明語言的差異)。如果還是上述代碼,我們使用g++編譯器編譯會發現,不管我們是否初始化,在頭文件中或者在.c文件中都算做定義,即變量都會分配在ELF的.bss/.data段,因爲c++已經規定只有使用extern關鍵字且沒有初始化的形式叫做聲明,比如 extern int a; ,其餘的都叫做既聲明又定義,比如 int a;

  • 來自 coolshell 的記錄:一個例子

    • 全局變量是C語言語法和語義中一個很重要的知識點

      • 首先它的存在意義需要從三個不同角度去理解:對於程序員來說,它是一個記錄內容的變量(variable);對於編譯/鏈接器來說,它是一個需要解析的符號(symbol);對於計算機來說,它可能是具有地址的一塊內存(memory)。

      • 其次是語法/語義:從作用域上看,帶static關鍵字的全局變量範圍只能限定在文件裏,否則會外聯到整個模塊和項目中;從生存期來看,它是靜態的,貫穿整個程序或模塊運行期間(注意,正是跨單元訪問和持續生存週期這兩個特點使得全局變量往往成爲一段受攻擊代碼的突破口,瞭解這一點十分重要);從空間分配上看,定義且初始化的全局變量在編譯時在數據段(.data)分配空間,定義但未初始化的全局變量暫存(tentative definition)在.bss段,編譯時自動清零,而僅僅是聲明的全局變量只能算個符號,寄存在編譯器的符號表內,不會分配空間,直到鏈接或者運行時再重定向到相應的地址上

    • 一個例子

      [ t.h ]

      #ifndef _H_
      #define _H_
      int a;
      #endif
      

      [ foo.c ]

      #include <stdio.h>
      #include "t.h"
      
      struct {
         char a;
         int b;
      } b = { 2, 4 };
      
      int main();
      
      void foo()
      {
          printf("foo:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
              \tsizeof(b)=%d\n\tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
              &a, &b, sizeof b, b.a, b.b, main);
      }
      

      [ main.c ]

      #include <stdio.h>
      #include "t.h"
      
      int b;
      int c;
      
      int main()
      {
          foo();
          printf("main:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
              \t(&c)=0x%08x\n\tsize(b)=%d\n\tb=%d\n\tc=%d\n",
              &a, &b, &c, sizeof b, b, c);
          return 0;
      }
      

      運行情況:

      foo:    (&a)=0x0804a024
      (&b)=0x0804a014
      sizeof(b)=8
      b.a=2
      b.b=4
      main:0x080483e4
      main:   (&a)=0x0804a024
      (&b)=0x0804a014
      (&c)=0x0804a028
      size(b)=4
      b=2
      c=0

        這個項目裏我們定義了四個全局變量,t.h頭文件定義了一個整型a,main.c裏定義了兩個整型b和c並且未初始化,foo.c裏定義了一個初始化了的結構體,還定義了一個main的函數指針變量。由於C語言每個源文件單獨編譯,所以t.h分別包含了兩次,所以int a就被定義了兩次。兩個源文件裏變量b和函數指針變量main被重複定義了,實際上可以看做代碼段的地址。但編譯器並未報錯,只給出一條警告:

        /usr/bin/ld: Warning: size of symbol 'b' changed from 4 in main.o to 8 in foo.o

        運行程序發現,main.c打印中b大小是4個字節,而foo.c是8個字節,因爲sizeof關鍵字是編譯時決議,而源文件中對b類型定義不一樣。但令人驚奇的是無論是在main.c還是foo.c中,a和b都是相同的地址,也就是說,a和b被定義了兩次,b還是不同類型,但內存映像中只有一份拷貝。我們還看到,main.c中b的值居然就是foo.c中結構體第一個成員變量b.a的值,這證實了前面的推斷——即便存在多次定義,內存中只有一份初始化的拷貝。另外在這裏c是置身事外的一個獨立變量。

        爲何會這樣呢?這涉及到C編譯器對多重定義的全局符號的解析和鏈接。在編譯階段,編譯器將全局符號信息隱含地編碼在可重定位目標文件的符號表裏。這裏有個“強符號(strong)”和“弱符號(weak)”的概念——前者指的是定義並且初始化了的變量,比如foo.c裏的結構體b,後者指的是未定義或者定義但未初始化的變量,比如main.c裏的整型b和c,還有兩個源文件都包含頭文件裏的a。當符號被多重定義時,GNU鏈接器(ld)使用以下規則決議:

      (1) 不允許出現多個相同強符號。
      (2) 如果有一個強符號和多個弱符號,則選擇強符號。
      (3) 如果有多個弱符號,那麼先決議到size最大的那個,如果同樣大小,則按照鏈接順序選擇第一個。

        像上面這個例子中,全局變量a和b存在重複定義。如果我們將main.c中的b初始化賦值,那麼就存在兩個強符號而違反了規則一,編譯器報錯。如果滿足規則二,則僅僅提出警告,實際運行時決議的是foo.c中的強符號。而變量a都是弱符號,所以只選擇一個(按照目標文件鏈接時的順序)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章