程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C語言全局變量那些事兒(1)

C語言全局變量那些事兒(1)

編輯:C++入門知識

作為一名程序員,如果說沉迷一門編程語言算作一種樂趣的話,那麼與此同時反過來去黑一門編程語言就是這種樂趣的升華。今天我們就來黑一把C語言,好好展示一下這門經典語言令人抓狂的一面。

我們知道,全局變量是C語言語法和語義中一個很重要的知識點,首先它的存在意義需要從三個不同角度去理解:對於程序員來說,它是一個記錄內容的變量(variable);對於編譯/鏈接器來說,它是一個需要解析的符號(symbol);對於計算機來說,它可能是具有地址的一塊內存(memory)。其次是語法/語義:從作用域上看,帶static關鍵字的全局變量范圍只能限定在文件裡,否則會外聯到整個模塊和項目中;從生存期來看,它是靜態的,貫穿整個程序或模塊運行期間注意,正是跨單元訪問和持續生存周期這兩個特點使得全局變量往往成為一段受攻擊代碼的突破口,了解這一點十分重要);從空間分配上看,定義且初始化的全局變量在編譯時在數據段(.data)分配空間,定義但未初始化的全局變量暫存(tentative definition)在.bss段,編譯時自動清零,而僅僅是聲明的全局變量只能算個符號,寄存在編譯器的符號表內,不會分配空間,直到鏈接或者運行時再重定向到相應的地址上。

我們將向您展現一下,非static限定全局變量在編譯/鏈接以及程序運行時會發生哪些有趣的事情,順便可以對C編譯器/鏈接器的解析原理管中窺豹。以下示例對ANSI C和GNU C標准都有效,筆者的編譯環境是Ubuntu下的GCC-4.4.3。

  1. /* t.h */ 
  2. #ifndef _H_ 
  3. #define _H_ 
  4. int a; 
  5. #endif 
  6.   
  7. /* foo.c */ 
  8. #include <stdio.h> 
  9. #include "t.h" 
  10.   
  11. struct { 
  12.    char a; 
  13.    int b; 
  14. } b = { 2, 4 }; 
  15.   
  16. int main(); 
  17.   
  18. void foo() 
  19.     printf("foo:\t(&a)=0x%08x\n\t(&b)=0x%08x\n 
  20.         \tsizeof(b)=%d\n\tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n", 
  21.         &a, &b, sizeof b, b.a, b.b, main); 
  22.   
  23. /* main.c */ 
  24. #include <stdio.h> 
  25. #include "t.h" 
  26.   
  27. int b; 
  28. int c; 
  29.   
  30. int main() 
  31.     foo(); 
  32.     printf("main:\t(&a)=0x%08x\n\t(&b)=0x%08x\n 
  33.         \t(&c)=0x%08x\n\tsize(b)=%d\n\tb=%d\n\tc=%d\n", 
  34.         &a, &b, &c, sizeof b, b, c); 
  35.     return 0; 

Makefile如下:

  1. test: main.o foo.o 
  2.     gcc -o test main.o foo.o 
  3.   
  4. main.o: main.c 
  5. foo.o: foo.c 
  6.   
  7. clean: 
  8.     rm *.o test 

運行情況:

  1. foo:    (&a)=0x0804a024 
  2.     (&b)=0x0804a014 
  3.     sizeof(b)=8 
  4.     b.a=2 
  5.     b.b=4 
  6.     main:0x080483e4 
  7. main:   (&a)=0x0804a024 
  8.     (&b)=0x0804a014 
  9.     (&c)=0x0804a028 
  10.     size(b)=4 
  11.     b=2 
  12.     c=0 

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

  1. /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)使用以下規則決議:

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

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

事實上,這種規則是C語言裡的一個大坑,編譯器對這種全局變量多重定義的“縱容”很可能會無端修改某個變量,導致程序不確定行為。如果你還沒有意識到事態嚴重性,我再舉個例子。


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