從C學C++(7)——static
成員
若無特殊說明,本博客所執行的C++標準均為C++11.
static
成員和成員函數
對于特定類型的全體對象而言,有時候可能需要訪問一個全局的變量。比如說統計某種類型對象已創建的數量。
通常在C中使用全局變量來實現,如果我們用全局變量會破壞數據的封裝,一般的用戶代碼都可以修改這個全局變量,這時我們可以用類的靜態成員來解決這個問題。
非static
數據成員存在于類類型的每個對象中static
數據成員獨立該類的任意對象存在,它是與類關聯的對象,不與類對象關聯。
個人理解:把它等同于python中類成員就可以了,不過C++中使用static
關鍵字其實說明它和一般靜態變量一樣,存在于.bss段或者.data段,生命周期是整個程序(不隨著對象實例創建或者銷毀),所以其是獨立于類對象實例存在的,但只是C++編譯器在語法上把它歸類于某個類(本質在運行時和其他靜態變量無異),需要我們使用類域或者對象實例去訪問(這個功能就和python中的類成員基本一致)。
類的static
成員
特點:
static
成員的名字是在類的作用域中,因此可以避免與其它類成員或全局對象名字沖突。- 可以實施封裝,
static
成員可以是私有的,而全局對象不可以。 - 閱讀程序容易看出
static
成員與某個類相關聯,這種可見性可以清晰地反映程序員的意圖。
定義和注意事項
-
static
成員需要在類定義體外進行初始化與定義。這個還好理解,因為類定義其實和C中結構類型聲明是類似的,它只是聲明了一種類似,因此,它的
static
成員也只是聲明里面有一個static
而已,真正這個獨立于類對象實例的static
成員總得找個地方定義它(畢竟它和一般的成員不一樣,一般的成員跟隨著對象的定義而創建(分配內存空間))。 -
特殊的整型
static const
成員:整型static const
成員可以在類定義體中初始化和定義。(個人感覺更像是個語法糖,平時還是不用為好,而且只有整形可以,浮點和其它類型不可以,所以還是不用為好)。
枚舉常量類型
在類中定義的枚舉常量類型類似于static const
成員,是常量,而且被所有對象共享,屬于類。
類的static成員函數
-
static
成員函數沒有隱含的this指針。因為是類成員函數,不是任何一個特定對象的成員函數,所以自然沒有隱含的this指針,也就意味著,在靜態成員函數中,沒有辦法直接訪問其他非靜態成員(函數)。
-
靜態成員函數不可以訪問非靜態成員。
-
非靜態成員函數可以訪問靜態成員。
具體對象實例可以訪問類變量,也可以訪問類函數一個道理。因為
static
聲明的成員(函數)是所有類共享的。
類/對象實例的大小計算
- 類大小計算遵循前面學過的結構體對齊原則。
- 類的大小與數據成員有關與成員函數無關。
- 類的大小與靜態數據成員(函數)無關。
- 虛函數對類的大小的影響:會使類對象多4個字節的大小,用于存放虛表指針。
- 虛繼承對類的大小也會有影響。
static用法總結
函數內部修飾變量使其生存期為整個程序(C也一樣)
用于函數內部修飾變量,即函數內的靜態變量這種變量的生存期長于該函數,使得函數具有一定的"狀態”。使用靜態變量的函數一般是不可重入的,也不是線程安全的,比如strtok(3)
。
函數外部修飾變量使其限制于該文件(C也一樣)
用在文件級別(函數體之外),修飾變量或函數,表示該變量或函數只在本文件可見,其他文件看不到也訪問不到該變量或函數。專業的說法叫“具有internal linkage”(簡言之:不暴露給別的translation unit,C/C++中最小編譯單元為文件)
修飾類的數據成員使其成為類成員(C沒有)
用于修飾類的數據成員,即所謂"靜態成員”。這種數據成員的生存期大于class的對象(實例/instance)。靜態數據成員是每個class有一份,普通數據成員是每個instance 有一份。
修飾類的成員函數使其成為類方法(C沒有)
用于修飾class的成員函數,即所謂“靜態成員函數”。這種成員函數只能訪問靜態成員和其他靜態成員函數,不能訪問非靜態成員和非靜態成員函數。
四種對象的作用域和生存期
棧對象
- 隱含調用構造/析構函數(程序中沒有顯示調用)
- 作用域為所屬的
{}
的塊作用域內。 - 生存期跟隨所屬函數的執行(分配空間)和退出(釋放空間)。
堆對象
- 隱含調用構造/析構函數(程序中沒有顯示調用)。
- 作用域為所屬的
{}
的塊作用域內。 - 生存期取決于用戶何時
delete
。
全局對象、靜態全局對象
- 全局對象的構造先于main函數(這個對于支持嵌入式的C++編譯器,會在進入main函數前的啟動匯編代碼中插入執行如
__libc_init_array()
等的全局對象初始化函數鏈表執行)。 - 已初始化的全局變量或靜態全局對象存儲于.data段中。
- 未初始化的全局變量或靜態全局對象存儲于.bss(Block Started by Symbol)段中。
- 全局對象和靜態對象的生存期都是整個程序執行期間。
靜態局部對象
- 已初始化的靜態局部變量存儲于.data段中。
- 未初始化的靜態局部變量存儲于.bss段中。
- 對于靜態局部變量,如果是內置類型(整形、浮點等),對象在編譯時就初始化好在.data段中了,但對于靜態局部的(我們自己定義的)類對象實例,其初始化是在代碼運行時完成的。(這個好理解,雖然其存在于.data段而非棧上,但由于類對象實例初始化時需要執行構造函數,而編譯器是沒有辦法執行函數的,只有運行到這段代碼的時候才能執行構造函數,所以其初始化必須推遲到運行時刻)。
static
和單例模式
單例模式
設計模式的一種:保證一個類只有一個實例,并提供一個全局訪問點。(目前暫時沒有想到其的用處,按照之前的經驗,感覺可能在對硬件封裝時可能會有用,畢竟底層真實的硬件可能只有一個)。
在C++中設計單例模式需要注意:禁止構造函數、拷貝函數和=運算符,并提供共一個全局的訪問點。
使用static
局部對象和引用返回實現單例模式
class Singleton{
public:static Singleton& getInstance() {static Singleton instance; // 局部靜態變量,保證只創建一次return instance;}~Singleton() {std::cout << "Singleton destroyed." << std::endl;}
private:Singleton(const Singleton& other); // 禁止拷貝構造Singleton& operator=(const Singleton& other); // 禁止賦值操作Singleton() {std::cout << "Singleton created." << std::endl;}
};
static
局部對象(利用其生存期為整個程序),且static
局部對象只會初始化一次的特性。通過返回引用,保證每次getInstance()
函數返回的都是同一個靜態局部對象。需要注意,這里在調用函數的時候,必須使用引用來接收返回值(如果使用對象的話,這里會發生從引用到對象的賦值,而我們已經將拷貝構造函數聲明為private
,因此會報錯,達到單例模式的目的)。
同時,因為返回的是引用,所以不存在多個對象釋放的時候出現空指針的問題,在程序結束的時候,只有靜態局部變量被釋放一次,其他都是引用,不存在釋放多次的問題。
Singleton& s1 = Singleton::getInstance(); // 獲取單例實例
Singleton& s2 = Singleton::getInstance(); // 再次獲取同一實例
const
成員函數/對象和mutable
const
成員函數
-
const
成員函數不會修改對象的狀態。 -
const
成員函數只能訪問數據成員的值,不能修改它的值。 -
需要注意
const
成員函數是在函數聲明和函數體之間加上const
關鍵字class Test{ public:int get_x() const{ //這才是const成員函數return x; //且const成員函數內部不能修改數據成員的值}const int get_x1(){return x; // 這個函數只是一個返回const int 類型的成員函數而已,不是const成員函數} private: int x }
const
對象的使用
-
如果把一個對象指定為
const
,就是告訴編譯器不要修改它。 -
const
對象的定義:const 類名 對象名(參數表);
-
const
對象定義的時候默認會把不會修改內部變量的成員函數定義為const
成員函數,而那些修改到內部變量的成員函數則是非const
的,因此,如果const
對象調用那些非const
成員函數則會報錯。這個好理解,因為成員函數默認是會傳入一個this指針,那些要修改內部對象的成員函數傳入的指針是非
const
的,而const
對象傳入自身指針是const
類型,對C++來說,沒有顯式使用const_cast<>()
去除const
屬性,是不可能完成const
到非const
的類型轉換的。
mutable
關鍵字
如果我們需要定義個const
對象,但這個對象中可能只需要一兩個成員是需要被外界配置修改的,這個時候可以使用 mutable
關鍵字,因為,mutable
修飾的數據成員即使在const
對象或在const
成員函數中都可以被修改。