C/C++ 中的static關鍵字
小目錄:
- 靜態成員變數(面向對象)
- 靜態成員函數(面向對象)
- 靜態全局變數(面向過程)
- 靜態局部變數(面向過程)
- 靜態函數(面向過程)
1. (面向對象的)靜態成員變數
在類內成員變數的聲明前加上關鍵字static,該數據成員就是類內的靜態數據成員。
//Example 5#include <iostream.h>class Myclass{public: Myclass(int a,int b,int c); void GetSum();private: int a,b,c; static int Sum;//聲明靜態數據成員};int Myclass::Sum=0; //定義並初始化靜態數據成員Myclass::Myclass(int a,int b,int c){ this->a=a; this->b=b; this->c=c; Sum+=a+b+c;}void Myclass::GetSum(){ cout<<"Sum="<<Sum<<endl;}void main(){ Myclass M(1,2,3); M.GetSum(); Myclass N(4,5,6); N.GetSum(); M.GetSum();}
靜態成員變數有以下特點:
- 靜態成員變數是該類的所有對象所共有的。對於普通成員變數,每個類對象都有自己的一份拷貝。而靜態成員變數一共就一份,無論這個類的對象被定義了多少個,靜態成員變數只分配一次內存,由該類的所有對象共享訪問。所以,靜態數據成員的值對每個對象都是一樣的,它的值可以更新;
- 因為靜態數據成員在全局數據區分配內存,由本類的所有對象共享,所以,它不屬於特定的類對象,不佔用對象的內存,而是在所有對象之外開闢內存,在沒有產生類對象時其作用域就可見。因此,在沒有類的實例存在時,靜態成員變數就已經存在,我們就可以操作它;
- 靜態成員變數存儲在全局數據區。static 成員變數的內存空間既不是在聲明類時分配,也不是在創建對象時分配,而是在初始化時分配。靜態成員變數必須初始化,而且只能在類體外進行。否則,編譯能通過,鏈接不能通過。在Example 5中,語句int Myclass::Sum=0;是定義並初始化靜態成員變數。初始化時可以賦初值,也可以不賦值。如果不賦值,那麼會被默認初始化,一般是 0。靜態數據區的變數都有默認的初始值,而動態數據區(堆區、棧區)的變數默認是垃圾值。
- static 成員變數和普通 static 變數一樣,編譯時在靜態數據區分配內存,到程序結束時才釋放。這就意味著,static 成員變數不隨對象的創建而分配內存,也不隨對象的銷毀而釋放內存。而普通成員變數在對象創建時分配內存,在對象銷毀時釋放內存。
- 靜態數據成員初始化與一般數據成員初始化不同。初始化時可以不加 static,但必須要有數據類型。被 private、protected、public 修飾的 static 成員變數都可以用這種方式初始化。靜態數據成員初始化的格式為:<數據類型><類名>::<靜態數據成員名>=<值>
- 類的靜態成員變數訪問形式1:<類對象名>.<靜態數據成員名>
- 類的靜態成員變數訪問形式2:<類類型名>::<靜態數據成員名>,也即,靜態成員不需要通過對象就能訪問。
- 靜態數據成員和普通數據成員一樣遵從public,protected,private訪問規則;
- 如果靜態數據成員的訪問許可權允許的話(即public的成員),可在程序中,按上述格式來引用靜態數據成員 ;
- sizeof 運算符不會計算 靜態成員變數。
class CMyclass{ int n; static int s;}; //則sizeof(CMyclass)等於4
何時採用靜態數據成員?
設置靜態成員(變數和函數)這種機制的目的是將某些和類緊密相關的全局變數和函數寫到類裡面,看上去像一個整體,易於理解和維護。如果想在同類的多個對象之間實現數據共享,又不要用全局變數,那麼就可以使用靜態成員變數。也即,靜態數據成員主要用在各個對象都有相同的某項屬性的時候。比如對於一個存款類,每個實例的利息都是相同的。所以,應該把利息設為存款類的靜態數據成員。這有兩個好處:
- 不管定義多少個存款類對象,利息數據成員都共享分配在全局數據區的內存,節省存儲空間。
- 一旦利息需要改變時,只要改變一次,則所有存款類對象的利息全改變過來了。
你也許會問,用全局變數不是也可以達到這個效果嗎?
同全局變數相比,使用靜態數據成員有兩個優勢:
- 靜態成員變數沒有進入程序的全局命名空間,因此不存在與程序中其它全局命名衝突的可能。
- 可以實現信息隱藏。靜態成員變數可以是private成員,而全局變數不能。
2.(面向對象的) 靜態成員函數
與靜態成員變數類似,我們也可以聲明一個靜態成員函數。
靜態成員函數為類服務而不是為某一個類的具體對象服務。靜態成員函數與靜態成員變數一樣,都是類的內部實現,屬於類定義的一部分。普通成員函數必須具體作用於某個對象,而靜態成員函數並不具體作用於某個對象。
普通的成員函數一般都隱含了一個this指針,this指針指向類的對象本身,因為普通成員函數總是具體地屬於類的某個具體對象的。當函數被調用時,系統會把當前對象的起始地址賦給 this 指針。通常情況下,this是預設的。如函數fn()實際上是this->fn()。
與普通函數相比,靜態成員函數屬於類本身,而不作用於對象,因此它不具有this指針。正因為它沒有指向某一個對象,所以它無法訪問屬於類對象的非靜態成員變數和非靜態成員函數,它只能調用其餘的靜態成員函數和靜態成員變數。從另一個角度來看,由於靜態成員函數和靜態成員變數在類實例化之前就已經存在可以訪問,而此時非靜態成員還是不存在的,因此靜態成員不能訪問非靜態成員。
//Example 6#include <iostream>using namespace std;class Student{private: char *name; int age; float score; static int num; //學生人數 static float total; //總分public: Student(char *, int, float); void say(); static float getAverage(); //靜態成員函數,用來獲得平均成績};int Student::num = 0;float Student::total = 0;Student::Student(char *name, int age, float score){ this->name = name; this->age = age; this->score = score; num++; total += score;}void Student::say(){ cout<<name<<"的年齡是 "<<age<<",成績是 "<<score<<"(當前共"<<num<<"名學生)"<<endl;}float Student::getAverage(){ return total / num;}int main(){ (new Student("小明", 15, 90))->say(); (new Student("李磊", 16, 80))->say(); (new Student("張華", 16, 99))->say(); (new Student("王康", 14, 60))->say(); cout<<"平均成績為 "<<Student::getAverage()<<endl; return 0;}運行結果:小明的年齡是 15,成績是 90(當前共1名學生)李磊的年齡是 16,成績是 80(當前共2名學生)張華的年齡是 16,成績是 99(當前共3名學生)王康的年齡是 14,成績是 60(當前共4名學生)平均成績為 82.25
靜態成員函數的特點:
- 出現在類體外的函數定義不能指定關鍵字static;
- 靜態成員之間可以相互訪問,即靜態成員函數(僅)可以訪問靜態成員變數、靜態成員函數;
- 靜態成員函數不能訪問非靜態成員函數和非靜態成員變數;
- 非靜態成員函數可以任意地訪問靜態成員函數和靜態數據成員;
- 由於沒有this指針的額外開銷,靜態成員函數與類的全局函數相比速度上會稍快;
- 調用靜態成員函數,兩種方式:
- 通過成員訪問操作符(.)和(->),也即通過類對象或指向類對象的指針調用靜態成員函數。
- 直接通過類來調用靜態成員函數。<類名>::<靜態成員函數名>(<參數表>)。也即,靜態成員不需要通過對象就能訪問。
拷貝構造函數的問題
在使用包含靜態成員的類時,有時候會調用拷貝構造函數生成臨時的隱藏的類對象,而這個臨時對象在消亡時會調用析構函數有可能會對靜態變數做操作(例如total_num--),可是這些對象在生成時卻沒有執行構造函數中的total_num++的操作。解決方案是為這個類寫一個拷貝構造函數,在該拷貝構造函數中完成total_num++的操作。
3. (面向過程的)靜態全局變數
在全局變數前,加上關鍵字static,該變數就被定義成為一個靜態全局變數。
//Example 1#include <iostream.h> void fn();static int n; //定義靜態全局變數 void main(){ n=20; cout<<n<<endl; fn();} void fn(){ n++; cout<<n<<endl;}
靜態全局變數有以下特點:
- 該變數在全局數據區分配內存;
- 未經初始化的靜態全局變數會被程序自動初始化為0(自動變數的自動初始化值是隨機的);
- 靜態全局變數在聲明它的整個文件都是可見的,而在文件之外是不可見的;
- 靜態變數都在全局數據區分配內存,包括後面將要提到的靜態局部變數。對於一個完整的程序,在內存中的分布情況如下:【代碼區】【全局數據區】【堆區】【棧區】,一般程序的由new產生的動態數據存放在堆區,函數內部的自動變數存放在棧區,靜態數據(即使是函數內部的靜態局部變數)存放在全局數據區。自動變數一般會隨著函數的退出而釋放空間,而全局數據區的數據並不會因為函數的退出而釋放空間。
Example 1中的代碼中將
static int n; //定義靜態全局變數
改為
int n; //定義全局變數
程序照樣正常運行。
定義全局變數就可以實現變數在文件中的共享,但定義靜態全局變數還有以下好處:- 靜態全局變數不能被其它文件所用;
- 其它文件中可以定義相同名字的變數,不會發生衝突;
將上述示例代碼改為如下:
//Example 2//File1#include <iostream.h> void fn();static int n; //定義靜態全局變數 void main(){ n=20; cout<<n<<endl; fn();} //File2 #include <iostream.h> extern int n;void fn(){ n++; cout<<n<<endl;}
編譯並運行Example 2,會發現上述代碼可以分別通過編譯,但運行時出現錯誤。 這就是因為靜態全局變數不能被其它文件所用,即使在其它文件中使用extern 進行聲明也不行。
我們將
static int n; //定義靜態全局變數
改為
int n; //定義全局變數
再次編譯運行程序,程序可正常運行。
因此,在一個文件中,靜態全局變數和全局變數功能相同;而在兩個文件中,要使用同一個變數,則只能使用全局變數而不能使用靜態全局變數。
4. (面向過程的)靜態局部變數
在局部變數前,加上關鍵字static,該變數就被定義成為一個靜態局部變數。
//Example 3#include <iostream.h>void fn();void main(){ fn(); //10 fn(); //11 fn(); //12}void fn(){ static n=10; cout<<n<<endl; n++;}
通常,在函數體內定義了一個變數,每當程序運行到該語句時都會給該局部變數分配棧內存。但隨著程序退出函數體,系統就會收回棧內存,局部變數也相應失效。
但有時候我們需要在兩次調用之間對變數的值進行保存。通常的想法是定義一個全局變數來實現。但這樣一來,變數已經不再屬於函數本身了,不再僅受函數的控制,這給程序的維護帶來不便。
靜態局部變數正好可以解決這個問題。靜態局部變數保存在全局數據區,而不是保存在棧中,每次的值保持到下一次調用,直到下次賦新值。
靜態局部變數有以下特點:
- 靜態局部變數在全局數據區分配內存;
- 靜態局部變數在程序執行到該對象的聲明處時被首次初始化,即以後的函數調用不再進行初始化;
- 靜態局部變數一般在聲明處初始化,如果沒有顯式初始化,會被程序自動初始化為0;
- 靜態局部變數始終駐留在全局數據區,直到程序運行結束。但其作用域為局部作用域,當定義它的函數或語句塊結束時,其作用域隨之結束;
5. (面向過程的)靜態函數
在函數的返回類型前加上static關鍵字,函數即被定義為靜態函數。靜態函數與普通函數不同,它只能在聲明它的文件當中可見,不能被其它文件使用。
//Example 4#include <iostream.h>static void fn();//聲明靜態函數void main(){ fn();}void fn()//定義靜態函數{ int n=10; cout<<n<<endl;}
定義靜態函數的好處:(類似於靜態全局變數)
- 靜態函數不能被其它文件所用;
- 其它文件中可以定義相同名字的函數,不會發生衝突;
參考:
C++ - 靜態成員變數與靜態成員函數hehekai:C++的static關鍵字 詳解hehekai:C++ static靜態成員變數和靜態成員函數推薦閱讀:
※美劇《矽谷》第三季第一集神秘代碼寫的是什麼?
※不調用畫圖 API,用C 或 C++ 如何實現畫一條線?
※編程時IDE里的Intellisence好像是個編譯器前端一樣,什麼都知道.這是怎麼實現的?
※字元串字面值傳入函數時候是用什麼形式?
※C/C++ 里指針聲明為什麼通常不寫成 int* ptr 而通常寫成 int *ptr ?