對C語言中的static關鍵字的深入理解

對C語言中的static關鍵字的深入理解

在閱讀一些項目源代碼時,我發現很多時候,會把函數和變量聲明爲static,所以,很好奇爲什麼要這樣做,於是有了下面這篇文章。

基本概念

使用static有三種情況:

  • 函數內部static變量
  • 函數外部static變量
  • static函數

函數內部的static變量,關鍵在於生命週期持久,他的值不會隨着函數調用的結束而消失,下一次調用時,static變量的值,還保留着上次調用後的內容。

函數外部的static變量,以及static函數,關鍵在於私有性,它們只屬於當前文件,其它文件看不到他們。例如:

/* test_static1.c */
#include <stdio.h>

void foo() {
}

static void bar() {
}

int i = 3;
static int j = 4;

int main(void){
    printf ("%d \n", i);
    printf ("%d \n", j);
    return 0;
}


/* test_static2.c */
void foo() {
}

static void bar() {
}

int i = 1;
static int j = 2;

將兩個文件一起編譯

gcc test_static1.c test_static2.c -o test_static

編譯器會提示:

/tmp/ccuerF9V.o: In function `foo':
test_static2.c:(.text+0x0): multiple definition of `foo'
/tmp/cc9qncdw.o:test_static1.c:(.text+0x0): first defined here
/tmp/ccuerF9V.o:(.data+0x0): multiple definition of `i'
/tmp/cc9qncdw.o:(.data+0x0): first defined here
collect2: ld returned 1 exit status

把與非static變量i相的語句註釋掉就不會有此提示i重複定義了,原因就在於使用static聲明後,變量私有化了,不同文件中的同名變量不會相互chong_tu。

static 函數也與此類似,將函數聲明爲static,說明我們只在當前文件中使用這個函數,其它文件看不到,即使重名,也不會相互chong_tu。

深入理解

從來就不應該僅僅滿足於瞭解現象,還要了解現象的背後有什麼

爲什麼函數內部的static變量和普通函數變量生命週期不一樣

我們的程序,從源代碼經過編譯,鏈接生成了可執行文件,可執行文件被加載到存儲器中,然後執行。以Unix程序爲例,每個Unix程序都有一個運行時存儲器映像。可以理解爲程序運行時,存儲器中的數據分佈。

linux_rtmi

圖1 Linux運行時存儲器映像

當程序運行時,操作系統會創建用戶棧(User stack),一個函數被調用時,它的參數,局部變量,返回地址等等,都會被壓入棧中,當函數執行結束後,這些數據就會被其它函數使用,所以函數調用結束後,局部變量的值不會被保持。我們將此區域放大,可以看到用戶棧中都有哪些內容。

sfs

圖2 棧幀結構

而static變量與普通局部變量不同,它不是保留在棧中。注意圖一中,有一塊區域,"Loaded from executable file",其中有一塊 .data, .bss區,static變量會被存儲在這裏,所以函數調用結束後,static變量的值仍然會得到保留。而 .data, .bss區,executable file,與程序的編譯,鏈接,相關。

首先,多個源代碼會分別被編譯成可重定位目標程序,然後鏈接器會最終生成可執行目標程序。可重定位目標程序的結構如圖3所示,可以看出,此時,.data, .bss區,已經出現。

re

圖3 可重定位目標程序

.data 區存儲已經初始化的全局C變量,.bss 區存儲沒有初始化的全局C變量,所以這兩個區域又被稱爲全局區。而編譯器會爲每個static變量在.data或者.bss中分配空間。

可執行目標程序的結構如圖4所示

ee

圖4 可執行目標程序

將圖4與圖1比較,就會發現,可執行目標程序的一部分被加載到存儲器中,這就是"Loaded from executable file"的來源。

另外,從圖一中,也可以看出,使用malloc分配的內存空間,與函數局部變量,static變量的不同。

爲什麼函數外部的static變量及static函數只對文件內部可見

要解釋這個問題,我們首先要理解問題本身。這個問題的本質其實是,當我們遇到一個變量或者函數時,我們去哪裏尋找它,static變量/函數與普通變量/函數的尋找方式有什麼不同。

我們回到剛纔的例子,這一次,仔細地觀察編譯鏈接時的提示信息:

/* test_static1.c */
#include <stdio.h>

void foo() {
}

static void bar() {
}

int i = 3;
static int j = 4;

int main(void){
    printf ("%d \n", i);
    printf ("%d \n", j);
    return 0;
}


/* test_static2.c */
void foo() {
}

static void bar() {
}

int i = 1;
static int j = 2;

將兩個文件一起編譯

gcc test_static1.c test_static2.c -o test_static

編譯器會提示:

/tmp/ccuerF9V.o: In function `foo':
test_static2.c:(.text+0x0): multiple definition of `foo'
/tmp/cc9qncdw.o:test_static1.c:(.text+0x0): first defined here
/tmp/ccuerF9V.o:(.data+0x0): multiple definition of `i'
/tmp/cc9qncdw.o:(.data+0x0): first defined here
collect2: ld returned 1 exit status

你會發現,雖然我們只用了一條命令對兩個文件進行編譯鏈接,但是,實際上,兩個源文件是被分別編譯成/tmp/ccuerF9V.o及/tmp/cc9qncdw.o,並且,錯誤並不是出現在編譯時,而是出現在鏈接時,鏈接器ld返回了1。鏈接是把兩個可重新定位的目標程序,組合在一起,組合的時候,我們發現了變量i及函數foo的定義出現chong_tu。而聲明爲static的變量j及函數bar並沒有提示chong_tu。

這說明,在ld進行鏈接時,需要進行某種檢查,去發現chong_tu。ld的輸入是每個源文件生成的可重定位目標文件,那麼這些目標文件裏一定會有一些信息,告訴ld它們有什麼變量,然後ld才能檢查是不是有chong_tu。

說起可重定位目標文件,我們一直都沒有解釋爲什麼要重定位。其實這很好理解,一個源文件編譯後,如果生成的目標文件中,各個地址就是最終運行時的地址,那麼這些地址很可能會和其它文件中的地址chong_tu。因爲編譯一個文件時,我們不會知道有其它文件的存在,所以編譯時無法確定最終的地址。因此,編譯單個文件時,生成的目標文件中的地址都是從0開始,鏈接時,鏈接器會將不同目標文件中的地址重新定位,最終生成可執行文件。注意這裏的chong_tu和前面說的chong_tu不是一回事,這裏的chong_tu是不同的可重定位目標文件中相同地址的chong_tu,前面一段講的是同名變量之間的chong_tu。

此時,我們不得不回到可重定位目標文件的格式。

re

圖3 可重定位目標程序

注意 .symtab節,這個節存儲符號表,假設當前可重定位目標模塊爲m, 符號表會告訴我們m中定義和引用的符號信息,主要分爲:

  • m定義,並可以被其它模塊引用的全局符號:m中的非static函數,非static全局變量。
  • 由其它模塊定義,並被m引用的全局符號:m中使用extern聲明的變量
  • 只被m引用的本地符號:m中的static函數,static全局變量。

現在編譯一下,然後用GNU READELF工具看一下符號表。

    $ gcc -c test_static1.c -o test_static1.o
    $ readelf -s test_static1.o

Symbol table '.symtab' contains 15 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS test_static1.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1 
     3: 00000000     0 SECTION LOCAL  DEFAULT    3 
     4: 00000000     0 SECTION LOCAL  DEFAULT    4 
     5: 00000005     5 FUNC    LOCAL  DEFAULT    1 bar
     6: 00000004     4 OBJECT  LOCAL  DEFAULT    3 j
     7: 00000000     0 SECTION LOCAL  DEFAULT    5 
     8: 00000000     0 SECTION LOCAL  DEFAULT    7 
     9: 00000000     0 SECTION LOCAL  DEFAULT    8 
    10: 00000000     0 SECTION LOCAL  DEFAULT    6 
    11: 00000000     5 FUNC    GLOBAL DEFAULT    1 foo
    12: 00000000     4 OBJECT  GLOBAL DEFAULT    3 i
    13: 0000000a    62 FUNC    GLOBAL DEFAULT    1 main
    14: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

表的數據結構不解釋,有興趣,看擴展閱讀部分。

現在,假如你是鏈接器ld,我給你2個可重定位目標程序,你從中得到兩個符號表,這時候,你就可以檢查出兩個符號表是否存在chong_tu了。

由於全局符號可能會定義相同的名字,鏈接器會有一套規則,來確定選擇哪個符號。符號分爲強符號與弱符號。

  • 強符號:函數和已經初始化的全局變量是強符號
  • 弱符號:未初始化的全局變量是弱符號

處理相同名字的全局符號的規則是:

  1. 不允許有多個強符號
  2. 如果有一個強符號,多個弱符號,那麼選擇強符號
  3. 如果有多個弱符號,那麼從中任意選擇一個

明白了這些規則,你其實可以明白很多事情,不僅僅包括什麼時候,變量名,函數名會chong_tu,還包括爲什麼要儘量避免使用全局變量,爲什麼要使用static把數據私有化。看看規則3,“任意”兩個字,有沒有讓你感覺有一絲不適。

這也是爲什麼我們要探索事物背後機理的原因,不僅僅是在出現錯誤時,我們知道哪裏有問題,還幫助我們寫出更健壯的程序。

擴展閱讀

《深入理解計算機系統》(Computer Systems, A Programmer's Perspective): 第七章 鏈接


注意:文章中用拼音表示的詞代表“敏感詞”,不這樣無法保存。

發佈了91 篇原創文章 · 獲贊 235 · 訪問量 71萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章