Chapter 10: Vector
std::vector 要解决的问题,是在元素数量运行期变化时,仍然给调用者提供接近数组的访问模型:元素顺序稳定、地址连续、下标访问常数时间、尾部追加摊还常数时间。读完本章后,读者应能从一次 vector 操作判断四件事:底层存储是否更换,哪些元素生命周期发生变化,哪些 iterator / pointer / reference 失效,以及异常路径上旧状态如何被保护。
本章使用一个贯穿材料:批量收集日志记录。日志记录在写入阶段不断追加,偶尔按时间插入一条补录记录,末尾完成后交给 C 接口读取连续内存。这个场景能覆盖 vector 的核心状态:size 表示已经构造的元素数量,capacity 表示当前分配的原始存储能容纳的元素数量,data() 暴露连续元素区域的起点。
标准语义层面,std::vector 是动态大小数组的序列容器,普通 T 版本满足连续容器要求;cppreference 的 std::vector 页面 汇总了连续存储、复杂度和失效规则。实现层面,libstdc++、libc++ 和 MSVC STL 的内部字段命名不同,但常见形状都可以抽象成三段边界:已分配存储起点、已构造元素末尾、已分配存储末尾。工程层面,所有判断都围绕这三段边界展开。
#include <string>
#include <vector>
struct LogRecord {
LogRecord(long long ts, std::string msg)
: timestamp(ts), message(std::move(msg)) {}
long long timestamp;
std::string message;
};
void collect_logs(std::vector<LogRecord>& records) {
records.reserve(1024);
records.emplace_back(1001, "start");
records.emplace_back(1002, "connect");
records.insert(records.begin() + 1, LogRecord{10015, "late record"});
}
这段代码中的 reserve、emplace_back 和 insert 共同推动 vector 在“原始存储容量”和“有效对象数量”之间移动边界。后续小节会持续回到这个例子,用同一套顺序解释每个操作:先看容量,再看元素构造和移动,再看提交点,最后看迭代器失效。
10.1 vector 的连续存储模型与三指针状态
vector 的连续存储模型把容器表示成一段可按下标寻址的元素数组。对 std::vector<LogRecord> 来说,records.data() 指向第一个 LogRecord,records.data() + records.size() 指向已构造元素之后的位置,records.data() + records.capacity() 指向当前分配存储之后的位置。size() 统计有效对象,capacity() 统计当前存储最多还能容纳多少个同类型对象。
常见实现把这个关系压缩成三个指针。不同实现可能使用指针、压缩 pair、allocator 基类优化或不同字段名,但抽象状态一致。第一段 [begin, end) 中的每个槽位已经完成构造,可以通过 operator[]、iterator 或 data() 间接访问。第二段 [end, cap) 只是原始存储,已经由 allocator 分配,里面还没有有效 LogRecord 对象。
// 教学简化结构,用于解释状态关系,非任何标准库真实源码。
template<class T, class Alloc>
struct vector_storage_shape {
T* begin; // 已分配存储起点,也是第一个已构造元素的位置
T* end; // 已构造元素末尾
T* cap; // 已分配存储末尾
Alloc alloc;
};
这个简化结构解释了 size() 和 capacity() 的来源:size() 等于 end - begin,capacity() 等于 cap - begin。empty() 只检查 begin == end。data() 返回 begin,当容器为空时返回值可以为空指针,也可以是某个非空哨兵值,调用者只能在 size() > 0 时解引用 data()。
三指针模型还解释了 vector 能把元素交给 C 接口的原因。普通 std::vector<T> 的元素连续存放,data() 返回的指针可以按照数组指针使用。日志批量写出时,如果底层接口接受 const LogRecord* 加长度,records.data() 和 records.size() 就能表达完整输入范围。
void write_records(const LogRecord* first, std::size_t count);
void flush(const std::vector<LogRecord>& records) {
if (!records.empty()) {
write_records(records.data(), records.size());
}
}
这段代码成立的前提是 records 在 write_records 使用期间保持存活,并且没有并发修改触发重分配。data() 暴露的是当前存储地址,地址稳定性依赖后续操作是否改变 capacity()。只读访问保持存储不变;追加、插入、reserve、shrink_to_fit 和某些 resize 调用都有可能更换存储。
下面的状态图把三指针关系和操作入口合在一起。图中 end 只前后移动时,旧存储保持;begin 和 cap 同时换到新块时,所有旧地址都失效。
判断 vector 状态时,先把问题翻译成三段边界:元素是否已经构造,存储是否仍属于当前容器,某个地址是否落在 [begin, end) 内。这个顺序比背 API 名称稳定,因为同一个 API 在不同输入下可能走不同路径。
10.2 reserve、resize 与 shrink_to_fit 的状态改变
reserve、resize 和 shrink_to_fit 都和容量相关,但它们改变的对象不同。reserve 只要求容量至少达到某个值;resize 改变有效元素数量;shrink_to_fit 请求释放未使用容量。判断这三个 API 时,先问它们是否改变 capacity(),再问它们是否构造或销毁元素。
reserve(n) 的核心语义是为未来元素预留存储。当 n <= capacity() 时,容器状态保持在原存储块上,size() 不变,元素生命周期不变。当 n > capacity() 时,容器分配新存储,把旧 [begin, end) 中的有效元素迁移到新存储,销毁旧元素,释放旧存储,然后把三指针指向新块。
std::vector<LogRecord> records;
records.reserve(1024); // size == 0, capacity >= 1024
records.emplace_back(1, "a");
records.emplace_back(2, "b");
const LogRecord* first = records.data();
records.reserve(1024); // capacity 未增长时,first 仍指向同一块存储
records.reserve(2048); // capacity 增长时,first 指向的旧存储失效
这段代码的关键点是 reserve 不创建 LogRecord。第一次 reserve(1024) 后,records[0] 仍然没有对象。只有 emplace_back、push_back、insert、增大 resize 等操作才会在原始存储上构造元素。把 reserve 当成“创建元素”会导致越界写入。
std::vector<int> values;
values.reserve(10);
// values[0] = 42; // 错误路径:size 仍为 0,位置 0 尚无有效 int 对象。
values.push_back(42); // 正确路径:先构造元素,再进入 [begin, end)。
resize(n) 直接改变 size()。当 n 小于当前大小时,尾部多余元素被销毁,容量通常保持。对于日志场景,records.resize(1) 会销毁从索引 1 开始的记录对象,这些对象中的 std::string 成员也随之析构。当 n 大于当前大小时,vector 需要在末尾构造新元素;如果当前容量不足,会先走重分配路径。
std::vector<std::string> names;
names.reserve(3);
names.push_back("alpha");
names.resize(3); // 构造两个空 string,size 变成 3
names.resize(1); // 销毁索引 1 和 2 的 string,capacity 通常仍为 3
resize 对元素类型有额外要求。无参数扩大的 resize(n) 需要能值初始化新元素;resize(n, value) 需要能从 value 拷贝构造新元素。对于只支持移动且没有默认构造的类型,调用形式会直接影响能否编译。
struct Token {
Token(int id) : id(id) {}
Token(const Token&) = delete;
Token(Token&&) noexcept = default;
int id;
};
std::vector<Token> tokens;
tokens.emplace_back(1);
// tokens.resize(2); // Token 没有默认构造函数,扩大 resize 不满足构造条件。
tokens.emplace_back(2);
shrink_to_fit() 表达的是收缩请求。标准层面它是非强制请求,具体实现可以释放未使用容量,也可以保持当前容量。工程判断应按实际 capacity() 变化确认它是否归还内存。调用后如果容量改变,所有 iterator、pointer 和 reference 都按重分配处理;如果容量不变,原有地址保持。
容量管理的稳定顺序是:增长前用 reserve 降低重分配次数;元素数量用 resize 表达;峰值之后需要主动降低内存占用时再考虑 shrink_to_fit,并把所有旧地址视为需要重新获取。日志收集场景中,如果可以预估最多 1024 条记录,先 reserve(1024) 能把大量尾部追加限制在同一块存储内。
10.3 push_back、emplace_back、insert、erase 与 clear
修改操作的共同问题是:哪些对象被构造,哪些对象被移动或赋值,哪些对象被销毁,边界指针如何更新。push_back 和 emplace_back 只在末尾增加一个元素;insert 在中间打开空位;erase 删除一个区间并把后缀元素前移;clear 销毁全部有效元素但通常保留容量。
push_back 接受一个已经存在的值,并从这个值拷贝或移动构造末尾元素。emplace_back 接受构造参数,并直接在末尾目标位置构造元素。二者在容量足够时都只移动 end,不改变 begin 和 cap。当容量不足时,二者都会进入重分配路径。
std::vector<LogRecord> records;
records.reserve(2);
LogRecord first{1001, "start"};
records.push_back(first); // 从 first 拷贝构造末尾元素
records.emplace_back(1002, "connect"); // 用参数直接构造末尾元素
这段代码展示的是构造入口差异。emplace_back 减少了临时 LogRecord 的显式创建,但它仍然可能触发整段重分配。工程上判断成本时,先看容量是否足够,再看构造参数形式。容量不足时,旧元素迁移成本通常大于单个新元素的构造差异。
insert(pos, value) 的状态变化更复杂。容量足够时,容器要在 pos 位置腾出一个槽位:后缀元素向后移动,目标位置构造或赋值新值,end 向后移动。容量不足时,容器分配新存储,按顺序构造前缀、新元素、后缀,然后提交新三指针。
std::vector<LogRecord> records;
records.reserve(4);
records.emplace_back(1001, "start");
records.emplace_back(1002, "connect");
records.emplace_back(1003, "done");
records.insert(records.begin() + 1, LogRecord{10015, "late record"});
在这个例子中,插入位置之后的 connect 和 done 需要后移。连续存储带来的代价就在这里:中间插入保持下标连续,代价是移动后缀元素。插入位置越靠前,后缀越长,移动成本越高。
erase(pos) 或 erase(first, last) 的方向相反。被删除区间内的元素生命周期结束,后缀元素前移覆盖空洞,end 向前移动。容量通常保持,所以 erase 后 capacity() 仍可能很大。它返回新的有效位置,工程代码应使用返回值继续遍历。
std::vector<LogRecord> records = {
{1001, "start"},
{1002, "connect"},
{1003, "done"}
};
for (auto it = records.begin(); it != records.end(); ) {
if (it->message == "connect") {
it = records.erase(it); // 返回删除位置之后的新位置
} else {
++it;
}
}
这段代码的关键是 erase 后不继续使用旧 it。删除位置及其后面的元素位置都发生了变化,旧 iterator 已经失去原容器中的稳定定位意义。返回值是标准接口提供的新定位点。
clear() 销毁 [begin, end) 中的所有有效元素,然后把 end 调回 begin。它通常不释放底层存储,所以 capacity() 保持。日志缓冲区反复使用时,clear() 可以把元素生命周期清空,同时复用已经分配的容量。
std::vector<std::string> buffer;
buffer.reserve(1000);
buffer.push_back("one batch");
buffer.clear(); // size == 0,字符串对象已销毁
buffer.push_back("next batch"); // 复用已有容量
修改操作的可复用判断顺序是:尾部追加优先检查是否还有容量;中间插入检查后缀移动长度和容量;删除检查被删区间和后缀前移范围;清空检查元素析构和容量保留。这个顺序能直接推出复杂度、失效范围和对象生命周期变化。
10.4 reallocate 路径:allocator、placement new、move / copy 与 move_if_noexcept
重分配是 vector 的核心实现路径。它发生在当前容量容纳不下新大小,或者某些容量调整请求需要换存储时。重分配路径要同时满足三个目标:分配足够的新原始存储,在新存储上构造等价元素,出现异常时保护旧容器状态。
常见实现会通过 allocator 取得原始存储,再通过 std::allocator_traits<Alloc>::construct 在目标地址构造元素。构造完成前,新存储只是一段候选状态。旧三指针仍然代表当前有效容器。只有所有必要对象都成功构造后,容器才销毁旧元素、释放旧存储,并提交新三指针。
// 教学简化代码,用于解释 reallocate 的阶段,省略增长策略和边界细节。
template<class T, class Alloc>
T* relocate_range(Alloc& alloc, T* old_begin, T* old_end, std::size_t new_capacity) {
T* new_begin = std::allocator_traits<Alloc>::allocate(alloc, new_capacity);
T* new_end = new_begin;
try {
for (T* p = old_begin; p != old_end; ++p, ++new_end) {
std::allocator_traits<Alloc>::construct(
alloc,
new_end,
std::move_if_noexcept(*p)
);
}
} catch (...) {
for (T* p = new_begin; p != new_end; ++p) {
std::allocator_traits<Alloc>::destroy(alloc, p);
}
std::allocator_traits<Alloc>::deallocate(alloc, new_begin, new_capacity);
throw;
}
return new_begin;
}
这段简化代码展示了两个边界。第一,allocate 只获得原始存储;对象生命周期从 construct 成功时开始。第二,新存储构造失败时,清理责任只覆盖已经构造成功的前缀,旧存储仍然完整存在,调用者可以保留旧状态。
std::move_if_noexcept 连接移动语义和强异常安全保证。它在元素的移动构造标记为 noexcept,或元素没有可用拷贝构造时返回右值引用;否则返回 const T&,让实现倾向走拷贝路径。cppreference 的 std::move_if_noexcept 页面 也说明它常用于 vector::resize 这类需要新存储并迁移元素的场景。
#include <type_traits>
#include <utility>
#include <vector>
struct RiskyMove {
RiskyMove() = default;
RiskyMove(const RiskyMove&) = default;
RiskyMove(RiskyMove&&) noexcept(false) {}
};
static_assert(std::is_copy_constructible_v<RiskyMove>);
static_assert(!std::is_nothrow_move_constructible_v<RiskyMove>);
对 RiskyMove 来说,重分配时走拷贝更容易保护旧状态。若移动构造在中途抛出,旧元素可能已经部分进入 moved-from 状态;若拷贝构造抛出,旧元素仍保持原状态。move_if_noexcept 的价值在于让容器根据元素类型的异常承诺选择更稳的迁移方式。
提交点是阅读 vector 源码时最值得抓住的位置。提交点之前,新存储只是临时资源,失败路径销毁新前缀并释放新块。提交点之后,旧存储开始被销毁并释放,容器三指针更新到新块。识别提交点,就能识别强异常安全保证的边界。
实际实现中,前缀、新元素、后缀的构造顺序可能因操作类型和优化策略而变化,但阶段关系稳定:先准备新状态,再提交,再回收旧状态。阅读 libstdc++ / libc++ 这类实现时,可以搜索 reserve、emplace_back、insert 周边的内部增长函数,然后沿着 allocator 分配、uninitialized 构造、guard 清理、指针提交这四类信号读。
10.5 iterator 失效、cache locality 与性能陷阱
vector 的 iterator 失效来自两个原因:存储地址更换,或同一存储内元素位置移动。地址更换时,所有指向旧块的 iterator、pointer 和 reference 全部失效。同一存储内插入或删除时,操作点之前的位置通常保持,操作点及其后的位置因元素移动而失效。
下面这组规则可以直接从三指针模型推出。只读操作不改变状态,位置稳定。reserve 和 shrink_to_fit 只有在容量改变时才让全部位置失效。尾部 push_back / emplace_back 若触发重分配,全部位置失效;若没有重分配,旧元素位置保持,但旧 end() 失效。insert 若没有重分配,插入点及其后的位置失效。erase 让删除位置及其后的位置失效。clear 让所有元素位置失效。
std::vector<int> values;
values.reserve(4);
values.push_back(1);
values.push_back(2);
values.push_back(3);
int* p0 = values.data();
auto it1 = values.begin() + 1;
values.push_back(4); // 容量足够,p0 仍指向同一块存储,it1 仍指向值 2
values.insert(values.begin(), 0); // 可能触发重分配;即使容量足够,it1 也不再代表原位置
这个例子展示了“地址稳定”和“逻辑位置稳定”的区别。尾部追加且容量足够时,旧元素地址保持;中间插入会移动后缀元素,原 iterator 表达的位置关系被更新后的序列打断。工程代码中保留下标有时比保留 iterator 更稳,因为下标可以在操作后重新计算 iterator。
std::vector<LogRecord> records = {
{1001, "start"},
{1002, "connect"},
{1003, "done"}
};
std::size_t index = 1;
records.insert(records.begin(), LogRecord{1000, "boot"});
LogRecord& original_connect = records[index + 1];
这段代码保存的是逻辑索引关系,插入后显式修正索引。它仍然要求开发者知道插入位置对索引的影响。若业务逻辑需要长期稳定引用元素,vector 通常要配合稳定 ID、外部索引表,或者换用节点式容器。
连续内存带来 cache locality。遍历 vector<int> 时,CPU 可以按连续地址预取后续元素,单个元素只占用值本身的存储。相比节点式链表,vector 遍历通常有更少的指针追踪、更少的分配元数据、更好的空间局部性。这个收益在大量顺序扫描、排序、二分查找、批量写出场景中明显。
性能陷阱也来自同一事实。中间插入和删除需要移动后缀;元素类型移动成本高时,线性移动会成为主要开销。频繁追加但不预估容量时,会产生多次重分配;每次重分配都要迁移现有元素。把 reserve 放在已知规模的构造阶段,能把多次增长合并成一次分配。
std::vector<LogRecord> build_records(std::size_t expected_count) {
std::vector<LogRecord> records;
records.reserve(expected_count);
for (std::size_t i = 0; i != expected_count; ++i) {
records.emplace_back(static_cast<long long>(i), "record");
}
return records;
}
这个构造路径适合“先收集、后读取”的日志场景。容量一次到位后,循环内大概率只构造新元素并移动 end。如果 expected_count 明显高估,额外容量会占用内存;如果明显低估,仍会发生增长。工程判断应使用数据规模、峰值内存和插入位置分布共同决定是否使用 vector。
10.6 vector<bool> Specialization Boundary and Mini Vector Checkpoint
std::vector<bool> 是 vector 章节必须单独划边界的特化。它可能用 bit-level 表示压缩布尔值,一个元素可以只占一位。这个空间优化改变了普通 std::vector<T> 的若干直觉:元素可能不以 bool 数组形式连续存储,operator[] 返回的通常是代理对象,单个 bit 的读写需要掩码操作。
标准库提供这个特化是历史设计结果。它在接口上接近 vector,但实现表示和元素引用模型不同。cppreference 的 std::vector<bool> 页面 说明其空间优化方式由实现定义,并列出了代理引用、非普通连续数组、并发修改粒度等边界。工程上,把它当作“动态 bitset 风格容器”更稳。
std::vector<bool> flags = {true, false, true};
auto ref = flags[0]; // 通常是代理对象,表示某个 word 中的一位
ref = false;
bool value = flags[1]; // 转换成普通 bool 值
这段代码的重点是 ref 的类型。对普通 std::vector<int> 来说,values[0] 返回 int&。对 std::vector<bool> 来说,flags[0] 通常返回类似 std::vector<bool>::reference 的代理值。代理对象记录所在 word 和 bit mask,赋值时修改对应 bit。它能模拟引用语义,但地址、并发粒度和某些算法要求都与普通元素引用不同。
选择 vector<bool> 时,先判断需求是“节省位级空间”还是“获得普通 bool 元素数组”。前者可以考虑 vector<bool>、std::bitset 或第三方 dynamic bitset;后者应优先使用 std::vector<unsigned char>、std::vector<std::uint8_t> 或自定义枚举字节存储。若需要把数据交给 C 接口按 bool* 处理,vector<bool> 通常无法满足这个地址模型。
mini vector 的检查点可以帮助读者把本章知识落到实现。一个教学版 mini_vector<T> 不需要覆盖完整标准接口,但必须具备五组不变量:三指针状态一致,allocator 负责原始存储,construct / destroy 负责对象生命周期,重分配先构造新状态再提交,修改操作返回或更新有效位置。
// 教学简化代码,只展示不变量和关键动作,省略 allocator propagation 等完整标准库细节。
template<class T, class Alloc = std::allocator<T>>
class mini_vector {
public:
mini_vector() = default;
~mini_vector() {
destroy_range(begin_, end_);
if (begin_) {
std::allocator_traits<Alloc>::deallocate(alloc_, begin_, capacity());
}
}
std::size_t size() const { return static_cast<std::size_t>(end_ - begin_); }
std::size_t capacity() const { return static_cast<std::size_t>(cap_ - begin_); }
T* data() { return begin_; }
const T* data() const { return begin_; }
template<class... Args>
T& emplace_back(Args&&... args) {
if (end_ == cap_) {
grow_for_one_more();
}
std::allocator_traits<Alloc>::construct(
alloc_,
end_,
std::forward<Args>(args)...
);
++end_;
return *(end_ - 1);
}
private:
void destroy_range(T* first, T* last) {
while (last != first) {
--last;
std::allocator_traits<Alloc>::destroy(alloc_, last);
}
}
void grow_for_one_more() {
std::size_t old_size = size();
std::size_t old_capacity = capacity();
std::size_t new_capacity = old_capacity == 0 ? 1 : old_capacity * 2;
T* new_begin = std::allocator_traits<Alloc>::allocate(alloc_, new_capacity);
T* new_end = new_begin;
try {
for (T* p = begin_; p != end_; ++p, ++new_end) {
std::allocator_traits<Alloc>::construct(
alloc_,
new_end,
std::move_if_noexcept(*p)
);
}
} catch (...) {
destroy_range(new_begin, new_end);
std::allocator_traits<Alloc>::deallocate(alloc_, new_begin, new_capacity);
throw;
}
destroy_range(begin_, end_);
if (begin_) {
std::allocator_traits<Alloc>::deallocate(alloc_, begin_, old_capacity);
}
begin_ = new_begin;
end_ = new_begin + old_size;
cap_ = new_begin + new_capacity;
}
Alloc alloc_{};
T* begin_ = nullptr;
T* end_ = nullptr;
T* cap_ = nullptr;
};
这份 mini_vector 的阅读重点有三个。第一,reserve 式增长只分配原始存储,元素通过 construct 进入生命周期。第二,增长失败时只清理新存储中已经构造的前缀,旧容器尚未提交新状态。第三,提交后旧元素被销毁,旧存储被释放,所有旧地址失效。真实标准库还要处理 allocator propagation、异常规格、范围插入、erase、swap、constexpr 支持和调试 iterator,但核心不变量仍围绕这三点展开。
本章对 vector 的最终判断顺序是:先看操作是否可能改变 capacity(),再看 [begin, end) 中哪些元素被构造、移动或销毁,然后判断 iterator / pointer / reference 是否跨过重分配或元素移动边界,最后根据访问模式选择容器。vector 适合顺序存储、随机访问、批量遍历和尾部增长;中间高频插入删除、长期稳定引用、位级布尔数组语义则需要更谨慎的容器选择。
最小自检任务
阅读下面代码,判断每个注释位置之后的 size、capacity、对象生命周期和迭代器状态。假设实现的增长策略在 reserve(3) 后容量正好为 3,且后续插入第 4 个元素时会发生重分配。
#include <string>
#include <vector>
struct Item {
std::string name;
};
int main() {
std::vector<Item> items;
items.reserve(3); // A
items.emplace_back(Item{"one"}); // B
items.emplace_back(Item{"two"}); // C
auto first = items.begin();
auto second = items.begin() + 1;
items.insert(items.begin() + 1, Item{"middle"}); // D
items.emplace_back(Item{"four"}); // E
items.erase(items.begin()); // F
items.clear(); // G
}
答案要点
A 点之后,size() 为 0,capacity() 至少为 3;只有原始存储,没有有效 Item 元素。B 点之后,索引 0 位置有一个有效 Item,size() 为 1。C 点之后,索引 0 和 1 都是有效对象,size() 为 2,容量仍为 3。
first 和 second 在 D 点之前分别指向原存储中的第 0 和第 1 个元素。D 点插入时容量足够,存储块保持,但插入点及其后的元素位置发生移动;first 仍指向第 0 个元素,second 失效,因为它位于插入点及其后范围。D 点之后 size() 为 3,容量仍为 3。
E 点追加第 4 个元素时容量不足,容器分配新存储并迁移 3 个旧元素,再构造新元素并提交新三指针。E 点之后 size() 为 4,容量大于 3,所有旧 iterator、pointer 和 reference 失效,包括 first 和 second。
F 点删除当前第 0 个元素,被删除元素生命周期结束,后缀元素前移,size() 变为 3;删除位置及其后的 iterator 失效。G 点 clear() 销毁全部有效元素,size() 变为 0,容量通常保持;先前指向元素的位置都需要重新获取后再使用。
本章知识点总结
- 连续存储:普通
std::vector<T>把有效元素放在连续内存中,因此支持常数时间下标访问和data()指针交付。 - 三指针状态:常见实现可抽象为
begin、end、cap三段边界,分别表示存储起点、已构造元素末尾和分配存储末尾。 - 有效对象:
size()只统计已经构造的元素,capacity()中多出的槽位只是原始存储。 - reserve 语义:
reserve预留容量,容量增长时迁移旧元素并使旧地址失效。 - resize 语义:
resize改变有效元素数量,扩大时构造新元素,缩小时销毁尾部元素。 - 收缩请求:
shrink_to_fit表达释放多余容量的请求,工程代码应按容量是否实际改变判断地址稳定性。 - 尾部追加:
push_back和emplace_back在容量足够时只构造末尾元素并移动end。 - 中间修改:
insert和erase会移动后缀元素,失效范围从操作位置开始向后扩展。 - 重分配路径:重分配先分配新原始存储并构造新状态,成功后提交三指针,再销毁旧元素和释放旧存储。
- 异常安全:
move_if_noexcept帮助容器在移动和拷贝之间选择更利于保护旧状态的迁移方式。 - 地址失效:容量改变时所有旧 iterator、pointer 和 reference 失效,同一存储内的位置移动也会使受影响位置失效。
- 局部性收益:连续内存让顺序遍历、随机访问、排序和批量交付具备良好 cache locality。
- 性能边界:中间高频插入删除和重元素迁移会放大线性移动成本,已知规模时应优先预留容量。
- 布尔特化:
vector<bool>通常使用 bit-level 表示和代理引用,选型时应把它视为动态位集合风格容器。 - 实现检查点:mini vector 的核心是不变量、allocator 原始存储、对象生命周期、提交点和失效规则。