那天我都准备下班了。
工位灯都关了一半。
群里突然有人 @我。
“线上有个小服务。
启动就崩。
刚发的版本。”
我第一反应是。
谁又把构造函数玩坏了。
因为这种事故有个味道。
像新买的咖啡机。
啪一声。
跳闸。
我们那会儿刚从“C with classes”走过来。
大家对构造函数的理解很朴素:就是“创建对象时顺便把成员赋个值”。
所以我们很自然地相信一件事。
初始化列表里写在前面的。
就应该先发生。
听起来很合理。
也很危险。
事故现场:看起来没毛病
代码长这样。 你看完大概率会点头。
#include <iostream>
struct Session {
int id;
int port;
Session(int p)
: port(p)
, id(port) // 我想让 id = port
{}
};
int main() {
Session s(8080);
std::cout << "id=" << s.id << ", port=" << s.port << "\n";
}
你看起来是在先给 port 赋值,再让 id 读 port。
你甚至能想象它会老老实实打印出 id=8080。
但它可能打印出 id 是一串莫名其妙的数字。更复杂一点,它会直接把你送进崩溃。
关键规则:初始化顺序不看初始化列表
C++ 规定得很死。 成员的初始化顺序只看它们在类里“声明”的顺序,不看初始化列表怎么排。
所以上面这个 Session 永远先初始化 id,再初始化 port。
id(port) 读到的就是“还没初始化的 port”。
这就是未初始化读取。 在 C 的世界里它叫“野值”,在 C++ 的世界里它更像“埋雷”。
初始化顺序 = 声明顺序。
为什么要这么设计?不是编译器故意刁难
你可以把一个对象想成一段固定布局的内存。 成员在内存里怎么排,是声明顺序决定的。
构造时按这个顺序“从前到后点亮”。 析构时再按相反顺序“从后到前熄灭”。
这套顺序必须稳定。 不然同一个类不同构造函数就会出现不同的资源收尾顺序。
你今天在一个构造函数里先关 B 再关 A,明天换个构造函数又反过来。
资源管理会变成赌博。
RAII 也就没法“靠语言替你兜底”了。
再来一个更像线上事故的版本
野值只是难看。更真实的事故是“引用/指针指向了还没出生的成员”。 而且它特别爱挑你发布的时候出现。
#include <iostream>
struct Buf {
Buf() : p(new int[4]) {}
~Buf() { delete[] p; }
int* data() { return p; }
int* p;
};
struct Packet {
int* out;
Buf buf;
Packet()
: out(buf.data())
, buf()
{}
};
int main() {
Packet x;
std::cout << static_cast<void*>(x.out) << "\n";
}
你以为 out(buf.data()) 写在前面就能先把 buf 准备好。
其实初始化顺序是先 out,再 buf。
buf.data() 这句会读 buf 里的指针 p。
但此时 buf 还没构造,p 还没被 new 赋值。
你得到的是一个看起来像地址、但其实没任何保证的东西。 它可能在你机器上一直没事,一上线就啪一下。
怎么写才不踩坑
做法很朴素:把依赖关系写进“声明顺序”。需要被别人用来初始化的成员,就放在前面。
上面的 Session 你有两个选择。第一个是调整声明顺序,让 port 先出生。
然后初始化列表也按声明顺序写。
struct Session {
int port;
int id;
Session(int p)
: port(p)
, id(port)
{}
};
第二个选择是别在初始化阶段引用另一个成员。 先把所有成员老老实实初始化完。 再在构造函数体里补一刀。
struct Session {
int id;
int port;
Session(int p)
: id(0)
, port(p)
{
id = port;
}
};
这两种哪个更好,要看你的“不变式”是什么。 不变式就是你希望对象从“构造完成”开始一直成立的那条规则。
如果 id 必须永远等于 port,那就让声明顺序表达这个依赖。
如果 id 只是“构造时算一下”,那构造函数体里做也没丢脸。
初始化列表的顺序,给人看。真正的初始化顺序,给标准看。
一个小技巧:让编译器替你提醒
很多编译器会对“初始化列表顺序和声明顺序不一致”给警告。你别嫌它烦,那是编译器在救你。
更好的习惯是:初始化列表里也按声明顺序写。不是为了改变顺序,是为了让代码读起来不撒谎。
读到这儿,你已经比当年的我强了。我那次事故最后就是靠着一条警告定位的。
警告长得很朴素。意思也很直白。
“你写的顺序。
不会生效。”