尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

STL 容器内幕:vector 的三个指针与 string 的 SSO

发布时间:2026/9/29 3:30:50

资讯中心
01
ARTICLE

STL 容器内幕:vector 的三个指针与 string 的 SSO

STL 容器内幕:vector 的三个指针与 string 的 SSO
① 钩子24 字节装下一百万个 intsizeof(std::vectorint)只有24 字节却能装下一百万个int——因为它自己只存三个指针。std::string的短字符串免费、不碰堆分配vector扩容按 2 倍翻。这些魔法在汇编和实测数据里全都能看见。这一集我们拆开两个最常用的容器vector三个指针 连续存储和stringSSO 短字符串优化看它们的布局、扩容、和为什么快。② 源码 vs 实测/汇编对照__attribute__((noinline))intv_get(conststd::vectorintv,size_t i){returnv[i];}intmain(){std::string s_shorthi;std::string s_longthis is a much longer string that surely exceeds the short string buffer;...std::vectorintv;for(inti0;i100;i)v.push_back(i);// 打印每次容量变化}实测输出g 15.2.0x86-64sizeof(string)32 sizeof(vectorint)24 short: obj0x...fd20 data0x...fd30 delta16 ← SSO数据在对象内部偏移 16 long: obj0x...fd40 data0x...4150 delta... ← 超过 15B 才堆分配地址在堆上很远 push 0 - capacity 1 (data 换地址) push 1 - capacity 2 (data 换地址) push 2 - capacity 4 (data 换地址) push 4 - capacity 8 ... push 8 - capacity 16 ... push 16 - capacity 32 ... push 32 - capacity 64 ... push 64 - capacity 128 ... ← 按 2 倍扩容每次 data 都搬家string 构造的汇编 —— SSO 分界就是一次比较basic_string::_M_construct(...): subq %rdx, %r8 ; 字符串长度 cmpq $15, %r8 ja .L10 ; 长度 15 → 走堆分配 ... ; ≤15B直接用对象内部的 16B 缓冲SSO零分配 ret .L10: call _M_create(...) ; 超长 → 堆分配 ... call memcpy ; 复制进新缓冲vector::operator[] —— 基址 下标 × 4一条指令v_get(vectorint const, size_t): movq (%rcx), %rax ; 第一个成员 data 指针 movl (%rax,%rdx,4), %eax ; data[i]连续存储的直接寻址 retvector 扩容push_back 满时的展开... call _Znwy ; 分配新容量旧容量 × 2 ... call memcpy ; 把旧元素整体搬过去 call _ZdlPvy ; 释放旧缓冲环境备注本集在 x86-64 Windows MinGW g 15.2.0libstdc实测。libstdc 的 SSO 阈值是 15 字节、对象 32 字节MSVC 的 STL 是 16 字节阈值数字略有差异机制一致。③ 为什么这么设计vector 三个指针begin/end/capacity→ 24 字节。元素连续存放operator[]是基址 下标直接寻址一条指令所以迭代快、缓存友好——这也是它比list快得多的根本原因。扩容 2 倍让插入的均摊成本是 O(1)。每次满时分配 2 倍新缓冲、把旧元素搬过去、释放旧的。代价是搬动时所有元素地址都变了迭代器全部失效且大对象搬动成本高——此时移动语义见 E12就是救命稻草。string 的 SSO短字符串优化libstdc 里 15 字节以内直接存在对象内部的 16 字节缓冲里完全不碰堆。只有超过 15B 才_M_create堆分配。cmpq $15, %r8就是这条分界线——短字符串免费是真的。为什么 string 占 32 字节对象里要塞下 16B 内部缓冲 大小 容量 指向缓冲的指针32 字节刚好装下整套状态。④ 深入一vector 的三个指针到底是什么std::vectorT的内部libstdc 实现通常是这样偏移 0 T* _M_start begin首元素地址 偏移 8 T* _M_finish end最后一个元素之后 偏移 16 T* _M_end_of_storagecapacity缓冲区末尾size()end - begin指针相减一条指令capacity()end_of_storage - beginoperator[]begin[i]基址寻址本集汇编已见全部操作都是指针算术没有遍历——这就是 vector轻的原因24 字节任何时刻都知道首尾和容量。对比std::listTlist 每个节点独立分配、存前后指针sizeof(list)也约 24 字节首尾 哨兵但访问要沿链走、缓存不友好。vector 的连续存储是缓存友好的机器基础。⑤ 深入二扩容的均摊 O(1)数学为什么 2 倍扩容让 push_back 均摊 O(1)每次扩容分配 2 倍大小搬移旧元素。设最终容量为 N则搬移成本总和 ≈ N/2 N/4 N/8 … N所以 N 次 push_back 的总成本 ≈ 2N每次 push 本体 摊到每次的搬移均摊 O(1)若按每次 1扩容总成本 O(N²)——灾难。工程含义知道规模就reserve把分配搬移从 N 次摊平变成 0~1 次。这也是性能敏感代码先 reserve的数学理由。⑥ 深入三SSO 的完整机制libstdc 的std::string32 字节布局大致是偏移 0 union { char _M_local_buf[16]; char* _M_data; } ← 短串用内部缓冲 / 长串用指针 偏移 16 size_t _M_string_length 当前长度 偏移 24 union { size_t _M_capacity; ... } 容量 / 短串标记短串≤15B数据放内部_M_local_buf_M_data指向自己内部data obj 16实测 delta16 就是证据长串_M_data指向堆缓冲切换逻辑就是本集cmpq $15, %r8; ja 堆分配这一条比较代价string 对象变大32 字节 vs 可能更小的实现换来大多数短字符串零堆分配。SSO 的意义绝大多数实际字符串都很短路径、键、名字SSO 让它们完全不碰堆——这在大量字符串场景容器、map 键里是巨大的性能红利。这也是为什么 string 32 字节还划算的原因。⑦ 常见误区误区 1“vector扩容会拷贝所有元素很慢”扩容频率低2 倍均摊 O(1)且 C11 起优先移动noexcept 时。真正要避免的是频繁小扩容——reserve解决。误区 2“string总是堆分配”短串 SSO 零分配。只有超过 SSO 阈值才堆分配。误区 3“vector和数组差不多list更灵活”list的灵活任意插入 O(1)换来节点分散、缓存差、无随机访问。99% 场景vector更快。误区 4“reserve和resize一样”reserve只扩容量不构造元素size 不变resize构造/销毁元素改变 size。用错会多构造或越界。误区 5“operator[]会检查越界”不检查快速。at()才检查抛异常E10 讲过。想安全用at()想快用operator[]。误区 6“vector的size()是 O(1) 遍历计数”size()就是end - begin一次指针相减本集 ④ 讲过O(1) 且常被优化成寄存器差零成本。误区 7“std::map一定比unordered_map慢”不一定。数据量小/键是小整数时红黑树的 log n 和缓存行为可能反而胜过哈希的哈希计算 桶冲突 链遍历。选型要实测别只看复杂度记号。误区 8“std::vector存不下就是容量不够”还可能抛std::bad_alloc分配失败或触发移动/拷贝异常。处理大容器时记得 try/catchbad_alloc或预估内存。误区 9“std::string的一定比快”s t若容量够就地追加零分配s t总是构造新 string分配。但两者若触发重新分配就都慢。用reserve规划容量最稳。误区 10“std::array和vector性能一样”访问都 O(1)、缓存友好。但array是栈/内嵌无堆分配、构造零vector是堆构造要分配。array通常更轻但大小编译期固定。误区 11“容器都适合多线程并发使用”标准容器非线程安全——两个线程同时改同一 vector/map 是数据竞争UBE16 会讲。要么加锁要么用并发专用容器TBB/并发 queue 等。误区 12“vector的元素一定在堆上”元素在vector 自己的缓冲区里缓冲区由_M_start指向堆分配。但std::array/局部数组在栈上。别把vector与栈/堆直接划等号。⑧ 实战启示知道要装多少就reserve避免反复扩容 → 搬移 → 释放每次都是分配 复制 释放。不要长期保存 vector 的迭代器/裸指针扩容后全部失效下一集 E15 展开。大量小字符串用string很划算SSO 不堆分配但字符串集合别用vectorstring反复拷贝考虑string_view、合并缓冲。按使用方式选容器vector连续 缓存友好适合随机访问/尾插map是红黑树 节点分散、O(log n) 查找unordered_map是哈希表 期望 O(1) 但更占内存、无序遍历。先想怎么用再选容器。大对象容器用vectorunique_ptrT搬动只搬指针8 字节避免大对象整体搬移E13 讲过。⑨ 扩展专题一vector 与缓存友好——为什么连续这么值钱E21 会专门讲性能这里先铺垫vector的连续存储是它碾压list的根本原因因为CPU 缓存按缓存行64B加载遍历vectorint顺序访问每个缓存行命中 16 个 int64B/4B几乎全命中遍历listint每个节点散落在堆上节点 前后指针 int约 24B每次访问大概率新缓存行 未命中惩罚实测遍历 vector 通常比 list 快一个数量级几十倍即使操作数相同。机器理由缓存是空间局部性的游戏。vector天然按顺序排布list天然打散。所以能连续就连续不只是风格是让 CPU 少跑内存的硬道理。⑩ 扩展专题二string_view 为什么免费std::string_view是一个只读字符串的窗口voidlog(std::string_view sv){fwrite(sv.data(),1,sv.size(),stderr);}内部只有两个指针/一个指针长度16 字节不拥有数据、不拷贝、不分配传入std::string/const char*/ 子串都零拷贝代价view 不保证数据生命周期——被 view 的字符串销毁后 view 悬垂和引用比拷贝快但危险同理。对比log(const std::string)若临时构造 string 可能触发拷贝分配string_view直接读原数据。这就是避免多余拷贝的又一招——但它要求调用方保证数据存活。⑪ 扩展 FAQQvector的data()返回什么A首元素指针_M_start可当数组用v[0]。空 vector 时可能返回空/未指定别解引用。Qshrink_to_fit一定缩容吗A非强制实现可能忽略。它尝试把 capacity 缩到 size但可能仍有对齐/实现保留。Qunordered_map为什么更占内存A哈希表要桶数组 节点 负载因子预留空间通常比map红黑树更费内存且迭代无序。换来期望 O(1) 查找。Qdeque是连续的吗A分块连续块内连续、块间链接。支持两端插入 O(1)operator[]O(1)但缓存友好度略逊 vector迭代器结构复杂。Qstd::array和vector区别Aarray是固定大小的栈/内嵌数组无堆分配、size 编译期定vector是动态堆数组。能用array就用零分配。⑫ 扩展实验跑容量序列运行 demo 看 push_back 的容量变化1/2/4/8/…/128复现2 倍扩容。SSO 分界构造 14/15/16/17 字节字符串观察data()地址与对象地址的 delta≤15 在内部、15 在堆。vector 布局打印sizeof(vectorint)24 与三个成员的相对偏移v与v[0]。reserve 前后对比push_back1 万次对比有/无reserve的耗时与分配次数。at 与 operator[]越界访问对比反汇编确认at有检查、operator[]没有。⑭ 扩展专题三vector 扩容与移动语义的配合E12 落地E12 讲过vector 扩容依赖 noexcept 移动现在看完整机制push_back满时新容量 旧 × 2_Znwy分配新缓冲搬移旧元素若元素可移动且 noexcept→ 逐个移动构造偷指针快否则 → 拷贝构造可能分配慢旧元素析构 旧缓冲_ZdlPvy释放更新三个指针_M_start/_M_finish/_M_end_of_storage。对vectorstd::string扩容每个 string 若超过 SSO移动只偷指针 置空——比拷贝分配 memcpy 内容便宜得多。这就是给移动构造标 noexcept让 vector 敢用移动的直接回报。一个反直觉点vector扩容失败bad_alloc时已搬元素已离开旧缓冲——标准用移动时不能保证强异常安全所以noexcept移动才被允许。这就是为什么可能抛的移动会让 vector 回退拷贝。⑮ 扩展专题四容器选型决策树从怎么用出发选容器需要随机访问[] / at? ├─ 需要动态大小 → vector默认首选 └─ 大小固定 → std::array 需要频繁头尾插删? ├─ 只尾插/尾删 → vector / deque └─ 头尾都要 → deque分块连续 需要按 key 查找? ├─ 有序 → std::map红黑树O(log n) └─ 无序/更快 → std::unordered_map哈希期望 O(1)更费内存 需要有序唯一集合 → std::set / std::unordered_set 需要先进先出 → std::queuedeque 适配 需要小数据集 → vector 线性扫描即可缓存赢过 log n一句话不知道选什么就vector。它连续、缓存友好、均摊 O(1) 尾插是绝大多数场景的最优解只有明确随机访问不需要/有序键查找/头插为主才换其他容器。⑯ 扩展 FAQ第二轮Qvectorbool是什么A特化版本按位存储不是 bool 数组——省内存但元素是代理对象operator[]返回代理而非 bool可能有意外的性能/语义坑。别默认用需要位集考虑std::bitset。Qemplace_back和push_back区别Aemplace_back(args...)在容器内就地构造省一次临时对象构造移动push_back(x)先构造 x 再移动/拷贝进容器。能用emplace_back尽量用配合 E11/E12 的零临时对象思想。Qstring的c_str()是 O(1) 吗A是c_str()返回内部_M_data含结尾\0长串是堆指针、短串是内部缓冲指针。别长期持有它后续修改字符串会失效。Qvector能用std::initializer_list构造吗A能vectorint v{1,2,3}它会先放临时数组再拷/移入——小列表无所谓大列表注意两次拷贝。Qstd::span和string_view关系AspanT是任意连续序列的窗口C20string_view是其 char 特化。都是不拥有、零拷贝、要保证生命周期。⑰ 扩展实验第二轮emplace vs push_backvectorstd::string用emplace_back(10,x)与push_back(std::string(10,x))反汇编对比临时对象构造次数。扩容搬移类型vectorBigNoexceptvsvectorBigThrowy扩容看反汇编是移动还是拷贝E12 讲过 noexcept 开关。vector 坑auto b vb[0];看类型是std::_Bit_reference而非bool体验代理对象的怪异。string_view 生命周期返回局部 string 的 view运行可能 UB看悬垂——理解view 不拥有。容器性能对比随机访问 遍历vector vs list vs map vs unordered_map计时对比数量级。⑲ 扩展专题五unordered_map 的期望 O(1)是怎么实现的std::unordered_map是哈希表链地址法内部大致桶数组bucket arrayvector 形态 桶 0 → 节点链红黑树不用这里用单向/双向链表 桶 1 → ... 桶 N → ... 节点{ key, value, 下一个节点指针 }find(key)算哈希 → 定位桶 → 沿链比较 key → 命中/未命中期望 O(1)负载因子低时桶里链很短通常 1 时平均每桶不到 1 个节点最坏 O(n)所有 key 撞进同一桶差哈希函数/被攻击。代价桶数组 节点分散缓存不友好 无序遍历 内存比 map 大。汇编层面find 哈希计算几条乘/移位 桶定位数组索引 链遍历比较循环。对比map的红黑树沿树指针下降log n 步。哈希通常更快但顺序性和稳定性不如树。⑳ 扩展专题六map 的红黑树节点长什么样std::mapK,V是红黑树自平衡二叉搜索树节点{ color(红/黑), left, right, parent, key, value }插入/删除/查找都是 O(log n)沿树下降比较 key节点各自分配不连续→ 遍历缓存不友好对比 vector 的连续;operator[]不存在就插入依赖查找 插入可能触发再平衡变色 旋转为什么有序中序遍历给出升序begin到end就是排序好的。选型需要按序访问/区间查询→map只求按 key 快速取→unordered_map。两者内部结构树 vs 哈希表决定了它们的性能画像——这正是本集容器 数据结构 内存布局的落地。㉑ 扩展 FAQ第三轮Qstd::deque的operator[]真的 O(1) 吗A是分块映射块指针数组 块内索引两次间接但比 vector 的一条指令慢且迭代器更复杂。Qvector插入中间为什么贵A要挪动后面所有元素O(n) 移动。insert在中间 搬移尾段 构造新元素。头插最贵尾插最便宜。Qstd::string拼接快吗Aa b会创建新 string分配 拷贝。连续若容量够则就地快反复可能反复分配——用reserve或append。Qstd::list什么时候值得用A需要O(1) 在任意位置插入/删除且迭代器稳定如 LRU 缓存、中间插删频繁。否则 vector 更快。Q容器的迭代器为什么浅拷贝很安全A迭代器通常是指针/指针对拷贝就是复制指针无所有权——但失效规则由容器结构决定vector 扩容全失效、list/map 插入不失效。Qstd::vector能存引用吗A不能引用不是对象无法按值存放。存std::reference_wrapperT或指针。这也是vector 存智能指针而非引用的原因。Qstd::string的substr会拷贝吗Asubstr返回新 string拷贝子串。想零拷贝看子串用string_viewE14 ⑩ 讲过。Q为什么vector::insert在首部最慢A首插要搬移全部已有元素O(n)尾插搬移 0 个O(1) 均摊。需要头插请用deque或list。Qstd::vector的swap是 O(1) 吗A是只交换三个指针swap或vector::swap。比拷贝所有元素快得多——这也是用 swap 实现强异常安全copy-and-swap的机器基础。Q为什么vector不设默认初始容量大点A省内存小 vector 不浪费。用reserve显式指定即可。默认按需扩容是最省内存的起点。Qstd::string和std::vectorchar选哪个A文本用 string有 SSO、c_str、编码辅助二进制缓冲区用 vector或std::byte。string 的 SSO 对短文本是显著优势。Qvector元素地址会变那std::reference_wrapper呢Areference_wrapper存的是地址vector 扩容后地址变了wrapper 指向旧地址 → 悬垂。稳定地址要么用 list/map要么存索引。㉒ 扩展实验第三轮哈希 vs 树同量级数据unordered_map与map插入/查找计时观察差距与内存占用。list vs vector 遍历遍历 100 万元素实测 list 慢多少倍缓存局部性的直观证明。string 拼接方式、预 reserve、append三种拼接 10 万次对比分配次数/耗时。vector 中间插入往 vector 中间插入 1 万次 vs 尾插计时对比 O(n) 搬移成本。deque vs vector 头插头插 10 万次deque O(1) vs vector O(n)计时对比。㉓ 悬念既然 vector 扩容会让所有地址搬家那迭代器这种指向容器内部的东西到底什么时候会失效range-for为什么被叫最安全的遍历
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。