C++ vector使用技巧与迭代器失效详解
目录
本质:std::vector 是一个封装了动态大小数组的顺序容器,它可以存储任意类型的元素,并且能够在运行时动态地调整大小。如果是元素很少不会向内存申请空间,直接用存放,如果元素多就会像内存申请空间(有很多底层和string相似,可以看看我发的string)。
以下是按照标准Markdown格式整理的表格结构,内容基于C++ vector的成员函数分类:
一,成员函数分类表
| 类别 | 函数名称 | 功能描述 |
|---|---|---|
| 构造/析构 | vector() | 构造空的vector容器 |
| ~vector() | 析构容器并释放内存 | |
| 赋值 | operator= | 用另一个vector的内容替换当前内容 |
| 迭代器 | begin() | 返回指向第一个元素的迭代器 |
| end() | 返回指向末尾(尾后)的迭代器 | |
| rbegin() | 返回指向反向起始的反向迭代器 | |
| rend() | 返回指向反向末尾的反向迭代器 | |
| cbegin() | 返回const版本的起始迭代器 | |
| cend() | 返回const版本的末尾迭代器 | |
| crbegin() | 返回const反向起始迭代器 | |
| crend() | 返回const反向末尾迭代器 | |
| 容量 | size() | 返回当前元素数量 |
| max_size() | 返回可容纳的最大元素数量 | |
| resize(n) | 调整容器大小为n个元素 | |
| capacity() | 返回当前分配的存储空间大小 | |
| empty() | 检查容器 是否为空 | |
| reserve(n) | 预分配至少能容纳n个元素的存储空间 | |
| shrink_to_fit() | 请求移除未使用的容量 | |
| 元素访问 | operator[] | 通过下标访问元素(无边界检查) |
| at() | 通过下标访问元素(带边界检查) | |
| front() | 访问第一个元素 | |
| back() | 访问最后一个元素 | |
| data() | 返回指向底层数组的指针 | |
| 修改器 | assign() | 用指定值替换容器内容 |
| push_back() | 在末尾添加元素 | |
| pop_back() | 移除末尾元素 | |
| insert() | 在指定位置插入元素 | |
| erase() | 移除指定位置的元素 | |
| swap() | 交换两个容器的内容 | |
| clear() | 清空容器内容 | |
| emplace() | 在指定位置原位构造元素 | |
| emplace_back() | 在末尾原位构造元素 |
表格说明
- 表格按功能类别分组,便于快速查找特定操作
- 所有成员函数均为public访问权限
- 函数命名遵循C++标准库惯例,括号()表示函数调用
- 元素访问类函数区分了边界检查行为(at() vs operator[])
二,使用细节(常用的)
1. 构造和遍历方式
a.构造:
C++98时候有4种构造方式:1,直接构造(就是不传参).2,传一个n和一个你要的元素x,会构造出n个x(底层和resize函数差不多).3,迭代器构造(实际底层是指针用迭代器封装).4,拷贝构造。
C++11在基础上添加了两种,但是我只是介绍常用的:5,传{}构造(std::vector<int> i2({ 1,2,3,4,5,6,7 });)->底层是vector (initializer_list<value_type> il, const allocator_type& alloc = allocator_type());
现在介绍一下initializer_list<value_type> 是 C++ 标准库中的一个模板类,它提供了一种方便的方式来传递和使用初始化列表, 是一种轻量级的容器类型,它允许使用花括号 {} 来初始化对象,并且可以在函数参数、构造函数等地方使用初始化列表,也支持迭代器。
那现在开始看看这个5个构造:
//vector()
std::vector<int> i;
//vector(int n, const T& value = T())
std::vector<int> i1(10,1);
//vector(InputIterator first, InputIterator last)
std::vector<int> i3(i2.begin(),i2.end());
//vector(const vector<T>&v)
std::vector<int> i4(i3);
//vector(std::initializer_list<T> il, const T& value = T())
std::vector<int> i2({ 1,2,3,4,5,6,7 });
//vector<T>& operator= (const vector<T>& v)
i = i4;
b.遍历:
和string的遍历没有什么太大变化,但是我还要讲讲:
下标遍历:
for (size_t j = 0; j < i.size(); j++)
{
std::cout << i[j] << " ";
}
while迭代器:
std::vector<int>::const_iterator x = i.begin();
while (x != i.end())
{
std::cout << *x << " ";
x++;
}
范围for:
for (auto& e : s)
{
std::cout << e << " ";
}
std::cout << std::endl;
2. string和vector<char>
string和vector<char>底层没有什么区别,都是用char数组存数据,但是string比vector<char>比较方便,比vector<char>多一些接口函数方便处理字符串。
3. push_back和emplace_back()对比
基本功能push_back() 将一个已存在的元素添加到 vector 末尾。emplace_back() 直接在 vector 末尾构造新元素,无需预先创建对象。
参数传递push_back() 接受已构造的对象作为参数,自定义类型需先实例化再传入。emplace_back() 接受构造对象所需的参数列表,直接在容器内构造对象。
性能表现push_back() 对基本类型性能接近 emplace_back();对自定义类型可能触发复制/移动操作,大对象开销较高。//可以传{},不能直接传值。emplace_back() 直接在目标内存构造对象,避免临时对象的复制或移动,尤其对自定义类型更高效。//不能传{}插入,直接传值就可以了。
std::vector<std::pair<int, int>> s;
s.push_back({ 1, 2 });
s.emplace_back(1, 2);
//如果反过来会报错。
异常安全性
两者均保证异常发生时容器状态不变。push_back() 在元素已存在时触发异常;emplace_back() 在构造过程中发生异常时对象未完全创建。
适用场景push_back() 适用于已有现成对象需插入容器的场景。emplace_back() 适合直接构造新对象并插入,尤其是需避免冗余复制/移动操作的自定义类型。
三,迭代器失效
1. 迭代器失效的原因
迭代器失效主要是由于 vector 的内存布局发生改变所导致的。vector 中的元素在内存中是连续存储的,当对 vector 进行某些操作时,可能会导致内存重新分配或者元素位置移动,从而使得原来的迭代器不再指向正确的元素。
2. 迭代器失效的具体场景
插入元素导致迭代器失效
- 尾部插入(
push_back或emplace_back)- 原理:当
vector的容量不足时,插入元素会触发内存重新分配。新的内存空间会被分配,原有元素会被复制或移动到新的内存中,原来的迭代器就会失效。 - 示例:
- 原理:当
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
// 插入元素,可能导致内存重新分配
vec.push_back(4);
// 此时 it 可能已经失效
std::cout << *it << std::endl;
- 中间插入(
insert)- 原理:在
vector中间插入元素时,插入位置之后的所有元素都会向后移动,这会导致插入位置及之后的迭代器失效。 - 示例:
- 原理:在
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin() + 1;
// 在 it 位置插入元素
vec.insert(it, 4);
// 此时 it 及之后的迭代器失效
std::cout << *it << std::endl;
删除元素导致迭代器失效
- 尾部删除(
pop_back)- 原理:删除
vector的最后一个元素,会使指向最后一个元素的迭代器失效。 - 示例:
- 原理:删除
std::vector<int> vec = {1, 2, 3};
auto it = vec.end() - 1;
// 删除最后一个元素
vec.pop_back();
// 此时 it 失效
std::cout << *it << std::endl;
- 中间删除(
erase)- 原理:删除
vector中间的元素时,删除位置之后的所有元素都会向前移动,这会导致删除位置及之后的迭代器失效。 - 示例:
- 原理:删除
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin() + 1;
// 删除 it 位置的元素
vec.erase(it);
// 此时 it 及之后的迭代器失效
std::cout << *it << std::endl;
3. 避免迭代器失效的方法
插入元素时
- 记录插入位置:在插入元素之前,记录插入位置的偏移量,插入后重新计算迭代器。
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin() + 1;
size_t offset = it - vec.begin();
vec.insert(vec.begin() + offset, 4);
it = vec.begin() + offset;
// 重新计算迭代器
std::cout << *it << std::endl;
删除元素时
- 使用
erase的返回值:erase方法会返回删除元素之后的下一个有效迭代器,可以直接使用这个返回值更新迭代器。
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin() + 1;
it = vec.erase(it);
// 使用 erase 的返回值更新迭代器
std::cout << *it << std::endl;
4. 总结
在使用 vector 的迭代器时,要时刻注意迭代器失效的问题。在进行插入或删除操作后,及时更新迭代器,避免使用失效的迭代器,以确保程序的正确性和稳定性。
更多推荐
所有评论(0)