我第一次被“静态成员”坑到。
不是崩溃。
是链接错误。
那种很有年代感的红字。
undefined reference to Foo::kCount
当年我还以为自己把 C++ 写坏了。
后来才明白。
我只是把“变量住哪”这件事想得太天真。
变量到底住在哪
在 C 的年代。
我们写变量。
心里默认它们有三种住法。
一种跟着函数走。
进栈。
出栈。
一种在堆上漂着。
你 malloc 了它才出现。
你 free 了它才消失。
还有一种。
从程序一出生就在那里。
到程序咽气才走。
老的编译器和链接器会把它们分到两个大房间。
.data 放“有初值的”。
.bss 放“没初值的”。
你不需要记这些段名。
你只要记住这个味道。
有些东西。
不是跟着对象走。
它是跟着“程序”走。
static 这个词。
最早就是在提醒你这个。
它不属于某一次调用。 它属于整个进程的一生。
class 里的 static:它不是对象的一部分
到了 C++。
我们开始把数据塞进 class。
新同学很容易产生一个错觉。
“成员变量嘛。
当然住在对象里。”
普通成员确实是这样。
但 static 成员不是。
你写一段最朴素的代码。
struct Foo {
static int kCount;
int x;
};
先别急着讨论 kCount 初始值。
先问一个更粗的问题。
sizeof(Foo) 是多少?
#include <cstddef>
static_assert(sizeof(Foo) == sizeof(int));
你看到这里。
脑子里应该有个画面。
kCount 没有挤进对象。
它不占每个 Foo 的那份内存。
它更像“全局变量穿了一件类的外套”。
它的生命周期。
跟进程走。
不是跟对象走。
你创建一万个 Foo。
也只有一个 Foo::kCount。
那条链接错误从哪来
回到我当年的事故现场。
我写了上面的 struct。
然后我在头文件里顺手用了它。
// foo.h
struct Foo {
static int kCount;
};
inline void touch() {
++Foo::kCount;
}
编译没问题。
一切都像是正常的。
直到链接阶段。
链接器问我。
“Foo::kCount 的那块内存。
你打算放哪。
多大。
名字到底是什么?”
我当时答不上来。
因为我只“声明”了它。
没“定义”它。
这就是 C++ 的老规矩。
类里那句。
多数时候只是声明。
你还得在某个 .cpp 里。
给它一个真正的落户。
// foo.cpp
int Foo::kCount = 0;
这句出现之后。
链接器就满意了。
它终于能把这块存储。
塞进 .data 或 .bss。
并且保证全程序只有一份。
这背后有个老词。
ODR。
One Definition Rule。
听起来像教条。
其实像物业规定。
同一套房。
只能有一个门牌号。
“只在头文件里写”的年代
后来大家写得越来越“现代”。
头文件里就想把东西都写完。
inline。
模板。
header-only。
爽是爽。
但 static 成员这事会突然变得尴尬。
你把定义也写进头文件。
很容易就会从“找不到定义”。
变成“定义太多”。
典型的报错是另一种红字。
multiple definition of Foo::kCount
那种感觉。
像你把同一个人。
在不同的城市。
办了两张身份证。
链接器又开始骂人。
C++17 的 inline static:一次很实用的妥协
C++17 给了一个很像救火栓的东西。
inline 变量。
它让“定义写在头文件里”变成合法。
你可以这么写。
struct Foo {
inline static int kCount = 0;
};
这一句同时完成了两件事。
它给了初始值。
也给了定义。
而且允许出现在多个编译单元里。
链接器会把它们当成“同一个东西”。
这就是 inline 的真正含义。
不是“更快”。
是“允许重复出现。
但语义上只有一个”。
那天我第一次把一个老项目升级到 C++17。
我删掉了几十个 foo.cpp 里只剩一行的定义。
当场觉得自己省了一辈子的碎片时间。
static 成员函数:它为什么能没有 this
顺手再说一句。
静态成员函数也是同一个味道。
它不属于某个对象。
所以它没有 this。
struct Foo {
inline static int kCount = 0;
static void inc() {
++kCount;
}
};
你可以用 Foo::inc() 调。
也可以用对象 f.inc() 调。
但第二种只是语法糖。
它并不会把 f 传进去。
所以静态成员函数里。
也不能直接摸普通成员。
struct Foo {
int x = 0;
static void bad() {
// ++x; // 不行。这里没有 this。
}
};
这不是编译器刁难你。
是对象模型在提醒你。
“类级别”的东西。
就别假装它是“对象级别”。
你需要带走的那张心智图
把话说得更直白一点。
普通成员。
住在对象里。
每个对象一份。
静态成员。
住在静态存储区。
全程序一份。
你把它写进 class。
只是为了把名字收进作用域。
让它看起来更像“某个类型的组成部分”。
这不是装饰。
这是一种约束。
它让你在评审里看到 Foo::kCount 时。
马上就能问出那句老问题。
“这东西是全局状态。
你确定我们要它吗?”
高手点不点赞。
往往就看你有没有先问这一句。