C++静态成员变量和静态成员函数详解

类的静态成员有两种:静态成员变量和静态成员函数。静态成员变量就是在定义时前面加了 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;
}