C++,vector:动态数组的原理、使用与极致优化

文章目录
- 引言
- 一、vector 的核心原理
- 1. 底层数据结构
- 1.1 内存布局的三指针模型
- 1.2 内存布局示意图
- 2. 动态扩容机制
- 2.1 动态扩容过程示例
- 3. 关键结论
- 4. 代码验证内存布局
- 5. 总结
- 二、vector 的使用方法
- 1. 基本操作
- 2. 迭代器与范围遍历
- 三、vector 的注意事项
- 1. 迭代器失效
- 2. 性能陷阱
- 3. 特殊类型处理
- 四、vector 的性能优化技巧
- 1. 预分配内存(reserve)
- 2. 使用 emplace_back 替代 push_back
- 3. 利用移动语义
- 4. 批量插入优化
- 5. 避免不必要的 resize
- 6. 释放多余内存(shrink_to_fit)
- 五、vector 与其他容器的对比
- 六、结语
引言
std::vector 是 C++ 标准模板库(STL)中最重要且高频使用的容器之一。它结合了数组的高效随机访问和动态内存管理的灵活性,是处理动态数据集合的首选工具。本文将全面剖析 vector 的实现原理、核心操作、常见陷阱及性能优化技巧,助您彻底掌握这一核心容器。
一、vector 的核心原理
1. 底层数据结构
vector 的底层是一个连续内存块,类似于传统数组,但支持动态扩容。其核心由三个指针管理:
- _start:指向容器首元素
- _finish:指向最后一个元素的下一个位置(即 size() 的位置)
- _end_of_storage:指向分配内存的末尾(即 capacity() 的位置)
std::vector 的核心特性是动态数组,其底层通过连续的物理内存存储元素。理解它的内存布局和指针管理机制,是掌握 vector 性能优化的关键。以下通过示意图和分步说明,详细解析其内存分配原理。
1.1 内存布局的三指针模型
- _start
- 指向动态分配内存块的起始地址(首元素的位置)。
- _finish
- 指向最后一个有效元素的下一个位置(即 size() 的位置)。
- 若容器为空,则 _start == _finish。
- _end_of_storage
- 指向当前分配内存块的末尾(即 capacity() 的位置)。
- 从 _finish 到 _end_of_storage 的空间为预留内存,用于后续插入操作。
1.2 内存布局示意图
假设一个 vector 已插入 3 个元素,并预留了 5 个元素的容量(size() = 3, capacity() = 5):
内存地址低 → 高
┌─────┬─────┬─────┬─────┬─────┬───────────────┐
│ 1 │ 2 │ 3 │ ? │ ? │ │
└─────┴─────┴─────┴─────┴─────┴───────────────┘
↑ ↑ ↑
_start _finish _end_of_storage
- 有效元素区间:[_start, _finish)(存储 3 个元素)。
- 预留空间:[_finish, _end_of_storage)(剩余 2 个元素位置)。
- ? 表示未初始化的内存:这些位置可能包含垃圾值,需通过 push_back 或 emplace_back 写入数据。
2. 动态扩容机制
当 size() == capacity() 时插入新元素会触发扩容:
- 分配新内存(通常为原容量的 1.5 或 2 倍,依编译器实现而定)。
- 将旧元素拷贝或移动到新内存。
- 释放旧内存,更新指针。
均摊时间复杂度:push_back 的均摊时间复杂度为 O(1),而非每次扩容 O(n)。
2.1 动态扩容过程示例
假设初始容量为 2,依次插入元素 A, B, C,观察内存如何变化:
- 初始状态(插入 A, B):
size() = 2, capacity() = 2
┌───┬───┐
│ A │ B │
└───┴───┘
↑ ↑ ↑
_start _finish _end_of_storage
- 插入第三个元素 C:
- 触发扩容(假设新容量为 2 倍,即 4)。
- 分配新内存块,拷贝旧元素,释放旧内存:
Step 1: 分配新内存(容量 4)
┌───┬───┬───┬───┐
│ │ │ │ │
└───┴───┴───┴───┘ Step 2: 拷贝旧元素 `A`, `B`
┌───┬───┬───┬───┐
│ A │ B │ │ │
└───┴───┴───┴───┘ Step 3: 插入新元素 `C`
┌───┬───┬───┬───┐
│ A │ B │ C │ │
└───┴───┴───┴───┘
↑ ↑ ↑
_start _finish _end_of_storage
- 最终状态:
- size() = 3, capacity() = 4,预留 1 个位置。
3. 关键结论
- 连续内存优势
- 支持 O(1) 时间的随机访问(通过指针算术运算,如 _start + index)。
- 对 CPU 缓存友好(局部性原理)。
- 扩容代价
- 扩容需重新分配内存、拷贝元素、释放旧内存,单次时间复杂度为 O(n)。
- 均摊时间复杂度为 O(1)(例如容量按 2 倍增长时,总拷贝次数为 1 + 2 + 4 + 8 + … ≈ 2n)。
- 预留空间的策略
- 合理使用 reserve() 预分配空间,避免频繁扩容。
- 扩容因子(如 1.5 或 2 倍)由编译器实现决定,通常选择 1.5 倍以减少内存浪费(详见 GCC 和 Clang 的实现)。
4. 代码验证内存布局
通过直接访问 vector 的底层指针(需谨慎,仅用于学习):
#include <vector>
#include <iostream> int main() { std::vector<int> v = {1, 2, 3}; v.reserve(5); // 强制预留容量为5 // 获取指针(注意:此方法依赖具体实现,非标准!) int* start = v.data(); int* finish = start + v.size(); int* end_of_storage = start + v.capacity(); std::cout << "start: " << (void*)start << "\nfinish: " << (void*)finish << "\nend_of_storage: " << (void*)end_of_storage << "\n元素分布:"; for (int* p = start; p != finish; ++p) { std::cout << *p << " "; } std::cout << "\n预留空间大小: " << end_of_storage - finish << std::endl;
}
输出示例(具体地址值随运行环境变化):
start: 0x55a1a3b6eeb0
finish: 0x55a1a3b6eebc
end_of_storage: 0x55a1a3b6eec8
元素分布:1 2 3
预留空间大小: 2
5. 总结
vector 的连续内存布局是其高性能的基石,但也带来扩容时的潜在开销。理解 _start、_finish 和 _end_of_storage 的协作机制,能够帮助开发者:
- 预判迭代器失效场景(如扩容后所有指针失效)。
- 优化内存分配策略(如提前 reserve 避免反复扩容)。
- 避免误用(如直接操作未初始化的预留空间)。
掌握这些底层原理后,可以更自信地编写高效、健壮的 C++ 代码。
二、vector 的使用方法
1. 基本操作
#include <vector> // 初始化
std::vector<int> v1; // 空容器
std::vector<int> v2(5, 10); // 5 个 10
std::vector<int> v3 = {1, 2, 3}; // 列表初始化 // 访问元素
int a = v3[0]; // 随机访问(不检查越界)
int b = v3.at(1); // 带越界检查(越界抛出 std::out_of_range)
int c = v3.front(); // 首元素
int d = v3.back(); // 尾元素 // 添加/删除元素
v3.push_back(4); // 尾部插入
v3.pop_back(); // 尾部删除
v3.insert(v3.begin(), 0); // 在指定位置插入
v3.erase(v3.begin() + 1); // 删除指定位置元素 // 容量管理
v3.reserve(100); // 预分配至少 100 元素空间(不改变 size)
v3.resize(10); // 调整 size 为 10(新增元素默认构造)
2. 迭代器与范围遍历
// 正向迭代器
for (auto it = v3.begin(); it != v3.end(); ++it) { std::cout << *it << " ";
} // 反向迭代器
for (auto rit = v3.rbegin(); rit != v3.rend(); ++rit) { std::cout << *rit << " ";
} // 基于范围的 for 循环(C++11+)
for (int x : v3) { std::cout << x << " ";
}
三、vector 的注意事项
1. 迭代器失效
以下操作可能导致迭代器失效(野指针):
- 插入元素:若触发扩容,所有迭代器、指针、引用失效。
- 删除元素:被删元素后的迭代器、指针、引用失效。
错误示例:
std::vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // 可能触发扩容,导致 it 失效
std::cout << *it; // 未定义行为!
2. 性能陷阱
- 频繁扩容:未预分配足够容量时,反复扩容会导致大量内存拷贝。
- 头部插入:insert(v.begin(), x) 的时间复杂度为 O(n),效率极低。
- 不必要的拷贝:未使用移动语义时,对象拷贝可能成为性能瓶颈。
3. 特殊类型处理
- vector bool 的特化:
vector bool 并非存储真正的 bool 类型,而是以位压缩存储,导致其行为与其他 vector 不同(如无法获取元素地址)。
四、vector 的性能优化技巧
1. 预分配内存(reserve)
提前分配足够容量,避免多次扩容:
std::vector<int> data;
data.reserve(1000); // 预分配 1000 元素空间
for (int i = 0; i < 1000; ++i) { data.push_back(i); // 无扩容开销
}
2. 使用 emplace_back 替代 push_back
emplace_back 直接在容器内构造对象,避免临时对象的拷贝/移动:
struct Point { Point(int x, int y) : x(x), y(y) {} int x, y;
}; std::vector<Point> points;
points.emplace_back(1, 2); // 直接构造,无需创建临时 Point 对象
3. 利用移动语义
对于临时对象或可移动对象,使用 std::move 减少拷贝:
std::vector<std::string> strs;
std::string s = "Hello";
strs.push_back(std::move(s)); // 移动而非拷贝,s 变为空
4. 批量插入优化
使用范围插入或初始化列表减少多次函数调用开销:
std::vector<int> v;
v.insert(v.end(), {1, 2, 3, 4, 5}); // 批量插入 // 或使用迭代器范围
std::array<int, 5> arr = {6, 7, 8, 9, 10};
v.insert(v.end(), arr.begin(), arr.end());
5. 避免不必要的 resize
- resize 会构造或销毁对象,若仅需扩容,优先使用 reserve。
6. 释放多余内存(shrink_to_fit)
在元素大量删除后,收缩内存至合适大小:
std::vector<int> v(1000);
v.resize(10);
v.shrink_to_fit(); // capacity() 可能降至接近 10
五、vector 与其他容器的对比

六、结语
std::vector 凭借其高效的随机访问和灵活的动态扩容,成为 C++ 开发中不可或缺的容器。然而,其性能优势的发挥依赖于正确的使用方式:
- 预分配内存减少扩容开销
- 优先选择移动语义避免深拷贝
- 警惕迭代器失效场景
- 合理选择容器类型(如高频头部操作时改用 deque)
掌握这些技巧后,vector 将成为您处理动态数据集合的利器。
扩展阅读
- 《Effective STL》Item 13-17:关于 vector 的最佳实践
- C++ 标准文档:vector 的复杂度保证
- 源码分析:GCC/libstdc++ 或 LLVM/libcxx 的 vector 实现
相关文章:
C++,vector:动态数组的原理、使用与极致优化
文章目录 引言一、vector 的核心原理1. 底层数据结构1.1 内存布局的三指针模型1.2 内存布局示意图 2. 动态扩容机制2.1 动态扩容过程示例 3. 关键结论4. 代码验证内存布局5. 总结 二、vector 的使用方法1. 基本操作2. 迭代器与范围遍历 三、vector 的注意事项1. 迭代器失效2. 性…...
bootstrap.yml文件未自动加载问题解决方案
在添加bootstrap.yml文件后,程序未自动扫描到,即图标是这样的: 查了一些资料,是缺少bootstrap相关依赖,虽然已经添加了spring-cloud-context依赖,但是这个依赖并未引入bootstrap依赖,可能是版本问题,需要手动引入 <dependency><groupId>org.springframework.cloud&…...
54【ip+端口+根目录通信】
上节课讲到,根目录起到定位作用,比如我们搭建一个php网站后,注册系统是由根目录的register.php文件执行,那么我们给这个根目录绑定域名https://127.0.0.1,当我们浏览器访问https://127.0.0.1/register.php时࿰…...
实现数组的扁平化
文章目录 1 实现数组的扁平化1.1 递归1.2 reduce1.3 扩展运算符1.4 split和toString1.5 flat1.6 正则表达式和JSON 1 实现数组的扁平化 1.1 递归 通过循环递归的方式,遍历数组的每一项,如果该项还是一个数组,那么就继续递归遍历,…...
blender 相机参数
目录 设置相机参数: 3. 设置相机参数示例 4. 相机透视与正交 5. 额外的高级设置 设置相机参数: 设置渲染器: 外参转换函数 转换测试代码: 获取blender渲染外参: 设置相机参数: 3. 设置相机参数示…...
物联网 STM32【源代码形式-ESP8266透传】连接OneNet IOT从云产品开发到底层MQTT实现,APP控制 【保姆级零基础搭建】
一、MQTT介绍 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)是一种基于发布/订阅模式的轻量级通讯协议,构建于TCP/IP协议之上。它最初由IBM在1999年发布,主要用于在硬件性能受限和网络状况不佳的情…...
软考高项笔记 信息技术及其发展
信息技术及其发展 ❝ 信息系统项目管理师第二章第一节 1. 网络标准协议的定义 网络协议是为计算机网络中进行数据交换而建立的规则、标准或约定的集合。网络协议由三个要素组成,分别是语义、语法和时序。 语义:解释控制信息每个部分的含义,它…...
计算机网络 性能指标相关
目录 吞吐量 时延 时延带宽积 往返时延RTT 利用率 吞吐量 时延 时延带宽积 往返时延RTT 利用率...
【初/高中生讲机器学习】0. 本专栏 “食用” 指南——写在一周年之际⭐
创建时间:2025-01-27 首发时间:2025-01-29 最后编辑时间:2025-01-29 作者:Geeker_LStar 你好呀~这里是 Geeker_LStar 的人工智能学习专栏,很高兴遇见你~ 我是 Geeker_LStar,一名高一学生,热爱计…...
[SAP ABAP] 性能优化
1.数据库编程OPEN SQL方面优化 1.避免使用SELECT *,只查询需要的字段即可 尽量使用SELECT f1 f2 ... (具体字段) 来代替 SELECT * 写法 2. 如果确定只查询一条数据时,使用 SELECT SINGLE... 或者是 SELECT ...UP TO 1 ROWS ... 使用语法 UP TO n ROWS 来…...
软件测试02----用例设计方法
今天目标 1.能对穷举场景设计测试点 2.能对限定边界规则设计测试点 3.能对多条件依赖关系进行设计测试点 4.能对项目业务进行设计测试点 一、解决穷举场景 重点:使用等价类划分法 1.1等价类划分法 重点:有效等价和单个无效等价各取1个即可。 步骤&#…...
Nginx 日志分析与监控
一、引言 在当今互联网时代,Web 服务的稳定运行和高效性能是至关重要的。Nginx 作为一款高性能的 HTTP 和反向代理服务器,以其出色的稳定性、高效性和丰富的功能,被广泛应用于各类 Web 项目中,成为了 Web 服务架构中不可或缺的一…...
冷启动+强化学习:DeepSeek-R1 的原理详解——无需监督数据的推理能力进化之路
本文基于 DeepSeek 官方论文进行分析,论文地址为:https://github.com/deepseek-ai/DeepSeek-R1/blob/main/DeepSeek_R1.pdf 有不足之处欢迎评论区交流 原文翻译 在阅读和理解一篇复杂的技术论文时,逐字翻译是一个重要的步骤。它不仅能帮助我们准确把握作者的原意,还能为后续…...
笔试-二进制
应用题 将符合区间[l,r]内的十进制整数转换为二进制表示,请问不包含“101”的整数个数是多少? 实现 l int(input("请输入下限l,其值大于等于1:")) r int(input("请输入上限r,其值大于等于l&#x…...
014-STM32单片机实现矩阵薄膜键盘设计
1.功能说明 本设计主要是利用STM32驱动矩阵薄膜键盘,当按下按键后OLED显示屏上会对应显示当前的按键键值,可以将此设计扩展做成电子秤、超市收银机、计算器等需要多个按键操作的单片机应用。 2.硬件接线 模块管脚STM32单片机管脚矩阵键盘行1PA0矩阵键盘…...
Spring Boot 2 快速教程:WebFlux处理流程(五)
WebFlux请求处理流程 下面是spring mvc的请求处理流程 具体步骤: 第一步:发起请求到前端控制器(DispatcherServlet) 第二步:前端控制器请求HandlerMapping查找 Handler (可以根据xml配置、注解进行查找) 匹配条件包括…...
unity学习25:用 transform 进行旋转和移动,简单的太阳地球月亮模型,以及父子级关系
目录 备注内容 1游戏物体的父子级关系 1.1 父子物体 1.2 坐标关系 1.3 父子物体实际是用 每个gameobject的tranform来关联的 2 获取gameObject的静态数据 2.1 具体命令 2.2 具体代码 2.3 输出结果 3 获取gameObject 的方向 3.1 游戏里默认的3个方向 3.2 获取方向代…...
CH340G上传程序到ESP8266-01(S)模块
文章目录 概要ESP8266模块外形尺寸模块原理图模块引脚功能 CH340G模块外形及其引脚模块引脚功能USB TO TTL引脚 程序上传接线Arduino IDE 安装ESP8266开发板Arduino IDE 开发板上传失败上传成功 正常工作 概要 使用USB TO TTL(CH340G)将Arduino将程序上传…...
Python量化交易助手:xtquant的安装与应用
Python量化交易助手:xtquant的安装与应用 技术背景和应用场景 在量化交易领域,Python因其强大的库支持和灵活性成为了许多开发者的首选语言。其中,xtquant 是迅投官方开发的一个Python包,专门用于与miniqmt通信,实现…...
上海路网道路 水系铁路绿色住宅地工业用地面图层shp格式arcgis无偏移坐标2023年
标题和描述中提到的资源是关于2023年上海市地理信息数据的集合,主要包含道路、水系、铁路、绿色住宅区以及工业用地的图层数据,这些数据以Shapefile(shp)格式存储,并且是适用于ArcGIS软件的无偏移坐标系统。这个压缩包…...
touch 命令与动态链接器漏洞分析:基于库文件劫持的提权攻击
touch 命令在 Linux 系统中,通常用于修改文件的访问时间(atime)和修改时间(mtime),或者在文件不存在时创建一个空文件。若 touch 命令被赋予 SUID(Set User ID)权限,它将以文件所有者(通常是 root )的身份执行。这为潜在的提权攻击提供了切入点,攻击者可利用此特性…...
DeepSeekMoE:迈向混合专家语言模型的终极专业化
一、结论写在前面 论文提出了MoE语言模型的DeepSeekMoE架构,目的是实现终极的专家专业化(expert specialization)。通过细粒度的专家分割和共享专家隔离,DeepSeekMoE相比主流的MoE架构实现了显著更高的专家专业化和性能。从较小的2B参数规模开始&#x…...
扩散模型(二)
相关阅读:扩散模型(一) Parameterization of L t L_t Lt for Training Loss 回想一下,我们需要训练一个神经网络来近似反向扩散过程中的条件概率分布,即, p θ ( x t − 1 ∣ x t ) N ( x t − 1 ; μ θ ( x t…...
【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.18 对象数组:在NumPy中存储Python对象
2.18 对象数组:在NumPy中存储Python对象 目录 #mermaid-svg-shERrGOBuM2rBzeB {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-shERrGOBuM2rBzeB .error-icon{fill:#552222;}#mermaid-svg-shERrGOBuM2rB…...
LabVIEW双光子成像系统:自主创新,精准成像,赋能科研
双光子成像系统:自主创新,精准成像,赋能科研 第一部分:概述 双光子成像利用两个低能量光子同时激发荧光分子,具有深层穿透、高分辨率、低光损伤等优势。它能实现活体深层组织的成像,支持实时动态观察&…...
bagging框架
bagging 1 bagging介绍 Bagging的全称是Bootstrap Aggregating,其思想是通过将许多相互独立的学习器的结果进行结合,从而提高整体学习器的泛化能力 bagging框架流程:首先,它从原始数据集中使用有放回的随机采样方式抽取多个子集…...
《机器学习数学基础》补充资料:仿射变换
本文是对《机器学习数学基础》 第 2 章 2.2.4 节齐次坐标系的内容拓展。 1. 名称的来源 仿射,是英文单词 affine 的中文翻译。 单词 affine,读音:[ə’faɪn]。来自于英语 affinity。英语词根 fin 来自于拉丁语 finis,表示“边…...
冲刺一区!挑战7天完成一篇趋势性分析GBD DAY1-7
Day1. 公开数据库的挖掘太火热了,其中GBD数据库的挖掘又十分的火爆.那我就来挑战一篇GBD、一篇关于趋势性分析的GBD! GBD数据库挖掘是目前的四大刊常客,经常出现在顶级期刊上面。这个数据库亮点就是:可视化,统计学简单、而数据可…...
ZK-ALU-在有限域上实现左移
先看在实数域上实现左移, 再看在有限域上的实现 左移-整数 计算机中的左移计算(<< 操作)通常由处理器的硬件电路直接支持,因此效率非常高。在编程语言中,左移操作可以通过位移运算符(例如 C/C 中的 <<&a…...
掌握API和控制点(从Java到JNI接口)_36 JNI开发与NDK 04
4、 *.so的入口函数:JNI_OnLoad() VM (virtual machine)的角色 Java代码在VM上执行。在执行Java代码的过程中,如果Java需要与本地代码(*.so)沟通时, VM就会把*.so視为插件<Tn>而加载到VM里。然后让Java函数呼叫到这插件<Tn>里的…...
