static關鍵字是C, C++中都存在的關鍵字, 它主要有三種使用方式, 其中前兩種只指在C語言中使用, 第三種在C++中使用(C,C++中具體細微操作不盡相同, 本文以C++為准).
(1)局部靜態變量
(2)外部靜態變量/函數
(3)靜態數據成員/成員函數
下面就這三種使用方式及注意事項分別說明
一、局部靜態變量
在C/C++中, 局部變量按照存儲形式可分為三種auto, static, register
與auto類型(普通)局部變量相比, static局部變量有三點不同
1. 存儲空間分配不同
auto類型分配在棧上, 屬於動態存儲類別, 占動態存儲區空間, 函數調用結束後自動釋放, 而static分配在靜態存儲區, 在程序整個運行期間都不釋放. 兩者之間的作用域相同, 但生存期不同.
2. static局部變量在所處模塊在初次運行時進行初始化工作, 且只操作一次
3. 對於局部靜態變量, 如果不賦初值, 編譯期會自動賦初值0或空字符, 而auto類型的初值是不確定的. (對於C++中的class對象例外, class的對象實例如果不初始化, 則會自動調用默認構造函數, 不管是否是static類型)
特點: static局部變量的”記憶性”與生存期的”全局性”
所謂”記憶性”是指在兩次函數調用時, 在第二次調用進入時, 能保持第一次調用退出時的值.
示例程序一
#include <iostream>
using namespace std;
void staticLocalVar()
{
static int a = 0; // 運行期時初始化一次, 下次再調用時, 不進行初始化工作
cout<<"a="<<a<<endl;
++a;
}
int main()
{
staticLocalVar(); // 第一次調用, 輸出a=0
staticLocalVar(); // 第二次調用, 記憶了第一次退出時的值, 輸出a=1
return 0;
}
應用:
利用”記憶性”, 記錄函數調用的次數(示例程序一)
利用生存期的”全局性”, 改善”return a pointer / reference to a local object”的問題. Local object的問題在於退出函數, 生存期即結束,. 利用static的作用, 延長變量的生存期.
示例程序二:
// IP address to string format
// Used in Ethernet Frame and IP Header analysis
const char * IpToStr(UINT32 IpAddr)
{
static char strBuff[16]; // static局部變量, 用於返回地址有效
const unsigned char *pChIP = (const unsigned char *)&IpAddr;
sprintf(strBuff, "%u.%u.%u.%u", pChIP[0], pChIP[1], pChIP[2], pChIP[3]);
return strBuff;
}
注意事項:
1. “記憶性”, 程序運行很重要的一點就是可重復性, 而static變量的”記憶性”破壞了這種可重復性, 造成不同時刻至運行的結果可能不同.
2. “生存期”全局性和唯一性. 普通的local變量的存儲空間分配在stack上, 因此每次調用函數時, 分配的空間都可能不一樣, 而static具有全局唯一性的特點, 每次調用時, 都指向同一塊內存, 這就造成一個很重要的問題 ---- 不可重入性!
這樣在多線程程序設計或遞歸程序設計中, 要特別注意這個問題.
下面針對示例程序二, 分析在多線程情況下的不安全性.(為方便描述, 標上行號)
① const char * IpToStr(UINT32 IpAddr)
② {
③ static char strBuff[16]; // static局部變量, 用於返回地址有效
④ const unsigned char *pChIP = (const unsigned char *)&IpAddr;
⑤ sprintf(strBuff, "%u.%u.%u.%u", pChIP[0], pChIP[1], pChIP[2], pChIP[3]);
⑥ return strBuff;
⑦ }
假設現在有兩個線程A,B運行期間都需要調用IpToStr()函數, 將32位的IP地址轉換成點分10進制的字符串形式. 現A先獲得執行機會, 執行IpToStr(), 傳入的參數是0x0B090A0A, 順序執行完應該返回的指針存儲區內容是:”10.10.9.11”, 現執行到⑥時, 失去執行權, 調度到B線程執行, B線程傳入的參數是0xA8A8A8C0, 執行至⑦, 靜態存儲區的內容是192.168.168.168. 當再調度到A執行時, 從⑥繼續執行, 由於strBuff的全局唯一性, 內容已經被B線程沖掉, 此時返回的將是192.168.168.168字符串, 不再是10.10.9.11字符串.
通常,在函數體內定義了一個變量,每當程序運行到該語句時都會給該局部變量分配棧內存。但隨著程序退出函數體,系統就會收回棧內存,局部變量也相應失效。
但有時候我們需要在兩次調用之間對變量的值進行保存。通常的想法是定義一個全局變量來實現。但這樣一來,變量已經不再屬於函數本身了,不再僅受函數的控制,給程序的維護帶來不便。
靜態局部變量正好可以解決這個問題。靜態局部變量保存在全局數據區,而不是保存在棧中,每次的值保持到下一次調用,直到下次賦新值。
靜態局部變量有以下特點:
- 該變量在全局數據區分配內存;
- 靜態局部變量在程序執行到該對象的聲明處時被首次初始化,即以後的函數調用不再進行初始化;
- 靜態局部變量一般在聲明處初始化,如果沒有顯式初始化,會被程序自動初始化為0;
- 它始終駐留在全局數據區,直到程序運行結束。但其作用域為局部作用域,當定義它的函數或語句塊結束時,其作用域隨之結束;
二、外部靜態變量/函數
在C中 static有了第二種含義:用來表示不能被其它文件訪問的全局變量和函數。, 但為了限制全局變量/函數的作用域, 函數或變量前加static使得函數成為靜態函數。但此處“static”的含義不是指存儲方式,而是指對函數的作用域僅局限於本文件(所以又稱內部函數)。注意此時, 對於外部(全局)變量, 不論是否有static限制, 它的存儲區域都是在靜態存儲區, 生存期都是全局的. 此時的static只是起作用域限制作用, 限定作用域在本模塊(文件)內部.
使用內部函數的好處是:不同的人編寫不同的函數時,不用擔心自己定義的函數,是否會與其它文件中的函數同名。
示例程序三:
//file1.cpp
static int varA;
int varB;
extern void funA()
{
……
}
static void funB()
{
……
}
//file2.cpp
extern int varB; // 使用file1.cpp中定義的全局變量
extern int varA; // 錯誤! varA是static類型, 無法在其他文件中使用
extern vod funA(); // 使用file1.cpp中定義的函數
extern void funB(); // 錯誤! 無法使用file1.cpp文件中static函數
靜態全局變量有以下特點:
- 該變量在全局數據區分配內存;
- 未經初始化的靜態全局變量會被程序自動初始化為0(自動變量的值是隨機的,除非它被顯式初始化);
- 靜態全局變量在聲明它的整個文件都是可見的,而在文件之外是不可見的;
靜態變量都在全局數據區分配內存,包括後面將要提到的靜態局部變量。對於一個完整的程序,在內存中的分布情況如下圖:
代碼區
全局數據區
堆區
棧區
一般程序的由new產生的動態數據存放在堆區,函數內部的自動變量存放在棧區。自動變量一般會隨著函數的退出而釋放空間,靜態數據(即使是函數內部的靜 態局部變量)也存放在全局數據區。全局數據區的數據並不會因為函數的退出而釋放空間。細心的讀者可能會發現,Example 1中的代碼中將
static int n; //定義靜態全局變量
改為
int n; //定義全局變量
程序照樣正常運行。的確,定義全局變量就可以實現變量在文件中的共享,但定義靜態全局變量還有以下好處:
- 靜態全局變量不能被其它文件所用;
- 其它文件中可以定義相同名字的變量,不會發生沖突;