引入
本文深入剖析 C++ 中的vector
容器,从其本质、使用、迭代器失效、底层实现到实际应用,进行了全面且细致的讲解。vector
作为动态数组,具备连续存储、动态扩容等特性,在使用上有多种构造方式、丰富的操作接口,但其迭代器在某些操作下会失效,需要开发者谨慎处理。文章详细阐述了vector
的模拟实现,揭示了如memcpy
拷贝自定义类型的陷阱,以及动态二维数组的相关知识,并结合 OJ 实战,展示了vector
在解决经典问题中的应用。此外,还指出常见误区并给出最佳实践建议,帮助读者深入理解并熟练运用vector
。
一、vector 的本质:会 “长大” 的数组
1.1 为什么需要 vector?
想象你要做一个班级成绩管理系统,一开始不知道有多少学生,用普通数组的话:
int scores[100]; // 假设最多100人,不够用就崩溃
问题:静态数组大小固定,浪费空间或不够用。
vector 解决方案:动态数组,自动管理内存,想存多少存多少,还能像数组一样快速访问。
1.2 vector 的三个核心属性(必懂!)
- start:指向内存起始位置(像数组首地址)
- finish:指向最后一个有效元素的下一个位置(类似数组
end
) - end_of_storage:指向内存空间的末尾(总容量边界)
举例:
vector<int> v = {1,2,3};
// start指向1,finish指向4(3的下一个位置),end_of_storage可能指向6(假设初始容量4,预留空间)
1.3 与其他容器的区别(选对工具很重要)
容器 | 内存布局 | 随机访问 | 尾部操作 | 中间插入 | 适用场景 |
---|---|---|---|---|---|
array | 静态连续 | O(1) | 低效 | 不支持 | 已知固定大小的数据 |
vector | 动态连续 | O(1) | O(1) | O(n) | 尾部频繁操作 + 随机访问 |
list | 动态链表 | O(n) | O(1) | O(1) | 频繁中间插入 / 删除 |
deque | 分段连续 | O(1) | O(1) | O(n) | 双端频繁操作(如栈 / 队列) |
二、vector 的使用:从定义到操作全解析
2.1 构造函数:5 种初始化方式(附代码示例)
构造函数列表
序号 | 函数形式 | 功能说明 |
---|---|---|
1 | vector() | 无参构造 |
2 | vector(size_type n, const value_type& val = value_type()) | 构造并初始化 n 个 val |
3 | vector(const vector& x) | 拷贝构造 |
4 | vector(InputIterator first, InputIterator last) | 使用迭代器进行初始化构造 |
各构造函数详细信息
1. vector();
(无参构造)
- 参数:无。
- 详细说明:此构造函数用于创建一个空的
std::vector
对象,容器中没有任何元素,容量为 0。适用于需要一个初始为空的容器,后续再动态添加元素的场景。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v1;
std::cout << "v1 的大小: " << v1.size() << std::endl;
return 0;
}
- 示例讲解:在这个示例中,首先包含了
<vector>
和<iostream>
头文件,前者用于使用std::vector
容器,后者用于输入输出操作。在main
函数里,通过std::vector<int> v1;
调用无参构造函数创建了一个空的std::vector<int>
对象v1
。接着使用v1.size()
获取v1
的元素数量,由于它是通过无参构造创建的,所以元素数量为 0,并将结果输出。
2. vector(size_type n, const value_type& val = value_type());
- 参数:
n
为元素个数,val
为每个元素的初始值(默认值为value_type()
)。 - 详细说明:构造一个
std::vector
,包含n
个元素,每个元素都是val
的副本。若n = 0
,容器为空。适用于初始化固定数量且值相同的元素。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v2(5, 10); // 5 个元素,每个元素为 10
std::cout << "v2 的大小: " << v2.size() << std::endl;
for (int i : v2) {
std::cout << i << " ";
}
return 0;
}
- 示例讲解:此示例同样包含了必要的头文件。在
main
函数中,使用std::vector<int> v2(5, 10);
调用带参数的构造函数,创建了一个包含 5 个元素,每个元素值都为 10 的std::vector<int>
对象v2
。通过v2.size()
输出v2
的元素数量,结果为 5。然后使用范围for
循环遍历v2
中的每个元素并输出,因此会依次输出 5 个 10。
3. vector(const vector& x);
(拷贝构造)
- 参数:
x
为要拷贝的vector
对象。 - 详细说明:通过拷贝另一个
vector
对象x
创建新vector
,新容器与x
元素、大小、容量完全相同。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v3(3, 5); // v3 有 3 个元素,值为 5
std::vector<int> v4(v3); // 拷贝 v3
std::cout << "v4 的大小: " << v4.size() << std::endl;
for (int i : v4) {
std::cout << i << " ";
}
return 0;
}
- 示例讲解:代码开始包含了相关头文件。在
main
函数中,先使用std::vector<int> v3(3, 5);
创建了一个包含 3 个元素,每个元素值为 5 的std::vector<int>
对象v3
。接着通过std::vector<int> v4(v3);
调用拷贝构造函数,创建了v4
,它是v3
的一个副本,元素和v3
完全相同。然后使用v4.size()
输出v4
的元素数量,结果为 3,再使用范围for
循环遍历v4
并输出每个元素,输出结果为 3 个 5。
4. vector(InputIterator first, InputIterator last);
- 参数:
first
和last
为输入迭代器,指向复制元素的范围[first, last)
。 - 详细说明:构造一个
vector
,将[first, last)
范围内的元素复制到新容器。可用于从其他容器(如list
)或迭代器范围初始化vector
。 - 示例:
#include <vector>
#include <list>
#include <iostream>
int main() {
std::list<int> lst = {1, 2, 3};
std::vector<int> v5(lst.begin(), lst.end()); // 用 list 的迭代器初始化 vector
std::cout << "v5 的大小: " << v5.size() << std::endl;
for (int i : v5) {
std::cout << i << " ";
}
return 0;
}
- 示例讲解:该示例包含了
<vector>
、<list>
和<iostream>
头文件。在main
函数中,首先创建了一个std::list<int>
对象lst
,并初始化为包含元素 1、2、3。然后使用std::vector<int> v5(lst.begin(), lst.end());
调用使用迭代器的构造函数,将lst
中从lst.begin()
到lst.end()
范围内的元素复制到v5
中。接着通过v5.size()
输出v5
的元素数量,结果为 3。最后使用范围for
循环遍历v5
并输出每个元素,依次输出 1、2、3。
2.2 迭代器:玩转 vector 的 “指针”
2.2.1 六大迭代器函数
迭代器函数列表
序号 | 函数形式 | 功能说明 |
---|---|---|
1 | begin() + end() | 获取第一个数据位置的 iterator/const_iterator,获取最后一个数据的下一个位置的 iterator/const_iterator |
2 | rbegin() + rend() | 获取最后一个数据位置的 reverse_iterator,获取第一个数据前一个位置的 reverse_iterator |
3 | cbegin() + cend() | 获取第一个数据位置的 const_iterator,获取最后一个数据的下一个位置的 const_iterator |
各迭代器函数详细信息
1. begin() + end()
- 参数:无(作为
vector
成员函数,直接通过容器对象调用,如v.begin()
、v.end()
)。 - 详细说明:
begin()
函数用于获取指向容器首个元素的迭代器(iterator
)或常量迭代器(const_iterator
),这取决于调用该函数的对象是否为常量对象。end()
函数用于获取指向容器最后一个元素下一个位置的迭代器(iterator
)或常量迭代器(const_iterator
)。二者共同定义了一个左闭右开区间[begin(), end())
,该区间覆盖了容器中的所有元素,常用于对容器进行正向遍历操作。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
return 0;
}
- 示例讲解:在这个示例中,
v.begin()
指向向量v
中的第一个元素1
,v.end()
指向向量v
中最后一个元素5
的下一个位置。通过for
循环,使用迭代器it
从begin()
开始,不断向后移动(++it
),直到it
等于end()
为止,从而遍历整个向量并输出每个元素,输出结果为1 2 3 4 5
。
2. rbegin() + rend()
- 参数:无(作为
vector
成员函数,直接通过容器对象调用,如v.rbegin()
、v.rend()
)。 - 详细说明:
rbegin()
函数用于获取指向容器最后一个元素的反向迭代器(reverse_iterator
),rend()
函数用于获取指向容器第一个元素前一个位置的反向迭代器(reverse_iterator
)。它们共同定义了一个左闭右开区间[rbegin(), rend())
,该区间可用于对容器进行反向遍历操作。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.rbegin(); it != v.rend(); ++it) {
std::cout << *it << " ";
}
return 0;
}
- 示例讲解:在此示例中,
v.rbegin()
指向向量v
中的最后一个元素5
,v.rend()
指向向量v
中第一个元素1
的前一个位置。通过for
循环,使用反向迭代器it
从rbegin()
开始,不断向后移动(++it
,对于反向迭代器,向后移动实际上是向前遍历元素),直到it
等于rend()
为止,从而实现对向量的反向遍历并输出每个元素,输出结果为5 4 3 2 1
。
3. cbegin() + cend()
- 参数:无(作为
vector
成员函数,直接通过容器对象调用,如v.cbegin()
、v.cend()
)。 - 详细说明:
cbegin()
函数用于获取指向容器第一个元素的常量迭代器(const_iterator
),cend()
函数用于获取指向容器最后一个元素下一个位置的常量迭代器(const_iterator
)。无论调用该函数的对象是否为常量对象,返回的都是常量迭代器,这意味着通过这些迭代器不能修改容器中的元素,主要用于在不需要修改元素的情况下对容器进行遍历,以保证数据的安全性。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.cbegin(); it != v.cend(); ++it) {
// *it = 10; // 编译错误,不能通过 const_iterator 修改元素
std::cout << *it << " ";
}
return 0;
}
- 示例讲解:在这个示例中,
v.cbegin()
指向向量v
中的第一个元素1
,v.cend()
指向向量v
中最后一个元素5
的下一个位置。通过for
循环,使用常量迭代器it
从cbegin()
开始,不断向后移动(++it
),直到it
等于cend()
为止,从而遍历整个向量并输出每个元素。如果尝试通过常量迭代器修改元素(如注释中的*it = 10;
),会导致编译错误,因为常量迭代器不允许修改其所指向的元素,输出结果为1 2 3 4 5
。
2.2.2 迭代器失效:最容易踩的坑(附解决方案)
场景 1:扩容导致迭代器失效(如 push_back
/insert
触发扩容)
- 失效本质与野指针形成:
vector
依赖连续内存存储元素。当通过push_back
或insert
操作添加元素触发扩容时,容器会申请一块新的更大内存空间(例如原容量为 3,添加元素后需扩容至 6),将旧内存中的元素逐一复制到新内存,随后释放旧内存。此时,指向旧内存的迭代器(如it
)未被更新,它所存储的地址指向的内存已被释放,成为 “野指针”(指向已释放或无效内存区域的指针 / 迭代器)。解引用野指针(如*it
)时,程序会访问到无效内存,引发未定义行为(可能表现为程序崩溃、读取到随机值等)。 - 错误代码深度分析:
vector<int> v = {1,2,3}; auto it = v.begin(); // it 指向旧内存中元素 1 的位置 v.push_back(4); // 触发扩容:旧内存释放,新内存分配并复制元素 1,2,3,再存入 4。此时 it 仍指向旧内存地址,成为野指针。 cout << *it; // 访问野指针指向的区域,该区域已非 `vector` 有效内存,行为不可预测。
- 正确做法原理:重新获取迭代器,扩容后,所有迭代器需重新赋值。
v.push_back(4); it = v.begin(); // 重新指向新内存的首元素
场景 2:删除元素导致迭代器失效(erase
操作)
- 失效本质与野指针形成:
当使用erase
删除元素时,被删除元素后方的所有元素会向前移动填补空缺。例如,对于[1,2,3,4]
,若pos
指向元素3
,执行v.erase(pos)
后,元素4
会前移到3
的位置。此时,未更新的pos
仍指向旧的3
的存储位置,而该位置要么已被后续元素覆盖,要么处于无效状态,使pos
沦为野指针。解引用这样的野指针(如*pos
),程序会访问到无意义或已被修改的内存区域,导致崩溃或逻辑错误。 - 错误代码深度分析:
auto pos = find(v.begin(), v.end(), 3); // 假设找到元素 3 的位置,pos 指向它。 v.erase(pos); // 删除元素 3,后续元素前移。pos 未更新,仍指向旧的 3 的位置,成为野指针。 cout << *pos; // 访问野指针,该位置已非有效元素存储区,操作非法。
- 正确做法原理:
erase
函数设计上会返回一个迭代器,该迭代器指向删除操作后下一个元素的位置(如删除3
后,返回指向4
的迭代器)。通过pos = v.erase(pos);
更新迭代器,使其指向有效元素(如4
),彻底消除野指针风险,保证后续操作基于合法内存地址进行。 -
pos = v.erase(pos); // pos 现在指向元素 4
综上,vector
的迭代器失效本质是迭代器指向的内存不再有效(被释放或元素布局改变),形成野指针。理解这一过程,在涉及容器修改操作(扩容、删除等)时,通过更新迭代器来规避野指针,是编写正确、健壮代码的关键。
2.3 空间管理:size vs capacity(彻底搞懂内存分配)
2.3.1 核心接口对比(表格 + 例子)
接口 | 作用 | 是否改变 size | 是否初始化新元素 | 示例(v 初始 size=0, capacity=0) |
---|---|---|---|---|
size() | 获取有效元素个数 | 否 | 否 | v.size() → 0 |
capacity() | 获取已分配内存可容纳的元素个数 | 否 | 否 | v.capacity () → 0(初始) |
resize(n) | 调整 size 为 n,若 n>capacity 则扩容 | 是 | 是(默认值初始化) | v.resize (5) → size=5,元素为 0 |
resize(n, val) | 调整 size 为 n,用 val 初始化新元素 | 是 | 是 | v.resize(3, 10) → [10,10,10] |
reserve(n) | 保证 capacity 至少为 n,若 n>current 则扩容 | 否 | 否(不初始化) | v.reserve(100) → capacity≥100 |
1. size()
- 作用:用于查询
vector
容器中当前有效元素的实际个数,就如同数清篮子里现有的苹果数量,仅为统计功能,不涉及对篮子(容器)本身容量或苹果(元素)的增删改操作。 - 是否改变
size
:否。它只是一个 “查看” 操作,不会对容器中有效元素的数量进行任何修改,就像只看不拿放,篮子里苹果数不变。 - 是否初始化新元素:否。该操作与新元素的创建或初始化毫无关系,它不关心篮子外面有没有苹果,只看篮子里现有多少。
- 示例:
#include <vector> #include <iostream> int main() { std::vector<int> v; std::cout << "v 的有效元素个数: " << v.size() << std::endl; // v 初始 size=0,输出 0 return 0; }
- 在此示例中,
v
是一个初始为空的vector
,v.size()
仅仅是去 “数” 容器里有多少元素,由于此时容器中没有任何元素,所以返回0
,整个过程没有对容器做任何改动。
2. capacity()
- 作用:查询
vector
容器已分配内存能够容纳的元素个数,这类似于知道篮子目前最多能装多少苹果(即使现在没装满),是对容器内存 “潜力” 的一种查询。 - 是否改变
size
:否。它关注的是容器的内存容量,与容器中实际装了多少元素(size
)没有关系,不会因为查询容量而改变元素个数。 - 是否初始化新元素:否。该操作仅聚焦于内存空间的信息,不涉及新元素的产生或初始化,就像只问篮子能装多少,不管有没有苹果、苹果怎么来的。
- 示例:
#include <vector> #include <iostream> int main() { std::vector<int> v; std::cout << "v 已分配内存可容纳的元素个数: " << v.capacity() << std::endl; // 初始 capacity=0,输出 0 return 0; }
- 这里
v
初始时没有进行任何扩容操作,就像一个刚拿出来还没调整大小的篮子,它能容纳的苹果数(capacity
)为0
,v.capacity()
只是把这个信息反馈出来,不涉及其他变化。
3. resize(n)
- 作用:调整
vector
的size
(有效元素个数)为n
。如果n
大于当前capacity
(已分配内存可容纳的元素个数),则容器会进行扩容,以便能容纳n
个元素;如果n
小于当前size
,则容器尾部的元素会被 “丢弃”,只保留前n
个元素;如果n
大于当前size
,新增的元素会以默认值初始化(如int
类型默认初始化为0
)。这好比调整篮子里苹果的数量,不够就换大篮子(扩容),多了就拿走一些,新增位置的苹果先默认放0
个。 - 是否改变
size
:是。该操作直接目标就是修改容器中有效元素的个数,将其明确调整为n
,就像主动增加或减少篮子里的苹果数。 - 是否初始化新元素:是。当
n
大于当前size
时,新增的元素位置需要 “放东西”,这些新位置会以默认值初始化,如同在新扩大的篮子区域先默认放0
个苹果。 - 示例:
#include <vector> #include <iostream> int main() { std::vector<int> v; v.resize(5); // 调整 size 为 5,若 capacity 不足则扩容,新元素默认初始化为 0 std::cout << "v 的 size: " << v.size() << std::endl; // 输出 5 for (int i : v) { std::cout << i << " "; // 输出 0 0 0 0 0 } return 0; }
- 此示例中,
v
初始size
和capacity
均为0
,v.resize(5)
要把size
改成5
。因为原来容量不够,所以容器会扩容,然后在这5
个位置上放入默认值0
,就像给一个小篮子换成大篮子,再在新的五个位置都先放0
个苹果,最终size
变为5
,元素都是0
。
4. resize(n, val)
- 作用:调整
vector
的size
为n
,与resize(n)
类似,但更 “明确”。若n > capacity
,触发扩容;若n < size
,删除尾部元素;若n > size
,新增元素用val
初始化。这就像调整篮子里苹果数,同时明确新位置的苹果要放成val
这个值。 - 是否改变
size
:是。直接将容器有效元素个数改成n
,主动控制篮子里苹果的数量。 - 是否初始化新元素:是。当
n > size
时,新增元素用val
来初始化,不像resize(n)
用默认值,这里是自定义值,如同给新篮子位置放特定数量的苹果。 - 示例:
#include <vector> #include <iostream> int main() { std::vector<int> v; v.resize(3, 10); // 调整 size 为 3,若 capacity 不足则扩容,新元素初始化为 10 std::cout << "v 的 size: " << v.size() << std::endl; // 输出 3 for (int i : v) { std::cout << i << " "; // 输出 10 10 10 } return 0; }
- 这里
v
初始size
和capacity
为0
,v.resize(3, 10)
要把size
变为3
。容器先扩容(如果需要),然后在这3
个位置都放入10
,就像准备一个能放3
个苹果的篮子,每个位置都放10
个苹果,最终size
是3
,元素都是10
。
5. reserve(n)
- 作用:保证
vector
的capacity
(已分配内存可容纳的元素个数)至少为n
。如果n
大于当前capacity
,容器会进行扩容,使capacity
不小于n
;如果n
小于或等于当前capacity
,则不做任何操作。该操作不改变容器中有效元素的个数(size
),也不对新的内存空间进行初始化,就像给篮子准备足够的空间,但不关心现在篮子里有几个苹果,新空间也不放苹果。 - 是否改变
size
:否。它只关注容器的内存容量,不涉及对现有元素个数的增加或减少,篮子里苹果数不变。 - 是否初始化新元素:否。仅对内存进行扩容(如果需要),但不会往新的内存空间放任何元素,新空间就像空着的位置,没苹果。
- 示例:
此示例中,#include <vector> #include <iostream> int main() { std::vector<int> v; v.reserve(100); // 保证 capacity 至少为 100,若初始 capacity 为 0 则扩容 std::cout << "v 的 capacity: " << v.capacity() << std::endl; // 输出 >=100 的值(如 100,具体取决于实现) std::cout << "v 的 size: " << v.size() << std::endl; // 输出 0,size 未改变 return 0; }
v.reserve(100)
是给v
这个容器 “准备” 至少能放100
个元素的内存空间(如果初始不够就扩容)。但容器里实际的元素个数(size
)还是0
,就像把篮子做得很大,但里面还没开始放苹果,新的篮子空间也没放任何东西,只是有了能放100
个苹果的潜力。
2.3.2 扩容策略:VS vs GCC(面试常考!)
VS(PJ STL):按 1.5 倍扩容
-
扩容过程分析
- 初始状态:当我们创建一个空的
vector
时,初始capacity
为 0。这意味着此时vector
还没有分配任何用于存储元素的内存空间。 - 插入元素 1:当插入第一个元素时,
vector
需要为这个元素分配内存,所以capacity
变为 1。 - 插入元素 2:插入第二个元素时,当前
capacity
刚好可以容纳,所以capacity
保持为 2。 - 插入元素 3:同理,当前
capacity
足够,capacity
仍为 3。 - 插入元素 4:还是能容纳,
capacity
为 4。 - 插入元素 5:此时当前
capacity
为 4,不足以容纳新元素,需要进行扩容。按照 VS 的 1.5 倍扩容策略,新的capacity
为4 * 1.5 = 6
。
- 初始状态:当我们创建一个空的
-
设计原因:VS 的 1.5 倍扩容策略是为了平衡空间利用率。每次扩容为原来的 1.5 倍,不会像 2 倍扩容那样每次都分配过多的额外空间,避免了大量内存的浪费。例如,如果只需要存储少量额外元素,2 倍扩容可能会导致分配的空间远远超过实际需求,而 1.5 倍扩容相对更 “节制”,能在一定程度上提高空间的使用效率。
GCC(SGI STL):按 2 倍扩容
-
扩容过程分析
- 初始状态:同样,创建空
vector
时,初始capacity
为 0。 - 插入元素 1:为第一个元素分配内存,
capacity
变为 1。 - 插入元素 2:
capacity
变为 2。 - 插入元素 3:当前
capacity
为 2,无法容纳新元素,进行扩容。按照 2 倍扩容策略,新的capacity
为2 * 2 = 4
。 - 插入元素 5:此时
capacity
为 4,再次插入元素时需要扩容,新的capacity
变为4 * 2 = 8
。
- 初始状态:同样,创建空
-
设计原因:GCC 的 2 倍扩容策略追求简单高效。每次扩容为原来的 2 倍,计算简单,并且能快速增加可用空间,减少扩容的次数。因为扩容操作涉及到内存的重新分配和元素的拷贝,频繁扩容会带来较大的性能开销,所以 2 倍扩容可以在一定程度上减少这种开销,提高插入元素的效率。
reserve(1000)
的作用:reserve
函数用于预先分配指定数量的内存空间。在这个例子中,v.reserve(1000)
会让vector
提前分配足够存储 1000 个元素的内存空间,将capacity
至少设置为 1000。这样在后续的for
循环中,使用push_back
插入 1000 个元素时,由于capacity
已经足够,不会触发扩容操作,也就避免了元素拷贝的开销,从而显著提升了性能。
2.4 增删查改:常用接口详解
序号 | 函数形式 | 功能说明 |
---|---|---|
1 | push_back | 尾插 |
2 | pop_back | 尾删 |
3 | find | 查找。(注意这个是算法模块实现,不是 vector 的成员接口) |
4 | insert | 在 position 之前插入 val |
5 | erase | 删除 position 位置的数据 |
6 | swap | 交换两个 vector 的数据空间 |
7 | operator[] | 像数组一样访问 |
8 | at | 访问指定位置的元素,并进行边界检查 |
各接口详细信息
1. push_back
- 函数形式:
void push_back(const T& val)
(假设T
是vector
存储的元素类型) - 参数:
val
,要插入到尾部的元素值。 - 详细说明:此函数用于在
vector
的尾部插入一个元素。若插入后元素个数超过当前capacity
,会触发扩容。均摊时间复杂度为 O (1)(考虑扩容影响)。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v; // 创建空的 vector
v.push_back(1); // 尾部插入 1,此时 v 为 [1]
v.push_back(2); // 尾部插入 2,此时 v 为 [1, 2]
for (int i : v) { // 遍历输出元素
std::cout << i << " ";
}
return 0;
}
- 示例讲解:首先创建一个空的
std::vector<int>
容器v
。第一次调用v.push_back(1)
,在容器尾部插入元素1
,此时容器内元素为[1]
。第二次调用v.push_back(2)
,在尾部插入元素2
,容器内元素变为[1, 2]
。最后通过范围for
循环遍历容器,依次输出每个元素,结果为1 2
。
2. pop_back
- 函数形式:
void pop_back()
- 参数:无。
- 详细说明:用于删除
vector
尾部的元素。若vector
为空,调用pop_back
会导致未定义行为(如 Debug 模式下触发断言)。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]
v.pop_back(); // 删除尾部元素 3,此时 v 为 [1, 2]
for (int i : v) { // 遍历输出元素
std::cout << i << " ";
}
return 0;
}
- 示例讲解:先创建一个包含
1、2、3
三个元素的std::vector<int>
容器v
。调用v.pop_back()
时,会删除容器尾部的元素3
,容器内元素变为[1, 2]
。最后通过范围for
循环遍历容器,输出结果为1 2
。
3. find
- 函数形式:
Iterator find(Iterator first, Iterator last, const T& val)
(需包含<algorithm>
头文件) - 参数:
first
和last
是迭代器,指定查找范围[first, last)
;val
是要查找的值。 - 详细说明:在指定范围内查找等于
val
的元素,找到返回指向该元素的迭代器,否则返回last
。是标准算法,非vector
成员函数。 - 示例:
#include <vector>
#include <iostream>
#include <algorithm> // 包含 find 所在的算法头文件
int main() {
std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]
auto it = std::find(v.begin(), v.end(), 2); // 在 [1, 2, 3] 中找 2
if (it != v.end()) { // 若找到(it 不指向 end)
std::cout << "找到元素 2" << std::endl;
}
return 0;
}
- 示例讲解:首先创建一个包含
1、2、3
的std::vector<int>
容器v
。通过std::find(v.begin(), v.end(), 2)
在容器v
的[begin(), end())
范围内查找值为2
的元素。由于v
中存在2
,find
会返回指向2
的迭代器it
。通过判断it != v.end()
(若没找到,it
会等于v.end()
),确认找到元素后输出提示信息找到元素 2
。
4. insert
- 函数形式:
Iterator insert(Iterator position, const T& val)
- 参数:
position
是迭代器,指定插入位置;val
是要插入的元素。 - 详细说明:在
position
之前插入val
,返回指向新插入元素的迭代器。若插入导致扩容,迭代器可能失效。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]
auto it = v.begin() + 1; // 迭代器指向位置 1(元素 2)
v.insert(it, 4); // 在该位置前插入 4,v 变为 [1, 4, 2, 3]
for (int i : v) { // 遍历输出元素
std::cout << i << " ";
}
return 0;
}
- 示例讲解:先创建一个包含
1、2、3
的std::vector<int>
容器v
。auto it = v.begin() + 1
获取指向第二个元素(2
)的迭代器it
。调用v.insert(it, 4)
时,会在it
指向的元素(2
)之前插入4
,容器内元素变为[1, 4, 2, 3]
。最后通过范围for
循环遍历容器,输出结果为1 4 2 3
。
5. erase
- 函数形式:
Iterator erase(Iterator position)
- 参数:
position
是迭代器,指定要删除的位置。 - 详细说明:删除
position
位置的元素,后面的元素向前移动,返回指向删除位置下一个元素的迭代器。迭代器在删除后会失效,需用返回值更新。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]
auto it = v.begin() + 1; // 迭代器指向位置 1(元素 2)
v.erase(it); // 删除该位置元素,v 变为 [1, 3]
for (int i : v) { // 遍历输出元素
std::cout << i << " ";
}
return 0;
}
- 示例讲解:创建包含
1、2、3
的std::vector<int>
容器v
。auto it = v.begin() + 1
获取指向第二个元素(2
)的迭代器it
。调用v.erase(it)
时,会删除it
指向的元素(2
),后面的元素3
向前移动,容器内元素变为[1, 3]
。最后通过范围for
循环遍历容器,输出结果为1 3
。
6. swap
- 函数形式:
void swap(vector& x)
- 参数:
x
是另一个vector
对象。 - 详细说明:交换当前
vector
和x
的数据空间,包括元素、size
、capacity
等。操作效率高,时间复杂度 O (1)。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v1 = {1, 2}; // v1 初始为 [1, 2]
std::vector<int> v2 = {3, 4}; // v2 初始为 [3, 4]
v1.swap(v2); // 交换 v1 和 v2 的数据空间
for (int i : v1) { // 遍历输出 v1 元素
std::cout << i << " ";
}
for (int i : v2) { // 遍历输出 v2 元素
std::cout << i << " ";
}
return 0;
}
- 示例讲解:先创建
v1
([1, 2]
)和v2
([3, 4]
)两个std::vector<int>
容器。调用v1.swap(v2)
时,会交换它们的数据空间,包括元素、大小和容量等信息。此时v1
变为[3, 4]
,v2
变为[1, 2]
。通过两个范围for
循环分别遍历v1
和v2
,输出结果为3 4
和1 2
。
7. operator[]
- 函数形式:
T& operator[](size_type pos)
(非const vector
);const T& operator[](size_type pos)
(const vector
) - 参数:
pos
是要访问元素的位置索引。 - 详细说明:像数组一样通过索引访问
vector
中的元素,不进行越界检查(越界行为未定义)。 - 示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]
std::cout << v[1] << std::endl; // 访问索引为 1 的元素(值为 2)
return 0;
}
- 示例讲解:创建包含
1、2、3
的std::vector<int>
容器v
。通过v[1]
访问索引为1
的元素(第二个元素),由于operator[]
不检查越界(此处索引合法),直接返回该位置的元素2
,并输出2
。
8. at
- 函数形式:
T& at(size_type pos)
(非const vector
);const T& at(size_type pos)
(const vector
) - 参数:
pos
是要访问元素的位置索引。 - 详细说明:用于访问
vector
中指定位置的元素,与operator[]
不同的是,at
会进行边界检查。如果pos
超出了vector
的有效范围(即pos >= size()
),会抛出std::out_of_range
异常,这样可以避免因越界访问导致的未定义行为,增强了程序的健壮性。 - 示例:
#include <vector>
#include <iostream>
#include <stdexcept> // 包含异常头文件
int main() {
std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]
try {
std::cout << v.at(1) << std::endl; // 正常访问索引 1 的元素(值为 2)
std::cout << v.at(3) << std::endl; // 越界访问(有效索引 0-2),触发异常
} catch (const std::out_of_range& e) {
std::cerr << "Out of range error: " << e.what() << std::endl; // 捕获异常并输出信息
}
return 0;
}
- 示例讲解:创建包含
1、2、3
的std::vector<int>
容器v
。在try
块中,首先通过v.at(1)
访问索引为1
的元素(合法访问),返回值2
并输出。接着尝试通过v.at(3)
访问索引为3
的元素,由于v
的有效索引范围是0
到2
,这是一次越界访问。此时at
会抛出std::out_of_range
异常,程序跳转到catch
块,捕获异常并输出错误信息Out of range error: vector::_M_range_check: __n (which is 3) >= this->size() (which is 3)
(不同编译器输出细节可能有差异,但都表明越界),避免了程序因越界访问导致的崩溃或未定义行为。
三、迭代器失效:从原理到实战(避坑指南)
3.1 失效的本质:指针指向无效内存
vector 的迭代器本质是原始指针T*
,当底层内存被释放或元素位置改变时,指针就失效了,就像你记住了朋友家的地址,但朋友搬家了,你再按旧地址去找就找不到了。
3.2 触发失效的操作(全列表)
改变底层空间的操作(导致旧内存释放)
这类操作会对 vector
的底层内存布局进行调整,通常会涉及到内存的重新分配,旧的内存空间会被释放,这就使得原本指向旧内存的迭代器变得无效。
-
push_back/insert
(可能扩容)push_back
:用于在vector
的尾部插入一个新元素。当插入元素后,vector
的元素数量超过了当前的capacity
(容量)时,就会触发扩容操作。扩容时,vector
会重新分配一块更大的内存空间,将旧内存中的元素复制到新内存中,然后释放旧内存。例如,初始capacity
为 2 的vector
,插入第 3 个元素时就可能触发扩容。insert
:可以在指定位置插入一个或多个元素。如果插入元素后导致元素数量超过当前capacity
,同样会触发扩容。例如,在vector
的中间位置插入元素,可能会使得原本的capacity
不够用,从而引发扩容。
-
resize
(可能扩容,且改变size
)resize
函数用于调整vector
的size
(元素数量)。如果指定的新size
大于当前的capacity
,就会触发扩容。同时,它会改变vector
的size
。如果新size
小于当前size
,则会删除尾部的元素;如果新size
大于当前size
,则会在尾部添加新元素(可以指定初始值)。例如,resize(10)
可能会让vector
扩容以容纳 10 个元素。
-
reserve
(直接改变capacity
,可能扩容)reserve
函数的作用是预先分配一定的内存空间,确保vector
的capacity
至少达到指定的值。如果指定的值大于当前的capacity
,就会触发扩容。例如,reserve(20)
会让vector
的capacity
至少变为 20,如果原本capacity
小于 20,就会进行扩容操作。
-
assign
(重新赋值,可能扩容)assign
函数用于将新的元素赋值给vector
,它会替换掉vector
中原来的所有元素。如果新元素的数量超过了当前的capacity
,就会触发扩容。例如,assign({1, 2, 3, 4, 5})
可能会因为新元素数量过多而导致扩容。
改变元素位置的操作(不释放内存,但元素移动)
这类操作虽然不会释放底层内存,但会改变元素在内存中的相对位置,这会使得原本指向某些元素的迭代器不再指向正确的元素。
-
erase
(删除位置后的元素前移)erase
函数用于删除vector
中指定位置的元素。当删除一个元素后,该元素后面的所有元素都会向前移动一个位置,以填补删除元素后留下的空缺。例如,删除vector
中间的一个元素,后面的元素会依次向前移动。这样一来,原本指向删除位置之后元素的迭代器就会失效,因为它们指向的元素已经发生了变化。
-
swap
(交换两个vector
的数据,迭代器指向对方的内存)swap
函数用于交换两个vector
的数据。交换后,两个vector
的元素、size
和capacity
都会相互交换。此时,原本指向一个vector
元素的迭代器会指向另一个vector
的对应位置,导致迭代器失效。例如,vector1
和vector2
交换后,vector1
的迭代器会指向vector2
的元素,这显然不是我们所期望的。
3.3 经典错误案例(附调试方法)
案例 1:扩容后继续使用旧迭代器(VS 直接崩溃,GCC 可能输出乱码)
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
auto it = v.begin(); // 初始it是end()(size=0)
v.push_back(1); // 扩容,capacity=1,it失效(原end()指向旧内存,已释放)
std::cout << *it; // VS报错“访问违规”,GCC可能输出随机值(未定义行为)
return 0;
}
-
详细分析:
- 首先创建了一个空的
vector
v
,此时v
的size
为 0,it
初始化为v.begin()
,实际上它指向的是v
的end()
位置。 - 调用
v.push_back(1)
时,由于v
原本为空,插入元素 1 会触发扩容操作。vector
会重新分配一块新的内存空间来存储元素 1,同时释放旧的内存空间。 - 此时,
it
仍然指向旧的内存地址,而这块内存已经被释放,成为了无效的内存。当尝试使用std::cout << *it;
解引用it
时,就会出现问题。在 VS 编译器中,会直接报错 “访问违规”,因为程序试图访问已经释放的内存;在 GCC 编译器中,可能会输出随机值,这是一种未定义行为,程序的表现是不可预测的。
- 首先创建了一个空的
-
调试方法:
- 可以在关键操作前后输出
vector
的size
和capacity
,观察内存的变化情况。例如,在push_back
前后分别输出:
- 可以在关键操作前后输出
std::cout << "Before push_back: size = " << v.size() << ", capacity = " << v.capacity() << std::endl;
v.push_back(1);
std::cout << "After push_back: size = " << v.size() << ", capacity = " << v.capacity() << std::endl;
- 使用调试器(如 Visual Studio 的调试工具或 GDB),在
push_back
之后设置断点,检查it
的值和指向的内存地址,确认其是否失效。
案例 2:erase
后未更新迭代器(GCC 允许运行,但结果错误)
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4};
auto it = v.begin() + 1; // 指向2
v.erase(it); // 删除2,后面的3、4前移
std::cout << *it; // GCC可能输出3(it现在指向原3的位置),但逻辑上it已失效,结果不可靠
return 0;
}
-
详细分析:
- 首先创建了一个包含 4 个元素的
vector
v
,并将迭代器it
指向元素 2 的位置。 - 调用
v.erase(it)
时,会删除it
指向的元素 2。删除后,元素 3 和 4 会向前移动一个位置,填补元素 2 的空缺。 - 此时,
it
仍然指向原来元素 2 的位置,但这个位置现在存储的是元素 3。虽然在 GCC 编译器中,程序可能会允许运行并输出 3,但从逻辑上来说,it
已经失效,因为它不再指向我们原本期望的元素。如果继续使用这个失效的迭代器进行操作,可能会导致程序出现错误的结果。
- 首先创建了一个包含 4 个元素的
-
调试方法:
- 在
erase
前后输出vector
的元素,观察元素的变化情况。例如:
- 在
std::cout << "Before erase: ";
for (auto num : v) {
std::cout << num << " ";
}
std::cout << std::endl;
v.erase(it);
std::cout << "After erase: ";
for (auto num : v) {
std::cout << num << " ";
}
std::cout << std::endl;
- 使用调试器,在
erase
之后设置断点,检查it
的值和指向的元素,确认其是否失效。
正确做法:每次可能失效的操作后,重新获取迭代器或使用返回值
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4};
auto it = v.begin();
while (it != v.end()) {
if (*it % 2 == 0) {
it = v.erase(it); // erase返回下一个有效迭代器,避免漏判
} else {
++it;
}
}
for (auto num : v) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
- 详细分析:
- 这段代码的目的是删除
vector
中的所有偶数元素。 - 首先,将迭代器
it
初始化为v.begin()
,指向vector
的第一个元素。 - 进入
while
循环,只要it
不等于v.end()
,就继续循环。 - 在循环中,检查
it
指向的元素是否为偶数。如果是偶数,调用v.erase(it)
删除该元素,并将返回的迭代器赋值给it
。erase
函数会返回指向删除元素下一个元素的迭代器,这样就保证了it
始终是一个有效的迭代器,避免了漏判元素的情况。 - 如果
it
指向的元素不是偶数,则将it
向后移动一个位置(++it
)。 - 最后,遍历
vector
并输出剩余的元素。
- 这段代码的目的是删除
通过这种方式,我们可以正确地处理可能导致迭代器失效的操作,避免程序出现错误。
四、深度剖析:模拟实现 vector(理解底层原理)
每个成员变量功能
_start
:指向存储元素的内存起始处。空vector
时为nullptr
,有元素后指向分配内存起点,是元素访问和迭代器起始的依据。_finish
:指向最后一个有效元素的下一个位置。界定有效元素范围[_start, _finish)
,用于计算元素数量和作为迭代器遍历终点。_endofstorage
:指向已分配内存的末尾。界定容量范围[_start, _endofstorage)
,用于计算容量和判断是否需要扩容。
协作机制
- 插入元素:检查
_finish
与_endofstorage
,若相等则扩容,更新三个指针,插入元素并后移_finish
。 - 删除元素:移除元素后元素前移,前移
_finish
更新有效范围。 - 扩容操作:分配新内存,
_start
指向新起点,复制元素,调整_finish
和_endofstorage
。
1. 默认构造函数
vector() : _start(nullptr), _finish(nullptr), _endofstorage(nullptr) {}
- 实现目的:初始化
vector
对象,使其处于空状态,为后续元素插入等操作做好准备。 - 方法选择原因:直接将三个指针成员(
_start
指向起始位置,_finish
指向最后一个元素的下一个位置,_endofstorage
指向分配内存的末尾)置为nullptr
,清晰表明容器初始无任何有效内存分配和元素存储。 - 注意事项:这是容器的初始状态,后续对容器的操作(如
push_back
)会基于此状态进行内存分配和元素管理。
2. 带参数的构造函数
vector(int n, const T& val = T()) { resize(n, val); }
- 实现目的:创建一个包含
n
个元素的vector
,每个元素初始化为val
。 - 方法选择原因:复用
resize
函数,因为resize
本身就负责调整容器大小并初始化元素。这样可以减少代码重复,提高代码复用性。 - 注意事项:若
T
类型没有默认构造函数,使用默认值T()
会报错,此时需用户显式提供val
。
3. 迭代器范围构造函数
template<class InputIterator>
vector(InputIterator first, InputIterator last) {
while (first != last) {
push_back(*first);
++first;
}
}
- 实现目的:通过给定的迭代器范围
[first, last)
初始化vector
,将范围内的元素逐个插入。 - 方法选择原因:利用
push_back
的尾部插入功能,逐个添加元素,适用于各种支持输入迭代器的容器(如list
、array
等),增强了vector
初始化的灵活性。 - 注意事项:确保传入的迭代器范围有效(
first
不超过last
),否则会陷入死循环。
4. 拷贝构造函数
vector(const vector<T>& v) {
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); i++) {
_start[i] = v._start[i];
}
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
}
- 实现目的:深拷贝一个
vector
,使新vector
与源vector
有独立的内存空间,避免浅拷贝导致的资源重复释放问题。 - 方法选择原因:手动分配内存(
_start = new T[v.capacity()]
)并逐个元素复制(_start[i] = v._start[i]
),而不用memcpy
。因为memcpy
是按字节复制,若T
是自定义类型(如包含指针成员),会导致浅拷贝(两个对象的指针成员指向同一内存),后续析构时会重复释放内存。逐个复制能确保每个元素都正确复制,实现深拷贝。 - 注意事项:确保源
vector
的capacity
和size
准确获取,避免内存分配不足或浪费。
5. 析构函数
~vector() {
if (_start) {
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
- 实现目的:释放
vector
动态分配的内存,防止内存泄漏。 - 方法选择原因:检查
_start
是否为nullptr
,若不为空则释放数组内存(delete[] _start
),并将三个指针置为nullptr
,确保对象不再持有无效内存指针。 - 注意事项:若
vector
中有指针成员指向其他资源,需在析构函数中额外处理这些资源的释放,这里vector
仅管理元素内存,故只需释放_start
。
6. 迭代器获取函数
iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator begin()const { return _start; }
const_iterator end()const { return _finish; }
- 实现目的:为
vector
提供迭代器,方便遍历容器元素。 - 方法选择原因:直接返回内部指针
_start
和_finish
,const
版本返回const_iterator
确保常量对象不能通过迭代器修改元素,符合常量对象的语义。 - 注意事项:迭代器的有效性依赖于
vector
的内部状态(_start
和_finish
),若vector
进行了可能改变内存布局的操作(如insert
、erase
导致扩容或元素移动),迭代器可能失效,需重新获取。
7. reserve
(预留空间)
void reserve(size_t n) {
if (n > capacity()) {
size_t sz = size();
T* tmp = new T[n];
if (_start) {
for (size_t i = 0; i < sz; i++) {
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
- 实现目的:确保
vector
至少有n
个元素的存储容量,若不足则扩容。 - 方法选择原因:先分配新内存(
T* tmp = new T[n]
),若原_start
有内存则逐个复制元素(避免memcpy
的浅拷贝问题),再释放旧内存(delete[] _start
),更新指针。这种方式保证了元素的深拷贝和内存的正确管理。 - 注意事项:扩容后,原来的迭代器会失效(因为内存地址改变),需重新获取迭代器。
8. resize
(调整大小)
void resize(size_t n, const T& val = T()) {
if (n < size()) {
_finish = _start + n;
} else {
reserve(n);
while (_finish != _start + n) {
*_finish = val;
++_finish;
}
}
}
- 实现目的:调整容器的有效元素个数为
n
,若n
大于当前容量则扩容。 - 方法选择原因:当
n
小于当前size
时,直接截断(_finish = _start + n
);当n
大于当前size
时,先调用reserve
扩容,再初始化新元素(*_finish = val
)。这样分情况处理,既保证了内存的合理分配,又实现了元素的初始化。 - 注意事项:若
val
是复杂对象(如自定义类),其赋值操作(*_finish = val
)可能涉及拷贝构造等操作,需确保T
类型的赋值操作正确。
9. push_back
(尾部插入)
void push_back(const T& x) { insert(end(), x); }
- 实现目的:在
vector
尾部插入一个元素。 - 方法选择原因:复用
insert
函数,在end()
(最后一个元素的下一个位置)前插入元素x
,代码简洁且复用性高。 - 注意事项:若插入导致扩容,所有指向
vector
的迭代器都会失效。
10. pop_back
(尾部删除)
void pop_back() { erase(--end()); }
- 实现目的:删除
vector
尾部的元素。 - 方法选择原因:先将
end()
指针前移(--end()
,指向最后一个元素),再调用erase
函数删除该位置元素,复用erase
函数实现尾部删除。 - 注意事项:若
vector
为空,调用pop_back
会导致未定义行为(erase
中的assert
检查会报错),使用前需确保vector
非空。
11. insert
(指定位置插入)
iterator insert(iterator pos, const T& x) {
assert(pos >= _start && pos <= _finish);
if (_finish == _endofstorage) {
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos) {
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
- 实现目的:在指定位置
pos
前插入元素x
。 - 方法选择原因:先检查
pos
合法性(assert(pos >= _start && pos <= _finish)
),若空间不足则扩容(计算偏移量len
,调用reserve
扩容后重新计算pos
),然后从_finish - 1
向前移动元素(*(end + 1) = *end
),为插入元素腾出位置,最后插入元素并更新_finish
。这种方式保证了插入操作的正确性和对迭代器失效的处理(扩容时迭代器失效,重新获取pos
位置)。 - 注意事项:插入操作可能导致迭代器失效(扩容或元素移动),调用后需确保使用返回的迭代器或重新获取迭代器。
12. erase
(指定位置删除)
iterator erase(iterator pos) {
assert(pos >= _start && pos < _finish);
iterator it = pos + 1;
while (it != _finish) {
*(it - 1) = *it;
++it;
}
--_finish;
return pos;
}
- 实现目的:删除指定位置
pos
的元素。 - 方法选择原因:检查
pos
合法性(assert(pos >= _start && pos < _finish)
),从pos + 1
开始向前移动元素(*(it - 1) = *it
)覆盖被删除元素,更新_finish
指针(--_finish
)。这种方式实现了元素的删除和后续元素的前移。 - 注意事项:被删除位置的迭代器
pos
在删除后指向已被覆盖的位置,需更新迭代器(通常使用返回的pos
或重新获取迭代器),否则会导致迭代器失效。
13. 获取容器状态函数
capacity
(获取容量)
size_t capacity()const { return _endofstorage - _start; }
-
实现目的:返回
vector
已分配内存可容纳的元素个数。 -
方法选择原因:通过计算
_endofstorage
(分配内存的末尾)与_start
(起始位置)的差值,直接得到容量,简单直观。 -
注意事项:容量是已分配内存的大小,与实际元素个数(
size
)不同。 -
size
(获取元素个数)
size_t size()const { return _finish - _start; }
- 实现目的:返回
vector
中当前有效元素的个数。 - 方法选择原因:计算
_finish
(最后一个元素的下一个位置)与_start
(起始位置)的差值,得到元素个数,清晰明了。 - 注意事项:
size
始终小于或等于capacity
。
14. 下标访问函数
T& operator[](size_t pos) {
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos)const {
assert(pos < size());
return _start[pos];
}
- 实现目的:提供类似数组的下标访问方式,访问
vector
中的元素。 - 方法选择原因:通过
assert
检查pos
越界(pos < size()
),确保安全访问,然后返回_start[pos]
。const
版本保证常量对象只能读取不能修改元素。 - 注意事项:
operator[]
不检查越界(仅通过assert
在调试时检查),使用时需确保pos
合法,否则会导致未定义行为。
15. 交换函数与赋值运算符重载
swap
(交换两个vector
的成员)
void swap(vector<T>& v) {
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
-
实现目的:快速交换两个
vector
的数据空间、大小和容量等信息。 -
方法选择原因:利用
std::swap
交换三个指针成员,实现高效交换。这种方式时间复杂度为 O (1),因为只是交换指针,不涉及元素的复制。 -
注意事项:交换后,两个
vector
的迭代器、引用等都指向对方原来的内存,需注意迭代器的有效性。 -
operator=
(赋值运算符重载)
vector<T>& operator=(vector<T> v) {
swap(v);
return *this;
}
- 实现目的:将一个
vector
的内容赋值给另一个vector
。 - 方法选择原因:通过调用
swap
函数,将当前vector
与传入的vector
v
交换数据空间等信息。由于v
是拷贝构造的临时对象,函数结束时v
会析构,释放其原来的资源,从而实现正确的赋值操作。这种实现方式保证了异常安全(若swap
过程中无异常,原对象资源已交换,不会造成泄漏)。 - 注意事项:赋值后,原来的
vector
内容被交换出去,其资源会在临时对象v
析构时释放,无需手动管理。
4.4 为什么不能用 memcpy?(自定义类型的噩梦)
在 vector
拷贝构造函数里,使用 memcpy
对包含指针成员的自定义类型复制时会导致浅拷贝问题。浅拷贝仅复制指针的值(内存地址),而非指针指向的数据,多个对象的指针成员会指向同一块内存。当其中一个对象释放该内存,其他对象的指针就会变成野指针,再次访问会引发未定义行为。为实现深拷贝,需逐元素赋值,调用自定义类型的赋值运算符,确保指针成员指向不同内存区域。
_start = new T[v.capacity()];
开辟空间后仍可能浅拷贝的原因
_start = new T[v.capacity()];
虽为新 vector
对象开辟了内存空间,但 memcpy
按字节复制对象的二进制内容,对于包含指针成员的自定义类型,仅复制指针的值,不复制指针指向的数据,仍会导致浅拷贝问题。
对于指针成员,memcpy
是否解引用复制
memcpy
对指针成员不进行解引用复制,只是将指针所占据的字节原样复制到目标对象的对应位置,即复制了指针的值(内存地址)。若要进行解引用复制,需手动编写代码,如在类中提供自定义的拷贝构造函数和赋值运算符,对指针指向的数据进行复制。
不同类型使用 memcpy
的情况
- 包含指针成员的自定义类型:使用
memcpy
会导致浅拷贝问题,不能使用,应采用逐元素赋值或自定义拷贝构造函数和赋值运算符实现深拷贝。 - 不包含指针成员且成员均为基本数据类型的自定义类型:使用
memcpy
通常是安全的,因为这些类型的数据可直接按字节复制。 - 包含标准库容器或其他复杂类型的自定义类型:不能使用
memcpy
,因为这些类型内部可能使用了动态内存分配,使用memcpy
会破坏其内部状态。
结论:对自定义类型,必须用拷贝构造函数逐个元素深拷贝,不能用 memcpy 这种浅拷贝。
以下就是模拟实现的vector的全部代码
vector.h代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
namespace NJ {
template<class T>
class vector {
public:
// 定义迭代器类型,方便后续使用
typedef T* iterator;
typedef const T* const_iterator;
// 获取普通迭代器,指向容器起始位置
iterator begin() {
return _start;
}
// 获取普通迭代器,指向容器末尾(最后一个元素的下一个位置)
iterator end() {
return _finish;
}
// 获取常量迭代器,指向容器起始位置,用于常量对象
const_iterator begin()const {
return _start;
}
// 获取常量迭代器,指向容器末尾(最后一个元素的下一个位置),用于常量对象
const_iterator end()const {
return _finish;
}
// 构造函数,创建包含n个元素的vector,元素值为val(默认值为T())
vector(int n, const T& val = T()) {
resize(n, val);
}
// 默认构造函数,初始化三个指针成员为空
vector()
: _start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{}
// 迭代器范围构造函数,将[first, last)范围内的元素插入到vector中
template<class InputIterator>
vector(InputIterator first, InputIterator last) {
while (first != last) {
push_back(*first); // 逐个将范围内元素插入
++first;
}
}
// 拷贝构造函数,深拷贝另一个vector
vector(const vector<T>& v) {
// 分配与v相同容量的内存
_start = new T[v.capacity()];
// 逐个元素复制(不用memcpy,因为memcpy是按字节复制,对于自定义类型(如包含指针的类)会导致浅拷贝,这里逐元素赋值确保深拷贝,避免资源重复释放等问题)
for (size_t i = 0; i < v.size(); i++) {
_start[i] = v._start[i];
}
_finish = _start + v.size(); // 更新finish指针
_endofstorage = _start + v.capacity(); // 更新endofstorage指针
}
// 交换两个vector的成员(指针、大小、容量),高效实现赋值等操作
void swap(vector<T>& v) {
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
// 赋值运算符重载,利用swap实现,确保异常安全(先交换,原资源会在swap的临时对象析构时释放)
vector<T>& operator=(vector<T> v) {
swap(v);
return *this;
}
// 析构函数,释放动态分配的内存
~vector() {
if (_start) {
delete[] _start;
_start = _finish = _endofstorage = nullptr; // 指针置空,避免野指针
}
}
// 预留至少n个元素的空间,若n大于当前容量则扩容
void reserve(size_t n) {
if (n > capacity()) {
size_t sz = size(); // 记录当前元素个数
T* tmp = new T[n]; // 分配新内存
if (_start) {
// 逐个元素复制(不用memcpy原因同上,避免浅拷贝)
for (size_t i = 0; i < sz; i++) {
tmp[i] = _start[i];
}
delete[] _start; // 释放旧内存
}
_start = tmp; // 更新指针
_finish = _start + sz; // 更新finish(元素个数不变)
_endofstorage = _start + n; // 更新endofstorage
}
}
// 调整容器大小为n,若n小于当前size则截断,若n大于当前size则扩容并初始化新元素为val
void resize(size_t n, const T& val = T()) {
if (n < size()) {
_finish = _start + n; // 截断
}
else {
reserve(n); // 先预留空间
while (_finish != _start + n) { // 初始化新元素
*_finish = val;
++_finish;
}
}
}
// 尾部插入元素,调用insert实现
void push_back(const T& x) {
insert(end(), x);
}
// 尾部删除元素,调用erase实现
void pop_back() {
erase(--end());
}
// 获取容器容量(总分配内存可容纳元素个数)
size_t capacity()const {
return _endofstorage - _start;
}
// 获取容器有效元素个数
size_t size()const {
return _finish - _start;
}
// 下标访问,非const对象,带断言检查越界
T& operator[](size_t pos) {
assert(pos < size());
return _start[pos];
}
// 下标访问,const对象,带断言检查越界
const T& operator[](size_t pos)const {
assert(pos < size());
return _start[pos];
}
// 在pos位置前插入元素x,返回插入位置的迭代器
iterator insert(iterator pos, const T& x) {
assert(pos >= _start && pos <= _finish); // 检查pos合法性
if (_finish == _endofstorage) { // 空间不足时扩容
size_t len = pos - _start; // 记录pos相对起始位置的偏移
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2; // 扩容策略(初始4,之后双倍)
reserve(newcapacity); // 扩容
pos = _start + len; // 更新pos(因为内存重新分配,需重新计算位置)
}
iterator end = _finish - 1; // 从末尾开始向前移动元素
while (end >= pos) {
*(end + 1) = *end; // 元素后移
--end;
}
*pos = x; // 插入元素
++_finish; // finish后移
return pos; // 返回插入位置迭代器
}
// 删除pos位置的元素,返回删除位置下一个元素的迭代器
iterator erase(iterator pos) {
assert(pos >= _start && pos < _finish); // 检查pos合法性
iterator it = pos + 1; // 从pos下一个元素开始
while (it != _finish) {
*(it - 1) = *it; // 元素前移覆盖
++it;
}
--_finish; // finish前移
return pos; // 返回删除位置迭代器(指向已删除元素位置,一般后续会更新迭代器)
}
private:
iterator _start = nullptr; // 指向容器起始位置
iterator _finish = nullptr; // 指向容器末尾(最后一个元素的下一个位置)
iterator _endofstorage = nullptr; // 指向容器内存末尾(总分配内存的最后一个位置)
};
}
Test.cpp代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include "vector.h"
#include <iostream>
// 测试默认构造函数和push_back
void TestVector1() {
NJ::vector<int> v; // 默认构造空vector
v.push_back(1); // 尾部插入1
v.push_back(2); // 尾部插入2
v.push_back(3); // 尾部插入3
v.push_back(4); // 尾部插入4
// 遍历输出元素
for (size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << " ";
}
std::cout << std::endl;
}
// 测试带初始值的构造函数
void TestVector2() {
// 构造包含5个元素,每个元素值为10的vector
NJ::vector<int> v(5, 10);
// 遍历输出元素
for (size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << " ";
}
std::cout << std::endl;
}
// 测试拷贝构造函数
void TestVector3() {
NJ::vector<int> v1(3, 5); // 构造v1,包含3个元素,值为5
NJ::vector<int> v2(v1); // 拷贝构造v2
// 遍历输出v2元素
for (size_t i = 0; i < v2.size(); ++i) {
std::cout << v2[i] << " ";
}
std::cout << std::endl;
}
// 测试赋值运算符重载
void TestVector4() {
NJ::vector<int> v1(3, 5); // 构造v1
NJ::vector<int> v2; // 构造空v2
v2 = v1; // 赋值操作
// 遍历输出v2元素
for (size_t i = 0; i < v2.size(); ++i) {
std::cout << v2[i] << " ";
}
std::cout << std::endl;
}
// 测试insert和erase
void TestVector5() {
NJ::vector<int> v; // 空vector
v.push_back(1); // 插入1
v.push_back(2); // 插入2
v.push_back(3); // 插入3
v.push_back(4); // 插入4
auto it = v.begin() + 1; // 迭代器指向1之后的位置(即2)
v.insert(it, 5); // 在该位置插入5
// 遍历输出插入后的元素
for (size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << " ";
}
std::cout << std::endl;
it = v.begin() + 2; // 迭代器指向插入5后的第三个位置(即3原来的位置,现在是5之后的元素)
v.erase(it); // 删除该位置元素
// 遍历输出删除后的元素
for (size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << " ";
}
std::cout << std::endl;
}
int main() {
TestVector1(); // 执行测试1
TestVector2(); // 执行测试2
TestVector3(); // 执行测试3
TestVector4(); // 执行测试4
TestVector5(); // 执行测试5
return 0;
}
五、动态二维数组:vector<vector<int>>
的正确打开方式
5.1 结构解析(以杨辉三角前 5 行为例)
vector<vector<int>>
是一个 二维动态数组,本质是 “容器的容器”:外层 vector
的每个元素本身又是一个 vector
,用于存储一行数据。以杨辉三角前 5 行为例:
初始化与内存结构
vector<vector<int>> vv(5); // 初始化外层vector,包含5个空的内层vector(每行是一个空的vector)
-
初始状态:
外层vector
(记为vv
)有 5 个元素,每个元素是一个空的vector<int>
。每个内层vector
的三个核心指针(_start
、_finish
、_endofstorage
)均为nullptr
,相当于 5 个 “空盒子”,尚未分配任何内存。 -
填充后状态(以第 0、1、2 行为例):
- 第 0 行:
[1]
- 内层
vector
的size=1
(_finish - _start = 1
),capacity≥1
(具体容量由编译器决定,通常初始为 1)。
- 内层
- 第 1 行:
[1, 1]
size=2
,capacity≥2
(可能扩容为 2 或更大,如 GCC 按 2 倍扩容)。
- 第 2 行:
[1, 2, 1]
size=3
,capacity≥3
(若之前容量为 2,插入第 3 个元素时触发扩容,容量变为 4 等)。
- 第 0 行:
关键特性
- 动态性:每行的长度可独立变化,无需预先指定二维数组的列数,适合处理行列数不确定的数据(如杨辉三角每行长度递增)。
- 内存非连续:外层
vector
的每个内层vector
的内存是独立分配的,即每行的元素在内存中连续,但行与行之间的内存地址不一定连续。
5.2 初始化技巧(避免越界)
直接使用 vv[i][j]
访问元素时,若内层 vector
尚未初始化(即第 i
行还没有分配内存),会导致 越界访问(未定义行为)。正确做法是先初始化每行的大小:
for (int i = 0; i < numRows; i++) {
vv[i].resize(i + 1, 1); // 第 i 行设置为 i+1 个元素,初始值为 1
}
细节解析
resize(i + 1, 1)
的作用:- 对第
i
行的内层vector
调用resize
,将其大小设为i + 1
,每个元素初始化为1
(杨辉三角每行首尾为 1,中间后续计算)。 - 若内层
vector
原本为空,resize
会触发内存分配,避免直接访问vv[i][j]
时因该行无内存导致的越界。
- 对第
- 直接访问的风险:
若未初始化第i
行,vv[i]
是一个空的vector
,此时vv[i][j]
会访问不存在的元素,导致程序崩溃或未定义行为(如访问野指针)。
5.3 访问方式(下标 vs 迭代器)
1. 下标访问(vv[i][j]
)
- 适用场景:已知行号和列号的场景(如杨辉三角计算中间元素),直观高效。
- 示例(计算杨辉三角中间元素):
for (int i = 2; i < numRows; i++) { for (int j = 1; j < i; j++) { vv[i][j] = vv[i-1][j-1] + vv[i-1][j]; // 核心逻辑:当前元素等于上一行相邻元素之和 } }
- 逻辑解析:
- 第
i
行(从第 0 行开始)的第j
个元素(j=1
到j=i-1
,首尾为 1 已初始化),等于上一行第j-1
和j
个元素之和。 - 下标访问直接定位到具体元素,时间复杂度为 O (1)(通过指针偏移直接访问)。
- 第
- 逻辑解析:
2. 迭代器访问
- 适用场景:通用算法(如遍历所有元素、使用 STL 算法如
for_each
)。 - 示例(遍历所有元素):
for (auto& row : vv) { // 遍历外层vector,row是内层vector的引用 for (auto num : row) { // 遍历内层vector的元素 cout << num << " "; } cout << endl; }
- 细节:
auto& row : vv
:使用引用避免拷贝整个内层vector
,提高效率。- 迭代器访问通过
begin()
和end()
获取首尾迭代器,适合未知下标或通用逻辑。
- 细节:
六、OJ 实战:用 vector
解决经典问题
6.1 只出现一次的数字(异或法)
问题描述
数组中只有一个数字出现一次,其他数字出现两次,找出该数字。
思路解析
- 异或运算性质:
- 交换律:
a ^ b = b ^ a
- 结合律:
(a ^ b) ^ c = a ^ (b ^ c)
- 自反性:
a ^ a = 0
,0 ^ a = a
- 交换律:
- 核心逻辑:将数组中所有元素依次异或,相同元素异或为 0,最终结果即为只出现一次的数字。
代码详解
class Solution {
public:
int singleNumber(vector<int>& nums) {
int res = 0;
for (int num : nums) { // 范围for遍历,自动获取nums的begin()和end()
res ^= num; // 等价于 res = res ^ num
}
return res;
}
};
- 关键点:
- 范围
for
循环:无需手动管理迭代器,直接遍历vector
中的每个元素,代码简洁。 - 异或累积:初始
res=0
,每次异或一个数,最终res
即为唯一出现的数字(如数组[2,2,1]
,异或后0^2^2^1 = 1
)。
- 范围
6.2 杨辉三角(动态二维数组)
问题描述
生成杨辉三角的前 numRows
行,每行首尾为 1,中间元素为上一行相邻元素之和。
关键点解析
- 二维数组初始化:
- 外层
vector
初始化为numRows
行(vector<vector<int>> triangle(numRows)
),每行是一个空的内层vector
。 - 对每行调用
resize(i+1, 1)
,初始化每行的大小为i+1
,元素初始化为 1(首尾元素固定为 1)。
- 外层
- 中间元素计算:
- 第
i
行第j
个元素(1 ≤ j ≤ i-1
)等于第i-1
行第j-1
和j
个元素之和(利用二维数组的下标访问直接计算)。
- 第
代码详解
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> triangle(numRows); // 外层vector包含numRows个内层vector
for (int i = 0; i < numRows; i++) {
triangle[i].resize(i + 1, 1); // 第i行有i+1个元素,初始化为1(首尾元素)
for (int j = 1; j < i; j++) { // j从1到i-1,计算中间元素
triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j];
}
}
return triangle;
}
};
- 细节注意:
- 当
i=0
或i=1
时,内层循环(j < i
)不执行(如i=0
时,j
从 1 开始,不满足j < 0
),避免越界(第 0 行和第 1 行只有首尾元素,无需计算中间元素)。
- 当
6.3 删除排序数组中的重复项(双指针法)
问题描述
给定排序数组,原地删除重复元素,返回有效元素个数,不使用额外空间。
思路解析
- 双指针法:
- 慢指针
slow
:记录当前有效元素的最后位置(初始为 0)。 - 快指针
fast
:遍历数组,当遇到与nums[slow]
不同的元素时,将该元素赋值给nums[slow+1]
,并移动slow
。
- 慢指针
- 核心逻辑:利用排序数组的特性,重复元素必相邻,通过快慢指针将不重复的元素 “前移”,最终
slow + 1
即为有效元素个数。
代码详解
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0; // 处理空数组
int slow = 0; // 慢指针,指向当前有效元素的最后一个位置
for (int fast = 1; fast < nums.size(); fast++) { // 快指针从第二个元素开始遍历
if (nums[fast] != nums[slow]) { // 发现不同元素
nums[++slow] = nums[fast]; // 慢指针先移动,再赋值(等价于尾插不重复元素)
}
}
return slow + 1; // 有效元素个数为slow+1(索引从0开始)
}
};
- 关键步骤解析:
- 初始化:若数组为空,直接返回 0。
- 快慢指针移动:
- 当
nums[fast]
与nums[slow]
相等时,快指针继续移动(跳过重复元素)。 - 当不相等时,慢指针先自增(指向新的有效位置),再将快指针当前元素赋值给慢指针位置(相当于在有效区域 “添加” 新元素)。
- 当
- 返回结果:慢指针最终指向最后一个有效元素的位置,有效元素个数为
slow + 1
(如数组[1,1,2]
,处理后slow=1
,有效个数为 2)。
总结
- 二维
vector
的核心:每行是独立的vector
,需先初始化每行的大小(如resize
)避免越界。 - 异或法:利用异或的数学性质,在 O (n) 时间和 O (1) 空间内解决唯一元素问题。
- 双指针法:通过快慢指针分工,原地处理排序数组,高效解决重复元素删除问题。
- 下标 vs 迭代器:下标访问适合已知位置的场景,迭代器适合通用遍历和算法,根据需求选择合适方式。
七、常见误区与最佳实践
7.1 误区排雷
-
认为 vector 增容一定是 2 倍
- 真相:VS 是 1.5 倍,GCC 是 2 倍,具体看编译器实现,不要固化思维。
-
用 memcpy 拷贝自定义类型
- 后果:浅拷贝导致资源泄漏,必须用拷贝构造或 STL 算法(如
copy
)。
- 后果:浅拷贝导致资源泄漏,必须用拷贝构造或 STL 算法(如
-
删除元素后不更新迭代器
- 错误:
v.erase(pos); ++it;
(可能跳过元素或访问失效位置),正确写法it = v.erase(pos);
(自动指向下一个有效元素)。
- 错误:
7.2 最佳实践
- 预分配空间:用
reserve(n)
避免多次扩容,尤其在大量尾插时(如读取文件数据到 vector)。 - 优先使用
operator[]
:下标访问比迭代器解引用更简洁,且现代编译器会优化到等价效率。 - 善用
emplace_back
(C++11+):原地构造元素,避免临时对象拷贝,提高效率。v.emplace_back(10); // 等价于push_back(10),但更高效
八、总结:vector 的核心价值
- 连续存储:保证随机访问高效,适合需要快速定位元素的场景。
- 动态扩容:自动管理内存,无需手动处理数组越界,减少代码复杂度。
- 迭代器抽象:让算法(如 find、sort)通用化,解耦数据结构与算法。
掌握 vector 的关键在于理解其底层实现(三指针模型、扩容策略、迭代器本质),并在实践中注意迭代器失效、空间管理等细节。建议通过模拟实现 vector 的核心接口(如构造函数、push_back、reserve)来加深理解,再结合 OJ 题目巩固应用,最终达到 “能用、明理、能扩展” 的 STL 使用境界。