目录
[2.2.1 initializer_list { } 构造](#2.2.1 initializer_list { } 构造)
[2.2.3.1 迭代器区间构造 和 n个val构造 的冲突](#2.2.3.1 迭代器区间构造 和 n个val构造 的冲突)
[3.insert (迭代器失效)](#3.insert (迭代器失效))
[4.erase (迭代器失效)](#4.erase (迭代器失效))
[4.2迭代器失效的检查:对比vs平台(强制检查) 和 g++平台(不检查)](#4.2迭代器失效的检查:对比vs平台(强制检查) 和 g++平台(不检查))
1.部分vector源码解析
在实现vector之前,我们先看看它的源码。很大部分我们都看不懂,直接看是看不懂的。所以主要看关键词和模块的功能,不追求每个都看懂。
看,vector里没啥东西,就单独包了些头文件。

扫一眼你就能发现,最重要的肯定是我画圈的两个头文件 。
其实最重要的是 stl_vector.h,最后一个头文件是用来实现vector<bool>的类型。
vector<bool>是 vector特化出来的类,是针对存储bool值的特化。这个类型单独提供一些接口,因为存bool值其实是用来标记的


接下来看源码。看不懂的不用看,只需要抓住主线去看:


第一张图是 typedef ,抓住主要几个核心的:
T value_type value_type* iterator size_t size_type
结合这两张图,我们就找到了vector底层的三个指针变量,看名字大概就能知道他们的作用,
我们猜测这就是核心的成员
它的迭代器是原生指针实现的。
接下来看它的构造函数:

这一块出现了很多 看似学过,但学的不多的函数,上面的几个简单函数还好,主要是中间的构造函数。
我们先了解第一个:通过查找源码发现以下:



如图,十分明显,这个最简单的构造和begin,end,capacity接口告诉我们,我们的猜测是正确的。

那这就是他们的用途,分别指向三个关键地方。
我们找找它的push_back,帮助我们理解源码:


下图中的 尾插 操作涉及 construct 函数,其实这个函数的作用(上图)是:利用定位new给finish位置初始化,因为底层是预先分配的原始内存,未初始化,没初始化那里就是随机值,直接尾插会导致未定义行为,必须利用构造函数初始化。
else,空间不足,肯定要扩容,那我们看看 inser_aux 的源码

它的参数是在pos位置插入x,不单单是尾插,那猜测insert也是调用这个函数。
为什么它还在检测空间是否足够?原因正如我们猜测:

insert也调用了insert_aux

对于insert_aux,可能有人有疑问,为什么这里要先把最后一个数据往后先构造,再赋值?
直接把 pos+1位置开始全部往后赋值一位,然后插入x不就行了吗?
原因一样,后面的空间都是随机值。如果是int这样的内置类型没事,但如果是string等,直接把最后一个数据往后赋值,可能导致未定义,因为是随机值,所以必须构造初始化,确保不会出问题。
2.vector底层实现
上面对很对类型进行了typedef,我们就不这么麻烦了,typedef几个关键的就行。
那么首先实现它的类:
namespace bit
{
template<class T>
class vector
{
public:
//typedef T* iterator;
using iterator = T*;
using const_iterator = const T*;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
}
}
看代码。我用 using 代替了 typedef
using 可以展开命名空间,也可以用来typedef using在这部分和typedef功能一样,不过在C++11有更强大的功能,后面再了解
2.0常用简单接口
1.capacity()

2.size()

3.empty()
_finish 和 _start 相同,则为空

4.clear()

2.1迭代器接口
前面源码解析时了解过,vector底层迭代器其实是指针,那我们直接把常用的迭代器接口实现:
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
const_iterator 和 iterator 都实现了,十分简单,string中也讲过类似。
2.2构造函数
vecotr()
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{}
构造,这是最简单的构造。
2.2.1 initializer_list { } 构造

我们学过,库vector可以这样构造,所以我们也实现一个:
思路:开一片和括号内size()大小一样的空间,然后push_back 结合 范围for 遍历拷贝
具体思路参考 目录 6.拷贝构造

本质上{1,2,3,4}会被转化为initializer_list<int>的临时对象,然后调用构造函数完成vector的初始化
2.2.2迭代器区间的构造

如图,迭代器不一定是vector的,不一定适合+,-,但迭代器一定适合++,所以用++的方式遍历赋值。
2.2.3n个val的构造

2.2.3.1 迭代器区间构造 和 n个val构造 的冲突

两个int参数完全匹配下面的 InputIterator 类型,虽然看似是迭代器类型,但是没指定一定是迭代器,也可以是别的,比如int
解决办法:

第一个参数传 10u ,确定传的是 usigned int (size_t)
哪怕这样:
都没事,因为他是两个不同的参数,符合一个,可以将就上面的模板。而下面的模板是双参数类型是且类型一致的,一个int一个char,只能将就上面的模板。
库里面是重载了一个int版本的来暂时解决这个问题:

源码如图。不过没有根本解决问题。
2.3reserve
思路:如果 扩容 n 大于 capacity ,才会扩容。本质是深拷贝一次。直接 new 一片空间给tmp,然后把原空间的值拷贝到tmp,最后再销毁旧空间。这就是深拷贝的基本思路。
代码:

修正1
不过这是错误的写法:
因为原本的size()是 原空间上的 _finish - _start ,第一次调用size()时还是正常的,但是第二次调用就出问题了:因为原_start已经被修改,此时调用size()就出问题,_finish还指向旧空间,_start已经指向新空间了,求不出正确的size(),从而导致错误

所以解决办法:在调用前存储一次size,同时这样也可以避免多次调用size()函数。
void reserve(size_t n)
{
//存储一遍size()
size_t sz = size();
if (n > capacity())
{
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
2.4push_back
思路:和string一样,首先判断空间是否满了,不满就直接插入。满了就扩容再插入。
代码:

2.5Print()
库vector是没有实现<< 和 >> 的,不能直接输入输出,需要我们自己写 Print 函数 思路:迭代器,范围for,利用[ ]遍历输出(麻烦,不推荐)

2.6析构函数
思路:把空间释放,然后指针全部置空,这片空间的起始地址是_start指向,而delete[ ]的作用是:自动释放该地址对应的整块连续内存

2.7pop_back
思路:这是尾删 ,如果不为空,就执行删除。empty() 在上面 2.0 的第3个函数,十分简单

2.8resize
2.8.1resize中对参数的理解

源码讲过,size_type 就是 size_t , value_type 就是 T 所以:

看图。缺省值一般是这三种 。resize考虑到 T 的类型可能不一定是字面量常量,也可能是类类型 ,**而匿名对象可以适配任意类型(包括内置类型和类类型)**于是就用匿名对象作为缺省值。
难道内置类型也有匿名对象?
还真有 ,C++对内置类型升级, 内置类型也有默认构造,析构(不做任何事情)。
因为有模板,为了泛型编程,模板里可能是类类型,内置类型,为了让内置类型适配类类型,C++对内置类型进行了上面的特殊处理

2.8.2resize
思路:n 比 size 大就先扩容到 n (不用检查是否大于capacity,reserve会检查,大自动扩,小没变化),把没初始化数据的地方初始化为 T() ,n 比 size 小就截断数据到n

2.9swap函数
string讲过类似的

3.insert (迭代器失效)
思路:检查pos合法性,空间满了就扩容,然后在pos位置插入x ,需要把pos+1到_finish-1的数据往后挪动一位。从后往前挪,避免数据覆盖。

别看这就写完了,其实还存在十分巨大的问题:迭代器失效------野指针
3.1迭代器失效------野指针(具体看迭代器类型)
不扩容,就能正常插入数据,但是一旦扩容,reserve会把三个底层指针修改,但是没修改pos指针 这就导致pos指向旧空间:

这会导致什么问题?pos 到 end(_finish - 1) 的数据挪动出错,因为pos和end不在一片空间 ,而且对pos的操作都会导致野指针
解决办法是:保存 pos到旧空间_start的相对长度len ,然后让pos指向新空间的同样相对位置_start+len :

void insert(iterator pos,const T& x)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage)
{
//扩容后,pos还指向旧空间,所以先记录相对长度
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
}
别看已经处理了迭代器失效,但没处理完:

内部不会失效了,但是外部的迭代器,实参不受形参改变而改变,使用过一次之后还是会失效
明明不扩容的时候它不会失效,为什么使用过就失效?
因为不确定它会不会扩容 。发生扩容就是大坑,所以是否扩容,都认为失效。insert以后,迭代器it就不能使用了。
那形参改成引用 & ,不就能改变实参了吗?
库里面其实没有这么实现。因为,你传的实参可能是一个表达式 ,比如v.begin(),it+3 ,它们的结果是临时对象,具有常性 。这些不会被识别为非const迭代器,导致编译不通过。
那再用const修饰形参?
也不能 。加了const修饰 ,那里面的pos就不能修改了 ,这就陷入了死循环。
在这部分,它是无解的 ,只能去小心。
下面会和erase统一讲解解决办法。
4.erase (迭代器失效)
思路:先检查pos位置是否合法。如果合法,那就把pos+1到_finish-1 的数据都往前挪一位。从前往后挪,避免值被覆盖。

那现在来判断一下,erase是否有迭代器失效?
4.1迭代器失效

答案是失效 。删除其他元素都没事,删除最后一个元素后会失效,删除后,_finish--,此时pos指向随机值。

实践中,我们可能不抹除数据,但也有编译器可能会抹除数据,导致_finish指向的数据是随机值,it 访问的就是随机值,失效。
因为不知道它是否指向最后一个数据,所以默认迭代器使用一次就失效。
我们用一个小题目检验这个知识:
4.2迭代器失效的检查:对比vs平台(强制检查) 和 g++平台(不检查)


编译出错。vs下严禁使用失效迭代器 ,它连结果都不会给你编出来 。这是vs的强制检查(介绍在下面)

删除2,数据往前挪,it指向3,按理来说迭代器不失效。但是vs编译器对迭代器进行了强制检查 :使用过后就被标记"失效状态"

这是vs平台 ,我们看看g++平台:

答案正确,能运行,不代表代码正确 !只是编译器没有进行强制检查,将错就错。 需要多组样例输入,检测各种可能性。
5.insert和erase迭代器失效的解决办法
insert和erase的迭代器失效怎么解决?我们参考库vector的方法:

指向新插入元素中第一个元素



指向删除元素的下一个元素位置
erase删除元素后,后面的数据会往前补齐,所以返回pos位置就是返回下一个位置的迭代器

insert同理:
iterator insert(iterator pos,const T& x)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage)
{
//扩容后,pos还指向旧空间,所以先记录相对长度
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
//内部自己更新了pos,只需要返回,这样就能修改实参
return pos;
}
既然上面说了形参修改不了实参,那返回形参,让实参接收形参,就能修改。
6.拷贝构造函数(传统,现代)
默认的浅拷贝无法实现我们的需求,我们自己实现深拷贝的拷贝构造:
首先,拷贝构造是特殊的构造,它也要走初始化列表,多个构造就要显式写多个初始化列表,麻烦,不如全部构造都不写初始化列表,直接在底层给缺省值,更简便:

拷贝构造的思路:举例子 v2(v1),v2需要接收v1的数据个数,空间至少是size()大小,不规定是capacity()。
那就需要深拷贝一个v1的大小的空间,我们以capacity()大小为例,然后用push_back结合范围for把v1的数据遍历拷贝到v2上

这就简单实现完成,不过当前代码还存在一点缺陷:如果T是int还好,但如果是string,或者vector等,范围for进行深拷贝的代价太大了
所以建议把auto 改为 const auto& 一是避免权限放大(这里只是遍历拷贝,不需要修改值,建议加上const),二是减少深拷贝,提高效率

6.1现代写法
现代写法的核心不是效率的提升,而是**"让别人帮我做事,我来获取他的成果":**

利用迭代器区间构造一个与 v 一样的空间 tmp,然后交换一下双方资源,"获取tmp的成果"
6.2更现代的写法
了解即可

7.operator(传统,现代)
思路:v1=v3 首先排除自身赋值的可能,提高效率,然后清理*this的数据(避免尾插到旧数据后),开辟空间把v3数据拷贝到*this,最后返回左操作数的引用,也就是*this,因为可能连续赋值。 v0=v1=v3

7.1现代写法
利用swap函数,交换双方资源,tmp在传参过程调用拷贝构造,已经和v3的元素一样

追求极简,忽略了自我赋值的可能,因为极少出现这种情况。
8.深度理解资源管理------更深层次的浅拷贝
看这个:

它能正常打印:

但是当插入第五个string时,就出问题了:

首先能发现,一定是和扩容有关 。因为前四个都不扩容,第五个会扩容。
为什么会出现这些生僻字,乱码 ?因为读取了随机值。

如图,delete[ ] 还未执行时,执行了 memcpy 把_start的值拷贝给给了tmp,然后看下一步:

当执行完delete[ ] _start ,tmp的值被修改了 ,这说明什么?这说明memcpy实际上是浅拷贝,导致删除_start牵连了tmp
但是为什么,明明tmp和_start的地址不一样呀,为什么会受到牵连?
这设计一个更深层次的浅拷贝的问题:

如图,内存地址确实不一样 。但是经过memcpy,一个字节一个字节的拷贝(二进制拷贝) 后:

它们string对象 内部的指针 指向同一片区域,_start删除后,tmp的string对象的_str都指向了随机值,就导致出现乱码,生僻字。这就是更深层次的浅拷贝
库vector里怎么解决的?库里的用了更复杂的技术比如 类型萃取 我们就不那么复杂,简单实现一下深拷贝即可:

我们这用for循环遍历赋值,tmp[ i ] 和_start[ i ] 其实就是string对象,string对象的赋值会调用他们string库里的赋值重载,进行深拷贝
但这样效率不高。每次调用string赋值重载啊会进行一次深拷贝,这里会进行多次深拷贝。

如图,这样进行深拷贝。
更优的写法:

这就是更优的写法,不需要深拷贝 ,利用swap函数,直接交换他们的底层指针指向的资源:

感谢支持!