解析static!(轉)

post0發表於2007-08-11
解析static!(轉)[@more@]

[size=18:7291e6e16b][color=darkblue:7291e6e16b][b:7291e6e16b]解析static![/b:7291e6e16b][/color:7291e6e16b][/size:7291e6e16b]

解析static&index=58

通常理解static只是指靜態儲存的概念,事實上在c++裡面static包含了兩方面的含義。

1)在固定地址上的分配,這意味著物件是在一個特殊的靜態區域上建立的,而不是每次函式呼叫的時候在堆疊上動態建立的,這是static的靜態儲存的概念。

2) 另一方面,static能夠控制物件對於聯結器的可見性。一個static物件,對於特定的編譯單元來說總是本地範圍的,這個範圍包括本地檔案或者本地的某一個類,超過這個範圍的檔案或者類是不可以看到static物件的,這同時也描述了連線的概念,它決定聯結器能夠看到哪些名字。

[b:7291e6e16b]一,關於靜態儲存[/b:7291e6e16b]

對於一個完整的程式,他們在記憶體中的分佈情況如下圖:

[img:7291e6e16b]http://asp.6to23.com/vcprogram/articlemanage/image/static.jpg[/img:7291e6e16b]

一般程式的由malloc,realloc產生的動態資料存放在堆區,程式的區域性資料即各個函式內部的資料存放在棧區,區域性資料物件一般會隨著函式的退出而釋放空間,對於static資料即使是函式內部的物件則存放在全域性資料區,全域性資料區的資料並不會因為函式的退出就將空間釋放。

[b:38f65016cf]二,函式內部的靜態變數[/b:38f65016cf]

通常,在函式體內定義一個變數的時候,編譯器使得每次函式呼叫時候堆疊的指標向下移動一個合適的位置,為這些內部變數分配記憶體。如果這個變數是一個初始化表示式,那麼每當程式執行到這兒的時候程式都需要對錶達式進行初始化。這種情況下變數的值在兩次呼叫之間則不能進行儲存。

有的時候我們卻需要在兩次呼叫之間對變數的值進行儲存,通常的想法是定義一個全域性變數來實現。但這樣一來,變數已經不再屬於函式本身了,不再僅受函式的控制。因此我們有必要將變數宣告為static物件,這樣物件將儲存在靜態儲存區域,而不是儲存在堆疊中。物件的初始化僅僅在函式第一個被呼叫的時候進行初始化,每次的值保持到下一次呼叫,直到新的值覆蓋它。下面的例子解釋了這一點。

[code:1:38f65016cf]//****************************************

// 1.cpp

//****************************************

1 #include

2 void add_n(void);

3 void main(){

4 int n=0;

5 add_n();

6 add_n();

7 add_n();

8 }

9 void add_n(void){

10 staticn=50;

11 cout<12 n++;

13 }[/code:1:38f65016cf]

程式執行結果為

[code:1:38f65016cf]n=50

n=51

n=52;[/code:1:38f65016cf]

HopeCao 回覆於:2003-06-18 14:32:41

從上面的執行結果可以看出static n確實是在每次呼叫中都保持了上一次的值。如果預定義的靜態變數沒有提供一個初始值的話,編譯器會確保在初始化的時候將用零值為其初始化。static變數必須被初始化,但是零值初始化僅僅只對系統預定義型別有效,比如int,char,bool等等。事實上我們用到的不僅僅是這些預定義型別,大多數情況下可能用到結構,聯合或者類等各種使用者自定義的型別,對於這些型別使用者必須使用建構函式進行初始化。如果我們在定義一個靜態物件的時候沒有指定建構函式的引數,那就必須使用預設的建構函式,如果預設的建構函式也沒有的話則會出錯。看下面的例子。

[code:1:30a24d7c00]//**************************************************

// 2.cpp

//**************************************************

1 #include

2 class x{

3 int i;

4 public:

5 x(int i=0):i(i){

6 cout<7 }//預設建構函式

8 ~x(){cout<9 };

10 void fn(){

11 staticx x1(47);

12 staticx x2;

13 }

14 main(){

15 fn();

16 }[/code:1:30a24d7c00]

程式執行結果如下

[code:1:30a24d7c00]i=47

i=0

x::~x()

x::~x()

[/code:1:30a24d7c00]

HopeCao 回覆於:2003-06-18 14:34:19

從上面的例子可以看出靜態的x物件既可以使用帶引數的建構函式進行初始化,象x1,也可以使用預設的建構函式,象x2。程式控制第一次轉到物件的定義點時候,而且只有第一次的時候才需要執行建構函式。如果物件既沒有帶引數的建構函式又沒有預設建構函式則程式將無法透過編譯。

[b:eaa8a56606]三,類中的靜態成員[/b:eaa8a56606]

static靜態儲存的概念可以進一步引用到類中來。c++中的每一個類的物件都是該類的成員的複製,一般情況下它們彼此之間沒有什麼聯絡,但有時候我們需要讓它們之間共享一些資料,我們可以透過全域性變數來實現,但是這樣的結果是導致程式的不安全性,因為任何函式都可以對這個變數進行訪問和修改,而且容易與專案中的其餘的名字產生衝突,因此我們需要一種兩全其美的方法,既可以當成全域性資料變數那樣儲存,又可以隱藏在類的內部,與類本身聯絡起來,這樣只有類的物件才可以操縱這個變數,從而增加了變數的安全性。

這種變數我們稱之為類的靜態成員,靜態成員包括靜態資料成員和靜態成員函式。類的所有的靜態資料成員有著單一的儲存空間而不管類的物件由多少,這些物件共享這塊儲存區域。因此每一個類的物件的靜態成員發生改變都會對其餘的物件產生影響。先看下面的例子。

[code:1:eaa8a56606]//**************************************

// student.cpp

//**************************************

1. #include

2. #include

3. class student{

4. public:

5. student(char* pname=”no name”){

6. cout<7. strcpy(name,pname);

8. number++;

9. cout<10. }

11. ~student() {

12. cout<13. number--;

14. cout<15. }

16. static number(){

17. return number; }

18. protected:

19. char nme[40]

20. staticint number;

21. }

22. void fn(){

23. student s1;

24. student s2;

25. cout<<:number>26. }

27. main(){

28. fn();

29. cout<<:number>30. }[/code:1:eaa8a56606]

程式輸出結果如下:

[code:1:eaa8a56606]create one student

1

create one student

2

2

destruct one student

1

destruct one student

0

0[/code:1:eaa8a56606]

上面的程式程式碼中我們使用了靜態資料成員和靜態函式成員,下面我們先闡述靜態資料成員。

HopeCao 回覆於:2003-06-18 14:38:32

[b:5282547077]四,靜態資料成員[/b:5282547077]

在程式碼中我們可以看出,number既不是物件s1也不是物件s2的一部分,它是屬於student這個類的。每一個 student物件都有name成員,但是number成員卻只有一個,所有的student物件共享使用這個成員。s1.number與 s2.number是等值的。在student的物件空間中沒有為number成員保留空間,它的空間分配不在student的建構函式里完成,空間回收也不再解構函式里面完成。因此與name成員不一樣,它不會隨著物件的產生或者消失而產生或消失。

由於靜態資料成員的空間分配在全域性資料區,因此在程式一開始執行的時候就必須存在,所以靜態成員的空間的分配和初始化不可能在函式包括main主函式中完成,因為函式在程式中被呼叫的時候才為內部的物件分配空間。這樣,靜態成員的空間分配和初始化只能由下面的三種途徑。一是類的外部介面的標頭檔案,那裡宣告瞭類定義。二是類定義的內部實現,那裡有類成員函式的定義和具體實現。三是應用程式的main()函式前的全域性資料宣告和定義處。由於靜態資料成員必須實際的分配空間,因此不可能在類定義標頭檔案中分配記憶體。另一方面也不能在標頭檔案中類宣告的外部定義,因為那會造成多個使用該類的源程式重複出現定義。靜態資料成員也不能在main()函式的全域性資料宣告處定義。如果那樣的話每一個使用該類的程式都必須在程式的 main()函式的全域性資料宣告處定義一下該類的靜態成員。這是不現實的。唯一的辦法就是將靜態資料成員的定義放在類的實現中。定義時候用類名引導,引用時包含標頭檔案即可。

例如[code:1:5282547077]class a{

staticint i;

public:

//

}

在類定義檔案中 int a::i=1;[/code:1:5282547077]

[color=blue:5282547077]有兩點需要注意的是[/color:5282547077]

[color=blue:5282547077]1.靜態資料成員必須且只能定義一次,如果重複定義,聯結器會報告錯誤。同時它們的初始化必須在定義的時候完成。對於預定義型別的靜態資料成員,如果沒有賦值則賦零值。對於使用者自定義型別必須透過建構函式賦值,如果沒有建構函式包括預設建構函式,編譯無法透過。

2. 區域性類中不允許出現靜態資料成員。因為靜態資料成員必須在程式執行時候就存在這導致程式無法為區域性類中的靜態資料成員分配空間。下面的程式碼是不允許的。[/color:5282547077]

[code:1:5282547077]void fn(){

class foo{

staticint i;//定義非法。

public:

// }

}[/code:1:5282547077]

HopeCao 回覆於:2003-06-18 14:43:07

[b:4008a128d4]五,靜態函式成員[/b:4008a128d4]

與靜態資料成員一樣,我們也可以建立一個靜態成員函式,它為類的全部服務而不是為某一個類的具體物件服務。靜態函式成員與靜態成員一樣,都是類的內部實現,屬於類定義的一部分。程式student.cpp中的number()就是靜態成員函式,它的定義位置與一般成員函式相同。

普通的成員函式一般都隱含了一個this指標,this指標指向類的物件本身,因為普通成員函式總是具體的屬於某個類的具體物件的。通常情況下 this是預設的。如函式add()實際上是寫成this.add().但是與普通函式相比,靜態成員函式由於不是與任何的物件相聯絡因此它不具有 this指標,從這個意義上講,它無法訪問屬於具體類物件的非靜態資料成員,也無法訪問非靜態成員函式,它只能呼叫其餘的靜態成員函式。如下面的程式碼

[code:1:4008a128d4]1 class x {

2 int i;

3 staticint j;

4 public:

5 x(int i=0):i(i){

6 j=i

7 }//非靜態成員函式可以訪問靜態函式成員和靜態資料成員。

8 int val() const {return i;}

9 staticint incr(){

10 i++;//錯誤的程式碼,因為i是非靜態成員,因此incr()作為靜態成員函式無

11     //法訪問它。

12 return ++j;

13 }

14 staticint fn(){

15 return incr();//合法的訪問,因為fn()和incr()作為靜態成員函式可以互相訪

16 //問。

17 }

18 };

19 int x::j=0;

20 main(){……}[/code:1:4008a128d4]

li2002 回覆於:2003-06-18 14:44:15

好啊,以前不瞭解static,現在知道了,建議加入精華!

HopeCao 回覆於:2003-06-18 14:49:34

[color=blue:17500aa702]根據上面的程式可以總結幾點:

1. 靜態成員之間可以相互的訪問包括靜態成員函式訪問靜態資料成員和訪問靜態成員函式。

2. 非靜態成員函式可以任意的訪問靜態成員函式和靜態資料成員。

3. 靜態成員函式不能訪問非靜態成員函式和非靜態資料成員。[/color:17500aa702]

由於沒有this指標的額外開銷,因此靜態成員函式與全域性函式相比速度上會有少許的增長。

[b:17500aa702]六,理解控制連線[/b:17500aa702]

理解控制連線之前我們先了解一下外部儲存型別的概念。一般的在程式的規模很小的情況下我們用一個源程式既可以表達完整。但事實上稍微有價值的程式都不可能只用一個程式來表達的,而是分割成很多的小的模組,每一個模組完成特定的功能,構成一個原始檔。所有的原始檔共享一個main函式。在不同的原始檔之間為了能夠相互進行資料或者函式的溝通,這時候通常宣告資料或者函式為extern,這樣用extern宣告的資料或者函式就是全域性變數或者函式。預設情況下的函式的宣告總是extern的。在檔案範圍內定義的全域性資料和函式對程式中的所有的編譯單元來說都是可見的。這就是通常我們所說的外部連線。

但有時候我們可能想限制一個名字物件的可見性,想讓一個物件在本地檔案範圍內是可見的,這樣這個檔案中的所有的函式都可以利用這個物件,但是不想讓本地檔案之外的其餘檔案看到或者訪問該物件,或者外部已經定義了一個全域性物件,防止內部的名字物件與之衝突。

透過在全域性變數的前面加上static,我們可以做到這一點。在檔案的範圍內,一個被宣告為static的物件或者函式的名字對於編譯單元來說是區域性變數,我們稱之為靜態全部變數。這些名字不使用預設情況下的外部連線,而是使用內部連線。從下面的例子可以看出static是如何控制連線的。

工程檔案為first.prj由兩個原始檔組成。

[code:1:17500aa702]exam1.cpp

exam2.cpp

//***************************************

// exam1.cpp

//***************************************

1 #include

2 int n;

3 void print_n();

4 void main()

5 {

6 n=20;

7 cout<8 print_n();

9 }

//****************************************

// exam2.cpp

//****************************************

10 staticn;

11 staticvoid staticfn();

12 void print_n()

13 {

14 n++;

15 cout<16 staticfn();

17 }

18 void staticfn()

19 {

20 cout<21 }[/code:1:17500aa702]

程式執行結果如下

[code:1:17500aa702]20

1

1[/code:1:17500aa702]

下面我們將對上面的程式進行少量的改造,在看看執行結果如何。

[color=darkred:17500aa702]改造一:[/color:17500aa702]

[code:1:17500aa702]將exam1.cpp的第二行int n改為extern int n,這就是告訴程式我在此處宣告瞭變數n,但是真正的定義過程在別的檔案中,此處就是exam2.cpp。但事實上exam2.cpp中僅僅宣告瞭 static int n。我們看看執行結果。vc中會透過編譯,但是在進行連線時候會給出一個“變數n找不到”的錯誤。這說明 exam1.cpp無法共享exam2.cpp中的static int n變數。[/code:1:17500aa702]

[color=darkred:17500aa702]改造二:[/color:17500aa702]

[code:1:17500aa702]我們在exam1.cpp的第二行和第三行之間增加void staticfn();同時在第八行和第九行之間增加staticfn()的呼叫。再看執行結果。vc會產生一個找不到函式staticfn的錯誤。這說明exam1.cpp無法共享 exam2.cpp中的staticfn()。[/code:1:17500aa702]

[color=blue:17500aa702]從上面的結論可以看出下面幾點:[/color:17500aa702]

[code:1:17500aa702]1. static解決了名字的衝突問題。使得可以在原始檔中建立並使用與其它原始檔甚至全域性變數一樣的名字而不會導致衝突的產生。這一點在很大的專案中是很有用處的。

2. 宣告為靜態的函式不能被其他的原始檔所呼叫,因為它的名字只對本地檔案可見,其餘的檔案無法獲取它的名字,因此不可能進行連線。

在檔案作用域下宣告的inline函式預設情況下認為是static型別。在檔案作用域下宣告的const的常量預設情況下也是static儲存型別的。[/code:1:17500aa702]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-944572/,如需轉載,請註明出處,否則將追究法律責任。

相關文章