sizeof(std::string)在你的机器上等于多少?如果你用的是 Clang + libc++,答案是 24;如果你用的是 GCC + libstdc++,答案是 32;如果你用的是 MSVC,答案是 32(Release)或 40(Debug)——而在 2011 年之前,GCC 的答案是 8,因为那时候的std::string只存一根指针,所有的元数据都藏在堆上那块内存的前面,靠一个原子引用计数来决定谁拷贝谁释放。这三组数字背后,是 C++ 标准库在过去 30 年里经历的三代内存布局博弈——每一代都在试图回答同一个问题:一个字符串对象的 24 到 32 字节栈空间里,到底应该放什么?
这不是一道无聊的 sizeof 题。如果你做过高频交易系统的字符串池优化,或者在嵌入式设备上被std::string的隐式堆分配折磨过,你就知道这 24 个字节的内部排列直接决定了三件事:短字符串能不能避免malloc、拷贝操作是 O(1) 还是 O(n)、以及多线程环境下const引用是否真的是只读的。三代实现——COW 引用计数、SSO 栈内联、libc++ 的__long/__shortunion 双态机——分别给出了三种截然不同的答案,而每一种答案都在特定的历史条件下是"最优解",又在下一个时代变成了"技术债"。
本文将从 GCC 4.x 时代的 COW 布局开始,逐字节拆解三代实现的内存结构,追踪每一代切换的真实动机——不是教科书式的"COW 不好所以换成了 SSO",而是从 C++11 标准对operator[]<