静态成员属于"类",不属于"某个对象"——全类只有一份,所有对象共享。 静态成员变量有个必须记住的写法:类里声明(加 static)、类外定义并初始化一次(漏了会报 undefined reference)。静态成员函数也有个必须记住的限制:它没有 this 指针(10.4.4),所以不能访问普通成员变量、也不能调用普通成员函数。它最经典的用途是"计数"——统计一共造了多少个对象。
这是 6.7.2「static 变量」在类里的版本:函数里的 static 是"初始化只一次、活到程序结束";类里的 static 是"全类共享一份"。两者共有的本质是:它不在"每个对象/每次调用"的栈里,而是在"全局"那一层。 另外,本节会首次提到 this 指针("静态成员函数没有 this"),10.4.4 紧接着把它讲透。
一、静态成员变量:全类共用一块
class Student {
private:
char name[20]; // 普通成员:每人都有一份
static int count; // 静态成员:全类只有一份(这里只是"声明")
public:
Student() { count++; } // 每造一个对象就加一
~Student() { count--; }
static int getCount() { return count; } // 静态成员函数
};
int Student::count = 0; // ★ 必须在类外"定义"一次(真正的内存在这里)
两个必须记住的点:
| 点 | 说明 |
|---|---|
| 类里那行是"声明" | static int count; 不分配内存,只是告诉编译器"有这么个成员" |
| 类外那行是"定义" | int Student::count = 0; 才真正分配内存并初始化。必须写,只能写一次 |
忘了写类外那句会怎样:编译能过,链接时失败:
undefined reference to `Student::count'
看到 undefined reference,按 6.7.4 的经验:这是"链接错误"——说明某个东西声明了但没定义。静态成员变量的类外定义,就是最常见的源头之一。
访问方式(两种都行):
cout << Student::getCount() << endl; // 推荐:用类名(说明它不属于某个对象)
cout << stu1.getCount() << endl; // 也能编译过,但看不出来"它是静态的"
注意:静态成员变量通常要放
private(它也是数据,一样要有门)。想从外面看,就提供静态成员函数。
二、静态成员函数:没有 this,所以"手伸不进对象"
规矩很硬:
| 能做 | 不能做 |
|---|---|
| 访问静态成员变量、调用静态成员函数 | 访问普通成员变量 |
被 类名:: 直接调用(不需要对象) |
调用普通成员函数 |
| 作普通的工具函数 | 写成 const 成员函数(没有 this 就没法"承诺不改") |
为什么:静态成员函数不属于任何对象——它没有"当前对象"(没有 this,10.4.4)。既然不知道在操作哪个对象,当然碰不到对象的数据。
报错长这样(在静态成员函数里写普通成员名):
error: invalid use of member 'Student::name' in static member function
看到 in static member function,就是"你想在没有对象的地方碰对象的数据"。
那它有什么用?
| 用途 | 说明 |
|---|---|
| 访问静态成员变量 | 最常见的用途(如 getCount()) |
| 写"类层面"的工具函数 | 比如 Point::distance(a, b)——两个对象都当参数传进来,不需要"当前对象" |
| 配合单例模式 | 提供一个"拿到唯一实例"的接口(后面章节会看到) |
注意第三种用途里的写法:把两个对象都作为参数传进去——因为静态成员函数没有"自己人"可用,一切都要显式传参:
class Point {
public:
static double distance(const Point &a, const Point &b); // 两个都传进来
};
三、典型用途:统计对象个数
这是静态成员变量最经典的应用,值得完整记住这套模式:
class Student {
private:
static int created; // 累计造了多少个
static int alive; // 当前活着几个
public:
Student() { created++; alive++; }
~Student() { alive--; }
static int getCreated() { return created; }
static int getAlive() { return alive; }
};
int Student::created = 0;
int Student::alive = 0;
跑起来:
{
Student a, b;
cout << Student::getAlive() << endl; // 2
}
cout << Student::getAlive() << endl; // 0 ← 两个都出作用域了(10.3)
cout << Student::getCreated() << endl; // 2 ← "造过"这个数字不会退回去
这就是"类级别的账本"——每个对象都是一笔流水,而账本只有一本(不像每个对象各有一份普通成员)。
四、静态成员的使用顺序陷阱
静态成员变量的初始化,发生在"程序开始、main 之前"——而且同一个编译单元内,按定义顺序初始化。
坑在哪:
// 文件 A.cpp
int getDefault();
int base = getDefault(); // 用一个函数返回值初始化
// 文件 B.cpp
class Config {
public:
static int value;
};
int Config::value = base; // 依赖另一个文件的变量
问题:base 和 Config::value 谁先初始化? 标准不保证跨文件的顺序。如果 Config::value 先跑,它就拿到了 base 的未初始化值(垃圾)。
实用建议:静态成员变量的初始化,不要依赖"另一个文件的全局/静态变量"。 想避免麻烦,可以用"函数内部的静态局部变量"(6.7.2 的写法)——它保证"第一次用到时才初始化",顺序就确定了。这个问题("静态初始化顺序问题")是 C++ 的老毛病,知道它存在即可。
五、动手 Demo:一个会记账的类
#include <iostream>
#include <cstring>
using namespace std;
class Robot {
private:
char name[20];
static int total; // 一共造了多少
static int alive; // 现在活着几个
public:
Robot(const char *n) {
strcpy(name, n);
total++;
alive++;
cout << "造出 " << name << "(现存 " << alive << " 台)" << endl;
}
~Robot() {
alive--;
cout << name << " 被销毁(现存 " << alive << " 台)" << endl;
}
static void report() { // 静态成员函数:只汇报"类层面"的账
cout << "累计生产 " << total << " 台,当前在用 " << alive << " 台" << endl;
}
};
int Robot::total = 0; // ★ 类外定义 ×2,一个都不能少
int Robot::alive = 0;
void makeSome() {
Robot r1("R2-D2");
Robot r2("C-3PO");
Robot::report(); // 函数里也能用类名调
} // ← 出作用域,r1、r2 依次析构(10.3)
int main() {
Robot::report(); // 还没有对象,依然能调(因为是静态的)
Robot r0("Wall-E");
Robot::report();
makeSome();
Robot::report(); // 累计 3,在用 1
return 0;
}
看到什么算成功:
累计生产 0 台,当前在用 0 台
造出 Wall-E(现存 1 台)
累计生产 1 台,当前在用 1 台
造出 R2-D2(现存 2 台)
造出 C-3PO(现存 3 台)
累计生产 3 台,当前在用 3 台
C-3PO 被销毁(现存 2 台)
R2-D2 被销毁(现存 1 台)
累计生产 3 台,当前在用 1 台
读结果四句话:
- 第一行
Robot::report()在"一个对象都没有"时就能调——这就是静态成员函数的意义:不需要对象。 total一路只增不减(3),alive会跟着析构降回来(3 → 1)——两本账各管各的。C-3PO先销毁、R2-D2后销毁——又一次验证 10.3 的"后造的先走"。- 两个类外定义都写了——少写任何一个就是
undefined reference。
30 秒实验:把 int Robot::alive = 0; 这一行注释掉,重新编译。你会看到 undefined reference to 'Robot::alive'——编译没问题,链接失败。这个错在 6.7.4 出现过(extern 讲"声明 vs 定义"),现在你知道它最常见的现场就是静态成员变量。
六、常见误区
- "静态成员变量在类里写一行就行了"——必须再写一行类外定义(
int 类名::变量 = 初值;)。类里是声明、类外是定义——这就是 6.7.4 那条"声明可以多份、定义只能一份"的规矩在类里的样子。 - "静态成员函数里能访问普通成员"——不能。它没有
this,不知道操作的是哪个对象。报错里带in static member function就是这个。 - "静态成员函数可以写成
const"——不行。const成员函数承诺的是"不修改我所属的那个对象",而静态成员函数根本不属于对象,没这个可承诺的对象(写了会报错)。 - "静态成员一定放
private"——推荐放private(既然是数据,就该有门)。但完全公开也有合理场景,比如类级别的常量:static const int MAX = 100;(虽然static const整型可以在类里直接给初值,比static int省事)。 - "静态成员变量的初始化顺序和写法无关、一定安全"——跨文件时不保证(第四节的坑)。别让一个静态成员的初值依赖另一个文件里的全局/静态变量。
自测三问(点开对答案)
1. 静态成员变量和普通成员变量,最本质的差别是什么?——**数量**:**普通成员每个对象一份**;**静态成员全类共用一份**(它在"全局"那一层,不在对象里)。所以它**不属于任何具体对象**。 2. 静态成员变量为什么要"类里声明、类外定义"?漏了会怎样?——类里那行**只是声明、不分配内存**;**类外的 `int 类名::变量 = 初值;` 才是定义**。漏了会**编译通过但链接失败**,报 `undefined reference to '类名::变量'`。 3. 静态成员函数为什么不能访问普通成员?——因为它**没有 `this` 指针**(不属于任何对象),**不知道自己该操作哪个对象**。它只能访问静态成员;要用对象的数据,就得**把对象当参数显式传进来**。- ☐ 10.4.4 隐藏的this指针(下一节)