动态数组与vector¶
动态数组是C++编程中最常用的数据结构之一。理解如何手动管理动态数组,以及如何使用标准库提供的 vector 容器,
是掌握C++内存管理和标准库使用的重要环节。
从 C 风格数组说起¶
C风格数组的局限性¶
在C++中,传统数组(C风格数组)存在诸多限制:
C风格数组的问题
- 大小固定:数组大小必须在编译时确定,无法根据运行时需求调整。
- 无越界检查:访问超出数组范围时编译不会报错,可能导致内存错误。
- 不能直接赋值:数组之间不能直接复制或赋值。
- 退化指针:数组名在表达式中会自动退化为指针,丢失大小信息。
对象数组的封装需求¶
当需要管理对象数组,尤其是大小需要在运行时确定或调整的动态对象数组时,手动封装是解决上述问题的一种方式。
动态对象数组的封装目标
- 在运行时确定数组大小。
- 自动管理内存的分配和释放。
- 提供安全的元素访问(带越界检查)。
- 提供简洁自然的接口。
- 支持动态调整大小。
动态数组的手动封装¶
元素类的设计¶
Point 是动态数组中将要存放的元素类型,其结构与之前的例子基本一致.
增加的复制构造函数和=运算符重载主要是为了实现该类对象的复制拷贝的功能。
数组元素类:Point
封装类的设计¶
下面是 ArrayOfPoints 类的完整实现,展示了如何封装动态对象数组,并增加了改变数组大小的功能。
动态数组封装类:ArrayOfPoints
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
ArrayOfPoints 改变大小操作总结¶
ArrayOfPoints 的动态调整操作
| 操作 | 函数 | 说明 | 对容量影响 |
|---|---|---|---|
| 改变大小 | resize(n) |
将大小改为 n,新元素默认构造 | capacity = n |
| 添加元素 | push_back(p) |
在末尾添加元素,自动扩容 | capacity 不足时翻倍 |
| 删除末尾 | pop_back() |
删除末尾元素 | capacity 不变 |
| 预留容量 | reserve(n) |
预分配容量,避免多次扩容 | capacity = max(capacity, n) |
封装类的使用¶
ArrayOfPoints 的使用样例
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | |
封装设计的优点¶
ArrayOfPoints 的优点
- 自动内存管理:构造函数分配内存,析构函数释放内存,无需用户手动
delete。 - 运行时确定大小:数组大小通过构造函数参数传入,支持动态确定。
- 越界检查:
element()函数使用assert检查下标合法性,防止内存越界。 - 自然接口:重载
operator[]使得访问方式与原生数组一致。 - 常版本支持:提供
const版本的访问函数,支持常对象的使用。 - 支持动态扩展:数组大小可以根据需要动态增长。
封装的局限性¶
手动封装的不足
- 只能管理一种类型:
ArrayOfPoints只适用于Point类型,无法通用。为适配不同类型可能需要编写大量相似代码。 - 复制问题:必须实现深拷贝(见前一节“浅拷贝与深拷贝”)。
- 不支持迭代器:无法使用标准库算法。
- 功能有限:缺少插入、删除、排序等常用操作。
- 实现较为复杂:动态控制逻辑的编写需要一定经验。
标准库容器:vector¶
vector 简介¶
vector 是 C++ 标准模板库(STL)中最常用的容器之一,它封装了动态数组,提供了安全、高效、通用的数组管理功能。
vector 的特点
- 通用性:可以存储任何类型的元素(通过模板实现)。
- 动态扩展:可以自动增长容量,无需手动管理内存。
- 安全访问:提供
at()方法进行越界检查。 - 丰富的接口:支持迭代器、插入、删除、排序等操作。
- 内存管理:自动管理内存分配和释放。
vector 的基本使用¶
vector 的基本操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
vector 的常用操作¶
vector 常用成员函数
| 操作 | 函数 | 说明 |
|---|---|---|
| 元素访问 | at(idx) |
带越界检查的访问 |
operator[](idx) |
下标访问(无越界检查) | |
front() / back() |
访问首/尾元素 | |
data() |
获取底层数组指针 | |
| 大小 | size() |
当前元素个数 |
capacity() |
当前容量 | |
empty() |
是否为空 | |
resize(n) |
调整大小 | |
| 修改 | push_back(val) |
在末尾添加元素 |
pop_back() |
删除末尾元素 | |
insert(pos, val) |
在指定位置插入 | |
erase(pos) |
删除指定位置元素 | |
clear() |
清空所有元素 | |
reserve(n) |
预留容量 | |
| 迭代器 | begin() / end() |
获取迭代器 |
rbegin() / rend() |
获取反向迭代器 |
元素访问方式对比¶
三种访问方式的比较
vector 的内存管理¶
vector 自动管理内存,当元素数量超过容量时,会自动重新分配更大的内存空间,并将原有元素移动到新空间。
vector 的内存自动扩展
典型的运行结果(容量增长策略因编译器而异):
Initial size: 0, capacity: 0
After push 0: size = 1, capacity = 1
After push 1: size = 2, capacity = 2
After push 2: size = 3, capacity = 4
After push 3: size = 4, capacity = 4
After push 4: size = 5, capacity = 8
After push 5: size = 6, capacity = 8
After push 6: size = 7, capacity = 8
After push 7: size = 8, capacity = 8
After push 8: size = 9, capacity = 16
After push 9: size = 10, capacity = 16
性能建议
- 如果预先知道元素数量,使用
reserve()预留容量,避免多次重新分配。 - 使用
reserve()而不是resize()来预分配空间(reserve只分配空间,不构造元素)。
手动封装 vs vector 的对比¶
对比总结
| 对比项 | 手动封装 (ArrayOfPoints) | vector |
|---|---|---|
| 通用性 | 只能管理特定类型 | 模板化,可管理任何类型 |
| 动态扩展 | 不支持 | 自动扩展容量 |
| 越界检查 | assert(调试模式) | at() 抛出异常 |
| 内存管理 | 手动 new/delete | 自动管理 |
| 遍历方式 | 仅下标 | 下标、迭代器、范围for |
| STL算法支持 | 不支持 | 完全支持 |
| 拷贝/赋值 | 需要自定义深拷贝 | 自动处理(值语义) |
| 改变大小 | resize(n)(capacity = n) |
resize(n)(capacity 可能不变或变化) |
| 添加元素 | push_back(p)(扩容翻倍) |
push_back(p)(扩容翻倍) |
| 删除末尾 | pop_back()(capacity 不变) |
pop_back()(capacity 不变) |
| 预留容量 | reserve(n) |
reserve(n) |
| 收缩容量 | 不支持 | shrink_to_fit()(C++11) |
| 容量策略 | 手动实现翻倍扩容 | 标准库实现,通常翻倍 |
| 编写成本 | 高 | 中等 |
使用建议
- 优先使用
vector:在绝大多数场景下,应优先使用标准库的vector,它更安全、更高效、功能更丰富。 - 理解封装原理:学习手动封装动态数组有助于理解
vector的内部工作原理,是深入学习 C++ 的重要基础。 - 特殊场景可自定义:当有特殊需求(如特定的内存分配策略、轻量级容器等)时,才考虑手动实现。
小结¶
-
动态数组手动封装是理解内存管理的重要练习,但实际开发中更推荐使用标准库容器。
-
vector是 STL 中最常用的容器,提供了动态数组的完整实现: - 自动内存管理(构造/析构自动分配和释放)。
- 动态扩展(自动调整容量)。
- 安全访问(
at()带越界检查)。 -
丰富的接口(迭代器、插入、删除等)。
-
vector与数组的对比: vector比 C 风格数组更安全、更灵活。-
vector比手动封装的动态数组更通用、功能更丰富。 -
性能考虑:
- 使用
reserve()预分配容量,减少重新分配的开销。 -
operator[]比at()更快(无越界检查开销)。 -
最佳实践:优先使用
vector,理解其原理,只在特殊需求下才手动实现动态数组。