一、痛点引入:为什么字符串值得单独写一篇?
很多 C++ 新手把 std::string 当成"高级的 char 数组",或者干脆当成 Java 的 String 来用。直到某天遇到这些诡异问题:
#include <string> #include <iostream> int main() { std::string a = "hello"; // 只有 5 个字符 std::string b = a; // 拷贝构造 b[0] = 'H'; // 修改 b std::cout << a << "\n"; // 输出 hello 还是 Hello? std::string c = "hello world, this is a very long string"; // 超过 40 字符 std::cout << sizeof(c) << "\n"; // 为什么 sizeof 还是 32? std::cout << sizeof(a) << "\n"; // 和 c 的 sizeof 一样? return 0; }两个问题的答案都藏在一个词里:SSO(Small String Optimization,小字符串优化)。
- 第 1 题:a 的 5 个字符存在对象内部,b = a 是逐字节拷贝,b 和 a 各自持有自己的副本,改 b 不影响 a,所以输出 hello。
- 第 2 题:无论字符串多长,sizeof(std::string) 都固定为 32 字节(GCC x86-64)——短字符串直接塞进这 32 字节里,长字符串则在堆上另开一块内存,对象里只存指针。
本文就把这 32 个字节逐字节拆开看,并回答三个核心问题:
- SSO 到底是怎么把字符串"塞进"对象里的?
- 容量不够时,string 怎么扩容?为什么常见实现选 2 倍?
- 历史上臭名昭著的 COW(写时复制)为什么被 C++11 抛弃?
二、先建立直觉:std::string 是"对象"不是"指针"
通俗类比:std::string 像一个"智能行李箱"。行李箱本体(对象)只有固定大小;东西少(短字符串)就直接放进行李箱夹层(SSO),东西多(长字符串)才去租一个仓库(堆内存),行李箱里只留一张仓库钥匙(指针)。
先看一个最朴素的"未优化 string"长什么样:
// 一个没有任何优化的字符串类(教学用) class NaiveString { char* data_; // 指向堆内存的指针,8 字节(x86-64) size_t size_; // 当前长度,8 字节 size_t capacity_; // 容量,8 字节 // sizeof = 24 };对于 1 个字符的字符串,也要堆分配一次、拷贝一个字符、再释放——性能灾难。高频短字符串场景(解析、日志、网络包解析)会被这种实现拖垮。
所以标准库实现者们想:能不能让短字符串直接用对象自己的内存?这就是 SSO 的起源。
三、SSO:小字符串优化(核心概念)
3.1 基本思想
在对象的 sizeof 空间里,划出一块"内置缓冲区(buffer)"。当字符串长度不超过阈值(通常是 15 或 22 字节)时,字符直接存在内置缓冲区里,不触发任何堆分配;只有超过阈值才走堆分配。
通俗类比:随身带一个 15 格的便签本,短消息写便签本上;长文件才去复印店(堆)打印。
3.2 libstdc++(GCC)的经典布局
GCC 的 libstdc++ 在 64 位平台上 sizeof(std::string) == 32,内部是 union:
// libstdc++ 简化后的布局(_GLIBCXX_USE_CXX11_ABI) union { struct { // 长字符串模式 char* _M_data; // 堆指针,8 字节 size_t _M_length; // 长度,8 字节 char _M_data[16]; // 高 16 字节的"尾巴",可以放短字符串的溢出部分 } _M_long; struct { // 短字符串模式 char _M_data[16]; // 16 字节缓冲区 char _M_local_bits; // 第 16 字节:既当容量/长度标记,又当短字符串标志 } _M_local; size_t _M_allocated_capacity; // 长模式下的容量 };关键点:union 让长/短两种模式共用同一块 32 字节内存,用最后一个字节的高位来区分当前是哪种模式:
- 短字符串:最后一个字节(_M_local_bits)的低位是长度,最高位为 0。
- 长字符串:最后一个字节的最高位为 1,其余 7 位与前面的字节共同组成容量值。
验证方法(GCC x86-64):
g++ -std=c++17 -o sso sso.cpp && ./sso// sso.cpp #include <string> #include <cstdio> int main() { std::string s = "0123456789ABCDE"; // 正好 15 字符 std::printf("sizeof(string) = %zu\n", sizeof(s)); const unsigned char* p = reinterpret_cast<const unsigned char*>(&s); std::printf("对象前16字节(hex): "); for (int i = 0; i < 16; ++i) std::printf("%02X ", p[i]); std::printf("\n"); std::printf("第16字节(hex): %02X\n", p[16]); std::printf("字符串内容: %s\n", s.c_str()); return 0; }输出:
看到 30 31 32 ... 正是 ASCII 字符 0 1 2 ...——15 个字符直接躺在对象内部,没有任何堆指针!第 16 字节是 0x0F = 15,表示长度 15(短模式下低位存长度)。
再试试 16 个字符,第 16 字节会变成 0x80 | ...(最高位置 1 表示长模式),前 8 字节变成堆指针:
std::string s = "0123456789ABCDEF"; // 16 字符,突破阈值输出:
前 8 字节 xx 是堆地址(每次运行不同),第 16 字节 0x90 的最高位是 1——进入长字符串模式。
3.3 SSO 阈值的由来
为什么是 15 字节?因为:
- sizeof(std::string) 是 32 字节(x86-64);
- 长模式需要 指针(8) + 长度(8) + 容量(8) = 24 字节;
- 短模式最多可用 32 - 1 = 31 字节,但为了 union 对齐和与长模式兼容,实际缓冲区是 16 字节,其中最后 1 字节做标记,有效载荷 15 字节;
- 15 字节能覆盖绝大多数"短字符串"场景(如单词、短 key、文件名)。
⚠️ 易错预警:不同实现的阈值不同,不要硬编码 15:
| 实现 | 平台 | sizeof(string) | SSO 阈值(有效字符) |
| libstdc++ (GCC) | x86-64 | 32 字节 | 15 |
| libc++ (Clang/LLVM) | x86-64 | 24 字节 | 22 |
| MSVC STL | x86-64 | 40 字节 | 15 |
| libstdc++ | 32 位 | 16 字节 | 7 |
libc++ 用 24 字节装下 指针 + 大小 + 联合体,缓冲区 23 字节,其中 1 字节做标志,有效载荷 22 字节。
四、三种主流实现的内存布局对比
通俗类比:同样卖"行李箱",GCC 做 32 寸、Clang 做 24 寸、MSVC 做 40 寸,夹层大小也不同——但功能一样。
4.1 libstdc++(GCC):union 双模式,阈值 15
- 布局:见上节。
- 短模式标志:_M_local_bits 最高位 0。
- 特点:经典、稳定、文档多。
4.2 libc++(Clang):压缩指针 + 22 字节缓冲
libc++ 的核心技巧是把容量和标志位塞进指针的低位,从而省出更多缓冲空间:
// libc++ 简化布局(x86-64) struct __long { size_t __cap_; // 容量,高 63 位是容量,最低位固定为 1(长模式标志) size_t __size_; // 长度 char* __data_; // 堆指针 }; struct __short { char __data_[23]; // 22 字节有效 + 1 字节标志 // 第 23 字节:低 7 位是长度,最高位是 0(短模式标志) };由于 x86-64 的堆地址对齐到 8/16 字节,指针最低位恒为 0,libc++ 用这个空闲位做"长/短"标志,一举两得。
4.3 MSVC STL:40 字节,分 3 段
MSVC 的 std::string(x64)是 40 字节:
// MSVC 简化布局 union { struct { char buf_[16]; } _Buf; // 短缓冲,16 字节 struct { char* ptr_; } _Ptr; // 长模式指针 }; size_t _Mysize; // 长度,8 字节 union { size_t _Myres; // 容量(长模式),8 字节 char _Pad[8]; // 短模式占位 };短模式用 _Myres 的最高位区分:短模式时 _Myres 高位为 0 且 _Buf 有效;长模式时 _Ptr 有效、_Myres 是真实容量。有效短字符串 15 字节。
4.4 对比表格
| 维度 | libstdc++ (GCC) | libc++ (Clang) | MSVC STL |
| sizeof(string) | 32 B | 24 B | 40 B |
| SSO 阈值 | 15 | 22 | 15 |
| 长短标志存放 | 独立字节高位 | 指针最低位 | 容量字段高位 |
| 短模式堆分配 | 无 | 无 | 无 |
| 默认 ABI | C++11 新 ABI(_GLIBCXX_USE_CXX11_ABI) | 单 ABI | 单 ABI |
⚠️ 易错预警:GCC 5.1 之前是老的 COW ABI;GCC 5.1 起默认启用新 ABI(SSO)。新旧 ABI 的 std::string 二进制不兼容,链接报 undefined reference to std::__cxx11::basic_string 错误时,多半是编译选项 _GLIBCXX_USE_CXX11_ABI 不一致导致的。
五、容量增长策略:1.5x 还是 2x?
5.1 为什么要扩容?
短字符串用 SSO,但一旦超过阈值就要堆分配;之后 push_back / append 时容量不够,就需要扩容:申请新内存 → 拷贝旧数据 → 释放旧内存。
5.2 指数增长的数学原理
通俗类比:房租到期换大房子。如果每次只多租 1 平米(线性增长),搬 100 次家;如果每次租"当前面积 × 2"(指数增长),最多搬 log 次。
设初始容量 1,倍率 r(r > 1),扩容次数 k,最终容量 C:
C ≈ r^k
- 线性增长(每次 +1):总拷贝次数 ≈ C²/2,O(n²)。
- 指数增长(每次 ×r):总拷贝次数 ≈ C·r/(r-1),O(n)。
结论:倍率越接近 1,空间浪费越小,但总拷贝次数越多;倍率越大,拷贝越少,但浪费越多。
5.3 为什么常见实现选 2 倍或 1.5 倍?
| 策略 | 代表实现 | 优点 | 缺点 |
| 2.0x | libstdc++(_M_allocated_capacity 翻倍) | 摊还 O(1),实现简单 | 最大浪费约 50% 内存 |
| 1.5x | libc++、MSVC | 内存浪费小,且有利于内存分配器复用释放的旧块(释放块大小与新块大小有更高重叠概率,能减少碎片、提升缓存命中) | 摊还系数稍大 |
| 精确/按需 | 用户自定义 reserve | 最省内存 | 频繁扩容,性能差 |
一个真实的性能对比(插入 1000 万字符,libstdc++ vs libc++ 各自的默认策略,仅示意量级):
| 指标 | 2.0x(libstdc++) | 1.5x(libc++) |
| push_back 总耗时 | 低 | 略高 |
| 峰值内存占用 | 高(最多多占 ~50%) | 低(最多多占 ~33%) |
| 分配次数(约) | log₂(n) ≈ 24 次 | log₁.₅(n) ≈ 36 次 |
实践建议:
- 能预知长度时,先 reserve(),一次到位,避免任何扩容拷贝:
std::string s; s.reserve(1024 * 1024); // 提前预留 1MB for (int i = 0; i < 1024 * 1024; ++i) s.push_back('a'); // 全程 0 次扩容高频拼字符串优先 append / +=(走容量检查),避免 s = s + c(会创建临时对象)。
极度敏感的场景可以先用 std::string_view 描述数据,最后一次性落进 std::string。
⚠️ 易错预警:扩容会使所有指针、迭代器、引用失效(因为底层内存搬走了)。但 c_str() 返回的指针在下次修改时也会失效。写代码时不要保存 c_str() 跨多次修改使用。
六、COW 的历史与为什么 C++11 之后被禁止
6.1 什么是 COW(Copy-on-Write,写时复制)?
通俗类比:图书馆里只有一本原版书,100 个人"借"的是同一本(共享同一份数据),只有某个人要在书上写笔记时,才给他复印一份。
COW 字符串的核心:拷贝构造时不复制数据,只复制指针并增加引用计数;只有真正要修改时才深拷贝。
// COW 伪代码 class CowString { struct Rep { char* data; size_t len; size_t cap; std::atomic<size_t> refs; }; Rep* rep_; public: CowString(const CowString& other) { rep_ = other.rep_; // 只复制指针 ++rep_->refs; // 引用计数 +1(原子操作) } char& operator[](size_t i) { if (rep_->refs > 1) // 有人共享,先拷贝 detach(); // 深拷贝一份新的 return rep_->data[i]; } };6.2 COW 的致命问题
- 多线程安全:引用计数需要原子操作,operator[] 每次都要检查 refs,比非 COW 的 SSO 慢。
- 引用失效语义:char& c = s[0]; 之后如果触发 COW 深拷贝,c 指向的旧内存失效——标准要求引用在修改间保持有效,COW 做不到。
- 线程间数据意外共享:s1 = s2; 后 s1 和 s2 共享内存,std::thread 各自修改时互相干扰,引发难查的 bug。
- API 矛盾:c_str() 返回的指针,COW 下可能因为另一个线程的读操作(operator[] 读取也可能触发拷贝?)而失效。
结论:C++11 标准明确要求 operator[]、c_str() 等操作的引用有效性语义(在下次非常量操作前保持有效),COW 实现无法满足,标准库实现纷纷放弃 COW,改用 SSO + 深拷贝。libstdc++ 在 GCC 5.1(2015)默认切换到新 SSO ABI,宣告 COW 时代结束。
⚠️ 易错预警:网上很多老教程(2015 年前)还在讲 COW,如果照搬到现代 GCC 上调试,看到的全是新 ABI 行为。判断当前库是否为 SSO:直接看 sizeof(std::string) 是否为 32(GCC x64)且短字符串地址稳定在栈上。
七、手写一个迷你 SSO String(可运行)
理解了原理,自己动手写一个 200 行以内的教学版,能彻底打通"布局→标志位→扩容"这条链路。
// mini_sso_string.hpp // 迷你 SSO String:内置 15 字节缓冲 + 堆扩容,教学用 #include <cstring> #include <cstddef> #include <stdexcept> #include <utility> class MiniString { public: // 短模式缓冲区大小(有效载荷 15 字节 + 1 字节标志) static constexpr size_t kLocalBuf = 16; static constexpr size_t kSSOCap = 15; MiniString() noexcept { init_short(); } MiniString(const char* s) { init_short(); if (!s) return; size_t n = std::strlen(s); if (n <= kSSOCap) { // 短:直接进缓冲 std::memcpy(buf_, s, n); set_short_size(n); } else { // 长:堆分配 grow_and_copy(s, n); } } // 拷贝构造:深拷贝(SSO 语义,无共享) MiniString(const MiniString& other) { init_short(); assign(other.c_str(), other.size()); } // 移动构造:搬走长指针,源对象回到短空态 MiniString(MiniString&& other) noexcept { if (other.is_long()) { ptr_ = other.ptr_; len_ = other.len_; cap_ = other.cap_; other.init_short(); } else { init_short(); assign(other.c_str(), other.size()); } } ~MiniString() { if (is_long()) ::operator delete(ptr_); } MiniString& operator=(const MiniString& other) { if (this != &other) assign(other.c_str(), other.size()); return *this; } size_t size() const noexcept { return is_long() ? len_ : short_size(); } size_t capacity() const noexcept { return is_long() ? cap_ : kSSOCap; } bool empty() const noexcept { return size() == 0; } const char* c_str() const noexcept { return is_long() ? ptr_ : buf_; } char* data() noexcept { return is_long() ? ptr_ : buf_; } // 非 const 版本:返回可写引用(教学简化:直接返回,不考虑再分配) char& operator[](size_t i) { if (i >= size()) throw std::out_of_range("index out of range"); return (is_long() ? ptr_ : buf_)[i]; } const char& operator[](size_t i) const { if (i >= size()) throw std::out_of_range("index out of range"); return (is_long() ? ptr_ : buf_)[i]; } void push_back(char c) { if (size() + 1 <= capacity()) { (is_long() ? ptr_ : buf_)[size()] = c; if (is_long()) ++len_; else set_short_size(short_size() + 1); return; } // 扩容:新容量 = max(2 * cap, 1),2 倍策略 size_t newcap = capacity() == 0 ? 1 : capacity() * 2; reserve(newcap); (is_long() ? ptr_ : buf_)[size()] = c; if (is_long()) ++len_; else set_short_size(short_size() + 1); } void reserve(size_t newcap) { if (newcap <= capacity()) return; // 够用,不缩容 char* np = static_cast<char*>(::operator new(newcap)); size_t oldn = size(); std::memcpy(np, c_str(), oldn); if (is_long()) ::operator delete(ptr_); ptr_ = np; len_ = oldn; cap_ = newcap; // 进入长模式 // 确保长模式标志位 set_long_flag(); } void assign(const char* s, size_t n) { if (n <= kSSOCap) { if (is_long()) ::operator delete(ptr_); init_short(); std::memcpy(buf_, s, n); set_short_size(n); } else { grow_and_copy(s, n); } } private: union { struct { char* ptr_; size_t len_; size_t cap_; }; // 长模式 24 字节 struct { char buf_[kLocalBuf]; }; // 短模式 16 字节 }; bool is_long_ = false; // 教学简化:独立标志位(真实库用 union 位标记) void init_short() noexcept { is_long_ = false; buf_[0] = '\0'; } bool is_long() const noexcept { return is_long_; } size_t short_size() const noexcept { return std::strlen(buf_); } void set_short_size(size_t n) noexcept { buf_[n] = '\0'; } void set_long_flag() noexcept { is_long_ = true; } void grow_and_copy(const char* s, size_t n) { cap_ = n + 1; // 至少容纳 n 字符 + '\0' ptr_ = static_cast<char*>(::operator new(cap_)); std::memcpy(ptr_, s, n); ptr_[n] = '\0'; len_ = n; is_long_ = true; } };配套测试程序:
// mini_sso_test.cpp #include "mini_sso_string.hpp" #include <cstdio> int main() { MiniString a = "hello"; // SSO,15 字节内 MiniString b = a; // 深拷贝,互不影响 b[0] = 'H'; std::printf("a=%s b=%s\n", a.c_str(), b.c_str()); // hello Hello MiniString c = "0123456789ABCDE"; // 正好 15 字符 std::printf("c size=%zu cap=%zu c_str addr=%p (栈上)\n", c.size(), c.capacity(), (const void*)c.c_str()); MiniString d = "this is a much longer string beyond sso"; // 触发堆分配 std::printf("d size=%zu cap=%zu\n", d.size(), d.capacity()); for (int i = 0; i < 100; ++i) d.push_back('x'); // 触发多次 2 倍扩容 std::printf("after push_back: size=%zu cap=%zu\n", d.size(), d.capacity()); MiniString e = std::move(d); // 移动:偷指针,不拷贝 std::printf("moved: e size=%zu, d empty=%d\n", e.size(), d.empty()); return 0; }编译运行:
g++ -std=c++17 -O2 -Wall -Wextra mini_sso_test.cpp -o mini_sso && ./mini_sso预期输出:
a=hello b=Hello c size=15 cap=15 c_str addr=0x7ffd... (栈地址,说明没堆分配) d size=42 cap=43 after push_back: size=142 cap=172 moved: e size=142, d empty=1
⚠️ 易错预警:真实库的短模式标志位藏在 union 字节里,我的教学版用独立 bool 简化——但这也演示了为什么真实库要用 union:独立 bool 会让 sizeof(MiniString) 变成 32(24+8 对齐),比 libstdc++ 的 32 大一倍内存浪费。真实实现用位标志就是为了不增加对象大小。
八、性能实测与优化建议
8.1 基准测试:SSO vs 无优化实现
用 100 万次短字符串构造对比(示意,环境不同数值会有差异):
| 场景 | libstdc++ (SSO) | 无优化 NaiveString(每次堆分配) | 提升 |
| 构造 1 万次 "abc" | ~0.02 ms | ~1.8 ms | ~90x |
| 拷贝 1 万次短串 | ~0.02 ms | ~1.6 ms | ~80x |
| 拼接 10 万字符 | ~0.9 ms | ~3.4 ms(含多次扩容) | ~3.8x |
结论:短字符串场景,SSO 的收益是数量级的——因为它完全绕开了 malloc/free(系统调用 + 锁 + 内存碎片)。
8.2 优化建议清单
- 优先 reserve:已知长度先预留,避免扩容拷贝。
std::string s; s.reserve(input.size() + 64); // 一次到位避免不必要的拷贝:函数入参用 const std::string& 或 std::string_view,返回用 std::string(依赖 RVO/移动)。
短字符串尽量别超过阈值:如果业务 key 基本 < 15 字节,SSO 就永远不触发堆分配。
批量拼接用 std::string::append 或 ostringstream:少建临时对象。
警惕 s = s + c 模式:
// 差:产生临时对象,两次数拷贝/移动 for (char c : chars) s = s + c; // 好:就地追加 for (char c : chars) s.push_back(c); // 或 s.append(chars.begin(), chars.end());8.3 内存观察工具
# 看 string 对象的内存布局 g++ -std=c++17 -g sso.cpp -o sso gdb ./sso (gdb) break main (gdb) run (gdb) p s # 查看 union 内部 (gdb) p &s # 栈地址九、进阶:空基类优化、迭代器失效、与 string_view 的关系
9.1 迭代器/引用失效规则(背下来)
| 操作 | 迭代器/引用是否失效 |
| push_back/append(触发扩容时) | 全部失效(内存搬家) |
| push_back/append(未扩容) | 迭代器不失效;c_str() 指针仍有效 |
| reserve(n)(n > capacity) | 全部失效 |
| erase/insert/replace | 指向被删/插入位置之后元素的迭代器失效 |
| shrink_to_fit | 可能全部失效 |
| 只读操作(size/c_str/operator[] const) | 不失效 |
⚠️ 易错预警:c_str() 返回的指针在任何下一次非常量操作后都可能失效,包括 operator[] 非 const 版本(如 s[0] = 'x')。把 c_str() 存下来跨多次修改使用是经典 bug 来源。
9.2 std::string_view:不拥有的"望远镜"
通俗类比:std::string 是"自己买了房子",std::string_view 是"拿着望远镜看别人家房子"——只读、不拥有、不拷贝。
void print_len(std::string_view sv) { // 零拷贝,可接受 string/字面量/char* std::cout << sv.size() << "\n"; } int main() { std::string s = "hello world"; print_len(s); // string → view:O(1) print_len("literal"); // 字面量 → view:O(1) print_len(std::string_view(s).substr(0, 5)); // 切片不拷贝 }适用场景:只读解析(JSON 解析器、协议解析、日志解析)用 string_view 可以避免大量字符串拷贝。
⚠️ 易错预警:string_view 不持有数据,悬垂风险极高:
std::string_view bad() { std::string tmp = "temporary"; return tmp; // tmp 析构后,view 悬垂!UB }9.3 空基类优化(EBO)与 string
std::string 继承自 std::char_traits<char> 等空基类时,借助 EBO,空基类不占空间——这也是 libstdc++/libc++ 能把对象压到 24/32 字节的原因之一。如果你自己写字符串类并组合空类成员,会白白多出字节:
class Bad { std::char_traits<char> t_; // 空类,但作为成员要占 1 字节 + 对齐填充 char data[15]; }; // sizeof = 16(多 1 字节) class Good : std::char_traits<char> { // 继承空基类,EBO 生效 char data[15]; }; // sizeof = 15十、C++23 新能力:resize_and_overwrite
C++23 给 std::string 加了 resize_and_overwrite,允许就地覆盖缓冲区,避免一次多余的清零/初始化:
// C++23 std::string s(64, '\0'); s.resize_and_overwrite(64, [](char* buf, std::size_t n) { // 往 buf 里写最多 n 字节,返回实际写入长度 std::memcpy(buf, "hello", 5); return 5; // 返回 5,字符串长度变为 5 }); // s == "hello"适用场景:网络收包、文件读取等"知道缓冲大小,需要直接写"的场景,省掉一次 memset。
⚠️ 易错预警:回调里必须返回实际写入长度,返回 0 会导致字符串为空;写越界仍是 UB,不会自动保护。
十一、常见问题速查表(FAQ)
| 问题 | 答案 |
| sizeof(std::string) 为什么固定不变? | 对象本体只存指针/长度/容量或短缓冲,长数据在堆上 |
| 短字符串为什么不堆分配? | SSO:字符存在对象内部缓冲区,阈值 15(GCC)/22(libc++) |
| 阈值是多少?能改吗? | 由实现决定,不可配置;GCC x64 为 15 |
| b = a; b[0]='H' 会影响 a 吗? | 不会。现代实现是深拷贝(SSO 语义),非 COW |
| 扩容为什么是 2 倍? | 摊还 O(1) 拷贝成本;libc++ 用 1.5 倍更省内存 |
| c_str() 什么时候失效? | 任何非常量操作后都可能失效(扩容/修改都会) |
| string 和 string_view 区别? | string 拥有数据;view 只读借用,不拷贝、易悬垂 |
| GCC 报 std::__cxx11 链接错误? | ABI 不匹配:部分编译单元用了 _GLIBCXX_USE_CXX11_ABI=0 |
| 如何避免扩容开销? | reserve() 预分配;批量拼接用 append |
| COW 还能用吗? | 标准要求已不允许;现代库全用 SSO |
| 32 位平台 sizeof 多少? | libstdc++ 为 16 字节,SSO 阈值 7 |
| 空字符串 std::string s; 会分配吗? | 不会,短模式空态,零分配 |
十二、总结
- SSO 是现代 std::string 的灵魂:短字符串(≤15/22 字节)直接存在对象内部,完全绕开堆分配,性能提升数量级。
- 长短模式共用 union:用标志位区分(libstdc++ 独立字节高位、libc++ 指针低位、MSVC 容量高位),对象大小恒定。
- 扩容用指数策略(2x / 1.5x),摊还 O(1);能预知长度就先 reserve。
- COW 因线程安全和引用语义问题被 C++11 标准淘汰,别再按老教程写 COW 字符串。
- 牢记失效规则:c_str() 和迭代器在扩容/修改后会失效;只读场景用 string_view 但要防悬垂。
一句话记忆:std::string 是"带夹层的智能行李箱"——短东西放夹层(SSO),长东西租仓库(堆),夹层塞不下就换大仓库(扩容),换仓库后旧钥匙作废(迭代器失效)。