vector 起步与底层:三个指针、迭代器失效的现场,与"为了模板升级内置类型"
我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!
一、vector 第一印象:和 string 高度相似,但少了两个东西
上手 vector 的第一个感受:用法和 string 高度相似 。毕竟 string 底层也是数组、也是动态扩容的顺序表,vector 只是把"字符"换成了任意类型 T。
但有两个东西 string 有、vector 没有:流插入和流提取。
想想其实合理------string 的每个元素是字符,整串打出来有天然的可读语义;vector 的元素类型是模板参数 T,编译器根本不知道你的自定义类型该怎么"流出去"。所以遍历 vector 只能走那三条老路:下标、迭代器、范围 for。
cpp
std::vector<std::string> v;
v.push_back("东神");
v.push_back("小东东");
for (auto e : v) {
std::cout << e << std::endl;
}
还有一个和 string 的细节差异:string 底层是数组但会多开一块空间存 \0 ,vector 不需要------它没有"以什么结尾"的约定,_finish 指到哪就是哪。
二、构造与 push_back 里的临时对象
cpp
std::vector<std::string> v;
std::vector<int> c1{ 1, 2, 3 };
push_back 的签名是 void push_back(const value_type& val),所以传 "张三" 时,走的是类型转换生成临时对象 的路:const char* → 临时 string → 挂到 const 引用上。同理 v2.push_back(A(2,2)) 是显式构造匿名对象,v2.push_back({3,3}) 是多参数的隐式转换------本质都是先生成临时对象,再进容器。
而匿名对象的本质就是生成一个临时对象,出了这一行代码就会死亡。
范围 for 那条伏笔在这里也对上了:范围 for 的本质就是将 *迭代器 赋值给 e ------auto 识别类型,再用拷贝构造出 e。一个是构造,一个是临时后拷贝,两码事。
三、底层结构:string 是三个指针,vector 也是三个指针
一般顺序表(C 语言版)的底层是 _size + _capacity 加一个定长/动态数组,两个成员。但 string 和 vector 的底层是三个指针:
_start --------- 指向数据头
_finish --------- 指向有效数据的尾(size 的位置)
_endofstorage --------- 指向容量尾(capacity 的位置)
所以 size() == _finish - _start、capacity() == _endofstorage - _start,全靠指针相减算出来。这个实现"有点像队列"------头、尾、capacity 三个游标的样子。
这也顺便解释了为什么拷贝构造要 size_t i = a._finish - a._start 这样先算长度、_start = new T[j] 再逐个搬------三个指针都要重新锚定到新空间。
四、迭代器失效:扩容那一刻,pos 变野指针
这是本篇的重点。场景:你在 erase/insert 里拿到了一个指向中间元素的迭代器 pos,然后一次扩容发生了------新开一块空间,数据整体搬家,旧空间释放。
扩容前:_start ──────────── 旧空间,pos 指向其中
扩容后:_start ────────► 新空间,旧空间已还给操作系统
pos 还指着旧空间,它成了野指针------这就是"迭代器失效"。
把条件说全,使传入指针失效有两个前置条件:
- 传入指针参数------函数拿到了你的迭代器;
- 要扩容------扩容后整体位置改变。
两个凑齐,传入的迭代器就废了。find 返回的指针也可能是失效的,因为空间可能在你 find 之后被重置了。所以规矩是:传入迭代器参数、函数内部又可能扩容的,就要返回其修正值 ------调用方拿返回值更新自己的迭代器,it = v.erase(it) 这个写法的由来就在这。
这就是"改变量内部数组的结构后,旧迭代器全部不可信"的本质:vector 的迭代器就是裸指针,指针指着的空间被换掉了。
五、size_t h(100); ------为了模板,C++ 给内置类型补了构造函数
底层1 笔记里那个 size_t h(100); 引出一段很有意思的推理:
模板之后,对于缺省参数和默认值,C++ 就不得不考虑内置类型的构造函数。所以:
cpp
int a = 110; // 赋值初始化
int a(110); // 构造式初始化------C++ 对内置类型进行了升级!
以前 C 语言里 int 哪来的构造函数?但模板泛型化之后,代码要写成对 T 一视同仁:T x; 得保证无论 T 是自定义类型还是 int 都能有一个"默认出来的值"。这样做就是为了适应模板------调默认构造,一般就是往 0 去靠近。内置类型在 C++ 里被补上了构造/析构的语法外衣,这是语言层面为了泛型做的让步。
六、写 vector 拷贝构造的三个提醒
笔记里给手写 vector 立了几条规矩:
- 有资源,就要写深拷贝,否则默认生成的拷贝函数是浅拷贝------一个函数结束时释放了这个空间,后面二次析构直接崩。
- 建议给成员定义缺省值 (C++11 的声明缺省值),针对于初始化列表------三个指针出生时就是
nullptr/0,忘了初始化也不至于带随机值上路。 - 构造、拷贝构造、赋值重载是三个完全不同的函数 :
T a = 10和a = 10调用的函数截然不同------前者是构造(含隐式转换),后者是赋值重载。
还有一句容易混的:"数组自定义 和嵌套类 完全不是一个东西"------数组元素是自定义类型,只是逐个调用构造即可;嵌套类是成员 private 里声明了自定义类类型的那种,两者在拷贝、析构上的行为完全不同层级的深浅拷贝问题,下一篇展开。
收个尾
vector 这一章的开局三件事:
- 底层三指针,size/capacity 全是指针差值;
- 迭代器失效 = 扩容换空间 + 裸指针指着旧空间,所以可能扩容的接口都要返回修正值;
- 内置类型的"构造函数"是给模板铺路的 ------
int a(110)不是语法糖,是泛型时代的必需品。
下一篇进 memcpy 的坑:为什么 vector<string> 的扩容拷贝用 memcpy 会出大事。