C++靜態成員變數和靜態成員函數詳解

2020-07-16 10:04:25
類的靜態成員有兩種:靜態成員變數和靜態成員函數。靜態成員變數就是在定義時前面加了 static 關鍵字的成員變數;靜態成員函數就是在宣告時前面加了 static 關鍵字的成員函數。

下面的 CRectangle 類就有兩個靜態成員變數和一個靜態成員函數。
class CRectangle{
private:
    int w,h;
    static int nTotalArea;  //靜態成員變數
    static int nTotalNumber;  //靜態成員變數
public:
    static void PrintTotal ();  //靜態成員函數
};
普通成員變數每個物件有各自的一份,而靜態成員變數只有一份,被所有同類物件共用。

普通成員函數一定是作用在某個物件上的,而靜態成員函數並不具體作用在某個物件上。

存取普通成員時,要通過物件名.成員名等方式,指明要存取的成員變數是屬於哪個物件的,或要呼叫的成員函數作用於哪個物件;存取靜態成員時,則可以通過類名::成員名的方式存取,不需要指明被存取的成員屬於哪個物件或作用於哪個物件。因此,甚至可以在還沒有任何物件生成時就存取一個類的靜態成員。

當然,非靜態成員的存取方式(也即物件名.成員名)其實也適用於靜態成員,但效果和類名::成員名這種存取方式沒有區別。

使用 sizeof 運算子計算物件所佔用的儲存空間時,不會將靜態成員變數計算在內。對上面的 CRectangle 類來說,sizeof(CRectangle) 的值是 8。

靜態成員變數本質上是全域性變數。一個類,哪怕一個物件都不存在,其靜態成員變數也存在。靜態成員函數並不需要作用在某個具體的物件上,因此本質上是全域性函數。

設定靜態成員的目的,是為了將和某些類緊密相關的全域性變數和全域性函數寫到類裡面,形式上成為一個整體。考慮一個需要隨時知道矩形總數和總面積的圖形處理程式,當然可以用全域性變數來記錄這兩個值,但是將這兩個變數作為靜態成員封裝進類中,就更容易理解和維護。

例如下面的程式:
#include <iostream>
using namespace std;
class CRectangle
{
private:
    int w, h;
    static int totalArea;  //矩形總面積
    static int totalNumber;  //矩形總數
public:
    CRectangle(int w_, int h_);
    ~CRectangle();
    static void PrintTotal();
};
CRectangle::CRectangle(int w_, int h_)
{
    w = w_; h = h_;
    totalNumber++;  //有物件生成則增加總數
    totalArea += w * h;  //有物件生成則增加總面積
}
CRectangle::~CRectangle()
{
    totalNumber--;  //有物件消亡則減少總數
    totalArea -= w*h;  //有物件消亡則減少總而積
}
void CRectangle::PrintTotal()
{
    cout << totalNumber << "," << totalArea << endl;
}
int CRectangle::totalNumber = 0;
int CRectangle::totalArea = 0;
//必須在定義類的檔案中對靜態成員變數進行一次宣告 //或初始化,否則編譯能通過,連結不能通過
int main()
{
    CRectangle r1(3, 3), r2(2, 2);

    //cout << CRectangle::totalNumber; //錯誤,totalNumber 是私有
    CRectangle::PrintTotal();
    r1.PrintTotal();
    return 0;
}
程式的輸出是:
2, 13
2, 13

這個程式的基本思想是:CRectangle 類只提供一個建構函式,所有 CRectangle 物件生成時都需要用這個建構函式初始化,因此在這個建構函式中增加矩形的總數和總面積的數值即可;而所有 CRectangle 物件消亡時都會執行解構函式,所以在解構函式中減少矩形的總數和總面積的數值即可。

第 7 行和第 8 行的兩個成員變數用來記錄程式中所有矩形物件的總數和它們的總面積。這兩個值顯然不能由每個物件都維護一份,而應該只有一份。

雖然也可以用兩個全域性變數來存放這兩個值,但那樣就無法從形式上一眼看出這兩個全域性變數和 CRectangle 類的緊密聯絡,也就看不出這兩個全域性變數會在哪些函數中被存取。把它們寫成 CRectangle 類的靜態成員變數,這個問題就迎刃而解了。

輸出矩形總數和總面積的函數 PrintTotal 沒有寫成全域性函數,而是寫成 CRectangle 類的靜態成員函數,道理也是一樣的。

靜態成員變數必須在類定義的外面專門宣告,宣告時變數名前面加類名::,如第 29 行和第 30 行。宣告的同時可以初始化。如果沒有宣告,那麼程式編譯時雖然不會報錯,但是在連結(link)階段會報告“識別符號找不到”,不能生成.exe檔案。

第 36 行如果沒有注釋掉,編譯會出錯。因為 totalNumber 是私有成員,不能在成員函數外面存取。

第 37 行和第 38 行的輸出結果相同,說明二者是等價的。

因為靜態成員函數不具體作用於某個物件,所以靜態成員函數內部不能存取非靜態成員變數,也不能呼叫非靜態成員函數。假如上面程式中的 PrintTotal 函數如下編寫:
void CRectangle::PrintTotal()
{
    cout << w << "," << totalNumber << "," << totalArea << endl;  //錯誤
}
其中存取了非靜態成員變數 w,這是不允許的,編譯無法通過。因為如果用CRetangle::PrintTotal();這種形式呼叫 PrintTotal 函數,那就無法解釋進入 PrintTotal 函數後,w 到底是屬於哪個物件的。

思考題:為什麼在靜態成員函數內不能呼叫非靜態成員函數?

在上面的程式中,CRectangle 類的寫法表面看起來沒有什麼問題,實際上是有漏洞的。原因是,並非所有的 CRectangle 物件生成時都會用程式中的那個建構函式初始化。如果使用該類的程式稍微複雜一些,包含以 CRectangle 物件為引數的函數,或以 CRectangle 物件為返回值的函數,或出現CRectangle rl(r2);這樣的語句,那麼就有一些 CRectangle 物件是用預設複製建構函式,而不是 CRec:tangle(int w_, int h) 進行初始化的。這些物件生成時沒有增加 totalNumber 和 totalArea 的值,而消亡時卻減少了 totalNumber 和 totalArea 的 值,這顯然是有問題的。

解決辦法是為 CRectangle 類編寫如下複製建構函式:
CRectangle::CRectangle(CRectangle & r)
{
    totalNumber++;
    totalArea += r.w * r.h;
    w = r.w; h = r.h;
}