此文來自CU的一篇精華帖,將static的知識總結的非常全面到位,故轉來與大家分享。
通常理解static只是指靜態存儲的概念,事實上在c++裏面static包含了兩方面的含義。
1)在固定地址上的分配,這意味着對象是在一個特殊的靜態區域上創建的,而不是每次函數調用的時候在堆棧上動態創建的,這是static的靜態存儲的概念。
2) 另一方面,static能夠控制對象對於連接器的可見性。一個static對象,對於特定的編譯單元來說總是本地範圍的,這個範圍包括本地文件或者本地的某一個類,超過這個範圍的文件或者類是不可以看到static對象的,這同時也描述了連接的概念,它決定連接器能夠看到哪些名字。
一,關於靜態存儲
對於一個完整的程序,一般程序的由malloc,realloc產生的動態數據存放在堆區,程序的局部數據即各個函數內部的數據存放在棧區,局部數據對象一般會隨着函數的退出而釋放空間,對於static數據即使是函數內部的對象則存放在全局數據區,全局數據區的數據並不會因爲函數的退出就將空間釋放。
二,函數內部的靜態變量
通常,在函數體內定義一個變量的時候,編譯器使得每次函數調用時候堆棧的指針向下移動一個合適的位置,爲這些內部變量分配內存。如果這個變量是一個初始化表達式,那麼每當程序運行到這兒的時候程序都需要對表達式進行初始化。這種情況下變量的值在兩次調用之間則不能進行保存。
有的時候我們卻需要在兩次調用之間對變量的值進行保存,通常的想法是定義一個全局變量來實現。但這樣一來,變量已經不再屬於函數本身了,不再僅受函數的控制。因此我們有必要將變量聲明爲static對象,這樣對象將保存在靜態存儲區域,而不是保存在堆棧中。對象的初始化僅僅在函數第一個被調用的時候進行初始化,每次的值保持到下一次調用,直到新的值覆蓋它。下面的例子解釋了這一點。
1. //****************************************
2. // 1.cpp
3. //****************************************
4. #include <iostream.h>;
5. void add_n(void);
6. void main(){
7. int n=0;
8. add_n();
9. add_n();
10. add_n();
11. }
12. void add_n(void){
13. staticn=50;
14. cout<<”n=”<<n<<endl;
15. n++;
16. }
程序運行結果爲:
1. n=50
2. n=51
3. n=52;
從上面的運行結果可以看出static n確實是在每次調用中都保持了上一次的值。如果預定義的靜態變量沒有提供一個初始值的話,編譯器會確保在初始化的時候將用零值爲其初始化。static變量必須被初始化,但是零值初始化僅僅只對系統預定義類型有效,比如int,char,bool等等。事實上我們用到的不僅僅是這些預定義類型,大多數情況下可能用到結構,聯合或者類等各種用戶自定義的類型,對於這些類型用戶必須使用構造函數進行初始化。如果我們在定義一個靜態對象的時候沒有指定構造函數的參數,那就必須使用缺省的構造函數,如果缺省的構造函數也沒有的話則會出錯。看下面的例子。
1. //**************************************************
2. // 2.cpp
3. //**************************************************
4. #include <isotream.h>;
5. class x{
6. int i;
7. public:
8. x(int i=0):i(i){
9. cout<<”i=”<<i<<endl;
10. }//缺省構造函數
11. ~x(){cout<<”x::~x()”<<endl;
12. };
13. void fn(){
14. staticx x1(47);
15. staticx x2;
16. }
17. main(){
18. fn();
19. }
程序運行結果如下:
1. i=47
2. i=0
3. x::~x()
4. x::~x()
從上面的例子可以看出靜態的x對象既可以使用帶參數的構造函數進行初始化,象x1,也可以使用缺省的構造函數,象x2。程序控制第一次轉到對象的定義點時候,而且只有第一次的時候才需要執行構造函數。如果對象既沒有帶參數的構造函數又沒有缺省構造函數則程序將無法通過編譯。
三,類中的靜態成員
static靜態存儲的概念可以進一步引用到類中來。c++中的每一個類的對象都是該類的成員的拷貝,一般情況下它們彼此之間沒有什麼聯繫,但有時候我們需要讓它們之間共享一些數據,我們可以通過全局變量來實現,但是這樣的結果是導致程序的不安全性,因爲任何函數都可以對這個變量進行訪問和修改,而且容易與項目中的其餘的名字產生衝突,因此我們需要一種兩全其美的方法,既可以當成全局數據變量那樣存儲,又可以隱藏在類的內部,與類本身聯繫起來,這樣只有類的對象纔可以操縱這個變量,從而增加了變量的安全性。
這種變量我們稱之爲類的靜態成員,靜態成員包括靜態數據成員和靜態成員函數。類的所有的靜態數據成員有着單一的存儲空間而不管類的對象由多少,這些對象共享這塊存儲區域。因此每一個類的對象的靜態成員發生改變都會對其餘的對象產生影響。先看下面的例子。
1. //**************************************
2. // student.cpp
3. //**************************************
4. 1. #include <iostream.h>;
5. 2. #include <string.h>;
6. 3. class student{
7. 4. public:
8. 5. student(char* pname=”no name”){
9. 6. cout<<”create one student”<endl;
10. 7. strcpy(name,pname);
11. 8. number++;
12. 9. cout<<number<<endl;
13. 10. }
14. 11. ~student() {
15. 12. cout<<”destruct one student”<<endl;
16. 13. number--;
17. 14. cout<<number<<endl;
18. 15. }
19. 16. static number(){
20. 17. return number; }
21. 18. protected:
22. 19. char nme[40]
23. 20. staticint number;
24. 21. }
25. 22. void fn(){
26. 23. student s1;
27. 24. student s2;
28. 25. cout<<student::number<<endl;
29. 26. }
30. 27. main(){
31. 28. fn();
32. 29. cout<<student::number<<endl;
33. 30. }
程序輸出結果如下:
1. create one student
2. 1
3. create one student
4. 2
5. 2
6. destruct one student
7. 1
8. destruct one student
9. 0
10. 0
上面的程序代碼中我們使用了靜態數據成員和靜態函數成員,下面我們先闡述靜態數據成員。
四,靜態數據成員
在代碼中我們可以看出,number既不是對象s1也不是對象s2的一部分,它是屬於student這個類的。每一個student對象都有name成員,但是number成員卻只有一個,所有的student對象共享使用這個成員。s1.number與s2.number是等值的。在student的對象空間中沒有爲number成員保留空間,它的空間分配不在student的構造函數裏完成,空間回收也不再析構函數裏面完成。因此與name成員不一樣,它不會隨着對象的產生或者消失而產生或消失。
由於靜態數據成員的空間分配在全局數據區,因此在程序一開始運行的時候就必須存在,所以靜態成員的空間的分配和初始化不可能在函數包括main主函數中完成,因爲函數在程序中被調用的時候才爲內部的對象分配空間。這樣,靜態成員的空間分配和初始化只能由下面的三種途徑。一是類的外部接口的頭文件,那裏聲明瞭類定義。二是類定義的內部實現,那裏有類成員函數的定義和具體實現。三是應用程序的main()函數前的全局數據聲明和定義處。由於靜態數據成員必須實際的分配空間,因此不可能在類定義頭文件中分配內存。另一方面也不能在頭文件中類聲明的外部定義,因爲那會造成多個使用該類的源程序重複出現定義。靜態數據成員也不能在main()函數的全局數據聲明處定義。如果那樣的話每一個使用該類的程序都必須在程序的main()函數的全局數據聲明處定義一下該類的靜態成員。這是不現實的。唯一的辦法就是將靜態數據成員的定義放在類的實現中。定義時候用類名引導,引用時包含頭文件即可。
例如:
1. class a{
2. staticint i;
3. public:
4. //
5. }
6. 在類定義文件中 int a::i=1;
有兩點需要注意的是
1.靜態數據成員必須且只能定義一次,如果重複定義,連接器會報告錯誤。同時它們的初始化必須在定義的時候完成。對於預定義類型的靜態數據成員,如果沒有賦值則賦零值。對於用戶自定義類型必須通過構造函數賦值,如果沒有構造函數包括缺省構造函數,編譯無法通過。
2. 局部類中不允許出現靜態數據成員。因爲靜態數據成員必須在程序運行時候就存在這導致程序無法爲局部類中的靜態數據成員分配空間。下面的代碼是不允許的。
1. void fn(){
2. class foo{
3. staticint i;//定義非法。
4. public:
5. // }
6. }
五,靜態函數成員
與靜態數據成員一樣,我們也可以創建一個靜態成員函數,它爲類的全部服務而不是爲某一個類的具體對象服務。靜態函數成員與靜態成員一樣,都是類的內部實現,屬於類定義的一部分。程序student.cpp中的number()就是靜態成員函數,它的定義位置與一般成員函數相同。
普通的成員函數一般都隱含了一個this指針,this指針指向類的對象本身,因爲普通成員函數總是具體的屬於某個類的具體對象的。通常情況下this是缺省的。如函數add()實際上是寫成this.add().但是與普通函數相比,靜態成員函數由於不是與任何的對象相聯繫因此它不具有this指針,從這個意義上講,它無法訪問屬於具體類對象的非靜態數據成員,也無法訪問非靜態成員函數,它只能調用其餘的靜態成員函數。如下面的代碼:
1. 1 class x {
2. 2 int i;
3. 3 staticint j;
4. 4 public:
5. 5 x(int i=0):i(i){
6. 6 j=i
7. 7 }//非靜態成員函數可以訪問靜態函數成員和靜態數據成員。
8. 8 int val() const {return i;}
9. 9 staticint incr(){
10. 10 i++;//錯誤的代碼,因爲i是非靜態成員,因此incr()作爲靜態成員函數無
11. 11 //法訪問它。
12. 12 return ++j;
13. 13 }
14. 14 staticint fn(){
15. 15 return incr();//合法的訪問,因爲fn()和incr()作爲靜態成員函數可以互相訪
16. 16 //問。
17. 17 }
18. 18 };
19. 19 int x::j=0;
20. 20 main(){……}
根據上面的程序可以總結幾點:
1. 靜態成員之間可以相互的訪問包括靜態成員函數訪問靜態數據成員和訪問靜態成員函數。
2. 非靜態成員函數可以任意的訪問靜態成員函數和靜態數據成員。
3. 靜態成員函數不能訪問非靜態成員函數和非靜態數據成員。
由於沒有this指針的額外開銷,因此靜態成員函數與全局函數相比速度上會有少許的增長。
六,理解控制連接
理解控制連接之前我們先了解一下外部存儲類型的概念。一般的在程序的規模很小的情況下我們用一個源程序既可以表達完整。但事實上稍微有價值的程序都不可能只用一個程序來表達的,而是分割成很多的小的模塊,每一個模塊完成特定的功能,構成一個源文件。所有的源文件共享一個main函數。在不同的源文件之間爲了能夠相互進行數據或者函數的溝通,這時候通常聲明數據或者函數爲extern,這樣用extern聲明的數據或者函數就是全局變量或者函數。默認情況下的函數的聲明總是extern的。在文件範圍內定義的全局數據和函數對程序中的所有的編譯單元來說都是可見的。這就是通常我們所說的外部連接。
但有時候我們可能想限制一個名字對象的可見性,想讓一個對象在本地文件範圍內是可見的,這樣這個文件中的所有的函數都可以利用這個對象,但是不想讓本地文件之外的其餘文件看到或者訪問該對象,或者外部已經定義了一個全局對象,防止內部的名字對象與之衝突。
通過在全局變量的前面加上static,我們可以做到這一點。在文件的範圍內,一個被聲明爲static的對象或者函數的名字對於編譯單元來說是局部變量,我們稱之爲靜態全部變量。這些名字不使用默認情況下的外部連接,而是使用內部連接。從下面的例子可以看出static是如何控制連接的。
工程文件爲first.prj由兩個源文件組成。
1. exam1.cpp
2. exam2.cpp
3. //***************************************
4. // exam1.cpp
5. //***************************************
6. 1 #include <iostream.h>;
7. 2 int n;
8. 3 void print_n();
9. 4 void main()
10. 5 {
11. 6 n=20;
12. 7 cout<<n<<endl;
13. 8 print_n();
14. 9 }
15.
16. //****************************************
17. // exam2.cpp
18. //****************************************
19. 10 staticn;
20. 11 staticvoid staticfn();
21. 12 void print_n()
22. 13 {
23. 14 n++;
24. 15 cout<<n<<endl;
25. 16 staticfn();
26. 17 }
27. 18 void staticfn()
28. 19 {
29. 20 cout<<n++<<endl;
30. 21 }
程序運行結果如下
1. 20
2. 1
3. 1
下面我們將對上面的程序進行少量的改造,在看看執行結果如何。
改造一:
1. 將exam1.cpp的第二行int n改爲extern int n,這就是告訴程序我在此處聲明瞭變量n,但是真正的定義過程在別的文件中,此處就是exam2.cpp。但事實上exam2.cpp中僅僅聲明瞭static int n。我們看看運行結果。vc中會通過編譯,但是在進行連接時候會給
一個“變量n找不到”的錯誤。這說明exam1.cpp無法共享exam2.cpp中的static int n變量。
改造二:
1. 我們在exam1.cpp的第二行和第三行之間增加void staticfn();同時在第八行和第九行之間增加staticfn()的調用。再看執行結果。vc會產生一個找不到函數staticfn的錯誤。這說明exam1.cpp無法共享exam2.cpp中的staticfn()。
從上面的結論可以看出下面幾點:
1. static解決了名字的衝突問題。使得可以在源文件中建立並使用與其它源文件甚至全局變量一樣的名字而不會導致衝突的產生。這一點在很大的項目中是很有用處的。
2. 聲明爲靜態的函數不能被其他的源文件所調用,因爲它的名字只對本地文件可見,其餘的文件無法獲取它的名字,因此不可能進行連接。
3. 在文件作用域下聲明的inline函數默認情況下認爲是static類型。在文件作用域下聲明的const的常量默認情況下也是static存儲類型的。