从零了解Vector详细解析
与string的衔接
顺序表 vector ,是一个标准的模板

vector 是一个双参数模板:
-
第一个参数:
T→ 容器里面存的数据类型(int/Channel) -
第二个参数:
Alloc→ 内存分配器,默认值就是allocator<T>
2. allocator<T> 是什么
std::allocator 是 C++ STL 默认内存管理器。
vector 需要堆内存存放元素;
allocator的工作就是:向操作系统申请内存、释放内存。
它封装了底层的:
-
new/delete -
malloc/free
它干两件核心工作
-
分配原始内存:只开辟一块裸内存,不调用构造函数
-
释放内存:归还内存,不调用析构函数
区分两个概念
分配内存:找一块空地
构造对象:在这块空地上调用类的构造函数
emplace_back:allocator 先分配内存 → 然后原地构造对象,正好就是你之前代码_channels.emplace_back(wfd,subid)的完整流程。

而 string 是一个具体类型,在库中string本质为 basic_string <char> 的别名
vector是类模板,类模板必须显示实例化
std::vector<T> —— 这是模板(图纸),不是一个可用的类型。
显式实例化:给模板参数传值,造出一个真实的类(成品)
std::vector<int> v1; // 显式实例化,得到一个实实在在的类
std::vector<int> v2(10,1);
std::vector<int> v3(v2.begin(),v2.end());
下面这个写法根本不能用来定义变量
为没有显式实例化,就是只写了模板名字,没有给尖括号传类型参数
std::vector; // 报错!!没有填模板参数T
遍历vector的方法:
用迭代器:
vector<int>::iterator it = v3.begin();
范围for:
for(auto e;v3)
{ //... }
vector不会缩容
而string会缩容:重新分配更小的内存 + 拷贝数据 + 释放旧的大内存
(操作系统的堆内存管理规则是:分配的内存块必须整体释放,无法拆分归还)
vector::resize

把vector的数据个数改为 n(size也会变)
<size:删除数据
>size:插入数据,空间不够就扩容
vector::operator[]
operator[] 就是下标运算符重载

v3[i] 编译器等价翻译成:v3.operator[](i)
1.两个重载版本
① 非 const 版本 —— 可读、可修改元素
int &a = v3[0];
a = 666; //可以修改容器里面的对象
返回值:引用 reference → int&
② const 版本(容器被 const 修饰时调用),只读
const std::vector<int>& b = v3;
const int& c = b[0];
// c = 999; //报错,不能修改
2.operator [] 最核心特点:不做越界检查
std::vector<int> v{10,20,30};
v[100] = 999; //下标100远远超出范围
编译不会报错,运行时不会抛出异常,直接访问非法内存 → 未定义行为 (UB),程序可能随机崩溃、乱改内存。
这是和
.at()的最大区别
| 方法 | 越界检查 | 越界行为 |
| vec[i]operator[] | 无检查 | 未定义行为,危险 |
| vec.at(i) | 边界检查 | 抛出 std::out_of_range 异常,安全 |
示例对比
v.at(100); //越界 →抛异常,可以try‑catch捕获
v[100]; //越界 →直接野内存访问,无法捕获
3.返回引用
std::vector<int> v = {1,2,3};int x = v[0]; // 值拷贝,x是副本,改x不会影响vint &y = v[0]; // 拿到容器内部元素的引用,改y就改v内部元素
y = 99;// v[0] 变成99
4.迭代器 / 引用失效大坑(网络编程必踩坑)
当你调用 push_back / emplace_back 触发 vector 扩容
之前用
operator[]获取到的引用Channel& ch = _channels[0];直接失效!变成野引用,再使用就是 UB 崩溃。
例子:
std::vector<int> v{1,2,3};int &ref = v[0];
v.reserve(100); //扩容!内存搬家!
ref = 99; //野引用!未定义行为!
5.底层原理(vector 内存连续)
vector 的元素在内存一块连续数组。operator[] 底层实现逻辑简化:
//伪代码
T& operator[](size_t pos)
{
return *( _start + pos );
}
_start 指向数组首地址,下标就是指针偏移。
所以
vec[2]和*(vec.begin()+2)完全等价。
6.什么时候用 [],什么时候用 at ()
-
你已经手动保证下标一定合法 → 用
[],速度更快,无额外检查开销(循环遍历 i 从 0 到 size ()-1)
for(int i=0;i<_channels.size();i++)
{
auto &ch = _channels[i]; //安全,i一定合法
}
-
下标来自外部输入、不确定是否合法 → 用
.at(),开启越界保护
vector::insert

insert:在指定迭代器位置,插入一个 / 一批元素。
注意:vector 内存连续,插入中间位置,后面所有元素向后移位,效率低 O (n)
重载 1:插入 n 个相同的值
iterator insert(iterator pos, size_type count, const T& value);
std::vector<int> v = {1,2,3};
v.insert(v.begin(), 3, 88);// 在最前面插入3个88// v: 88,88,88,1,2,3
重载 2:插入一段区间 [first, last)
template<class InputIt>
iterator insert(iterator pos, InputIt first, InputIt last);
std::vector<int> a = {1,2,3};
std::vector<int> b = {100,200};// 在a的尾部,插入b的全部元素
a.insert(a.end(), b.begin(), b.end());// a: 1,2,3,100,200
重载 3:初始化列表 C++11
v.insert(v.begin(), {5,6,7});
返回值极其重要(大坑!迭代器失效)
vector 一旦在中间insert,有可能触发扩容,所有旧迭代器全部失效!
std::vector<int> v = {1,2,3};auto it = v.begin()+1;
v.insert(it, 99); //插入后,it失效!!不能再用it!// 正确做法:用insert返回的新迭代器
it = v.insert(v.begin()+1, 99);
不要保存旧迭代器;
insert返回新迭代器,赋值回去。
vector::erase

erase 的作用:删除 vector 中一个或者一段区间的元素
注意: vector 内存连续,删除中间元素后,后面所有元素必须向前移动补齐空位,时间复杂度 O (n);并且会造成迭代器失效,这是最高频的坑
重载 1:删除单个元素(传迭代器)
iterator erase(iterator pos);
重载 2:删除一段区间,左闭右开
[first, last)
iterator erase(iterator first, iterator last);
返回值:返回被删除元素的下一个位置的新迭代器
最经典坑:一边遍历一边删除
错误代码(失效崩溃)
std::vector<int> v = {1, 2, 2, 3};
for(auto it = v.begin(); it != v.end(); it++)
{
if(*it == 2)
{
v.erase(it); // erase后it立刻失效!循环下一次it++直接崩溃
}
}
正确写法(利用 erase 返回值)
for(auto it = v.begin(); it != v.end(); )
{
if(*it == 2)
{
it = v.erase(it); //接收返回的有效迭代器,不做it++
}
else
{
++it;
}
}
原理:erase 返回删除点后面合法的迭代器,不需要再 it++。
vector::push_back

在 vector 的尾部追加一个元素
// 范围for
for (auto e : v2)
{
cout << e << endl;
}
//其中 e 得到的是v2中所存的类型的深拷贝,而v2类型为string,拷贝效率低 ——> 用引用
for (const auto &e : v2)
for循环
// 二维数组,如4*5的
vector<int> v(5, 1);
vector<vector<int>> v2(4, v);
for (auto e : v2)
{
for (auto e1 : e)
{
cout << e1 << " ";
}
cout << endl;
}

简易版 vector 的底层模型(STL 源码思路,简化掉分配器 allocator)
template <class T>
class vector
{
private:
T *_a;
size_t _size;
size_t _capacity;
};
-
_a
_a 是一个指针,类型:T*
_a 保存堆上那块连续数组内存的起始地址。
-
vector 的所有元素,并不存在栈上;而是在堆 (heap) 上面开辟一块连续的内存存放。
-
_a 就是指向这块堆数组第一个元素的指针。
-
三个成员变量一一对应含义
| 成员 | 含义 |
| T* _a | 内存起始指针,堆数组首地址 |
| size_t _size | 当前元素个数。你能访问的有效元素数量。v.size() 返回的值 |
| size_t _capacity | 已经分配的内存总容量。这块堆内存一共可以放下多少个 T 对象。v.capacity() |
内存布局示意图:
堆内存: [ T0 ][ T1 ][ T2 ][ T3 ][ 空闲 ][ 空闲 ]
↑
_a
_size = 3 //有效元素0,1,2
_capacity = 6 //整块内存一共能存6个元素
vector 下标运算符重载
//可读可写,非const对象调用
T& operator[](int i)
{
return _a[i];
}
//只读版本,const对象调用,不能修改元素
const T& operator[](int i) const
{
return _a[i];
}
下标运算符 operator[] 底层怎么实现
依靠 _a 指针偏移
T& operator[](size_t pos)
{
return _a[pos]; //等价于 *( _a + pos )
}
_a+pos:指针向后偏移 pos 个 T 类型的距离,找到对应元素。 这也就是为什么 vector 下标访问速度极快。
push_back 扩容时 _a 发生了什么
-
判断:
_size == _capacity→ 内存满了,必须扩容 -
开辟一块更大的新堆内存,得到新指针
T* new_a -
把
_a指向的旧内存所有元素拷贝到new_a -
释放旧的 _a 指向的堆内存
-
将
_a = new_a;,让指针指向新内存 -
_capacity更新为新容量 -
在尾部放入新元素,
_size++
扩容后旧的
_a内存被释放! 所以之前保存的引用、迭代器(本质就是基于旧_a的指针)全部失效。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)