🔷 第十章 类和对象
📄 一句话概括

静态成员属于"类",不属于"某个对象"——全类只有一份,所有对象共享。 静态成员变量有个必须记住的写法:类里声明(加 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 台

读结果四句话:

  1. 第一行 Robot::report() 在"一个对象都没有"时就能调——这就是静态成员函数的意义:不需要对象。
  2. total 一路只增不减(3),alive 会跟着析构降回来(3 → 1)——两本账各管各的。
  3. C-3PO 先销毁、R2-D2 后销毁——又一次验证 10.3 的"后造的先走"。
  4. 两个类外定义都写了——少写任何一个就是 undefined reference。

30 秒实验:把 int Robot::alive = 0; 这一行注释掉,重新编译。你会看到 undefined reference to 'Robot::alive'——编译没问题,链接失败。这个错在 6.7.4 出现过(extern 讲"声明 vs 定义"),现在你知道它最常见的现场就是静态成员变量。

六、常见误区

  1. "静态成员变量在类里写一行就行了"——必须再写一行类外定义(int 类名::变量 = 初值;)。类里是声明、类外是定义——这就是 6.7.4 那条"声明可以多份、定义只能一份"的规矩在类里的样子。
  2. "静态成员函数里能访问普通成员"——不能。它没有 this,不知道操作的是哪个对象。报错里带 in static member function 就是这个。
  3. "静态成员函数可以写成 const"——不行。const 成员函数承诺的是"不修改我所属的那个对象",而静态成员函数根本不属于对象,没这个可承诺的对象(写了会报错)。
  4. "静态成员一定放 private"——推荐放 private(既然是数据,就该有门)。但完全公开也有合理场景,比如类级别的常量:static const int MAX = 100;(虽然 static const 整型可以在类里直接给初值,比 static int 省事)。
  5. "静态成员变量的初始化顺序和写法无关、一定安全"——跨文件时不保证(第四节的坑)。别让一个静态成员的初值依赖另一个文件里的全局/静态变量。
自测三问(点开对答案) 1. 静态成员变量和普通成员变量,最本质的差别是什么?——**数量**:**普通成员每个对象一份**;**静态成员全类共用一份**(它在"全局"那一层,不在对象里)。所以它**不属于任何具体对象**。 2. 静态成员变量为什么要"类里声明、类外定义"?漏了会怎样?——类里那行**只是声明、不分配内存**;**类外的 `int 类名::变量 = 初值;` 才是定义**。漏了会**编译通过但链接失败**,报 `undefined reference to '类名::变量'`。 3. 静态成员函数为什么不能访问普通成员?——因为它**没有 `this` 指针**(不属于任何对象),**不知道自己该操作哪个对象**。它只能访问静态成员;要用对象的数据,就得**把对象当参数显式传进来**。

相关笔记