C++STL:熟悉vector的底层实现,部分源码解析,迭代器失效和深层次浅拷贝

目录

1.部分vector源码解析

2.vector底层实现

2.0常用简单接口

1.capacity()

2.size()

3.empty()

4.clear()

2.1迭代器接口

2.2构造函数

[2.2.1 initializer_list { } 构造](#2.2.1 initializer_list { } 构造)

2.2.2迭代器区间的构造

2.2.3n个val的构造

[2.2.3.1 迭代器区间构造 和 n个val构造 的冲突](#2.2.3.1 迭代器区间构造 和 n个val构造 的冲突)

2.3reserve

修正1

2.4push_back

2.5Print()

2.6析构函数

2.7pop_back

2.8resize

2.8.1resize中对参数的理解

2.8.2resize

2.9swap函数

[3.insert (迭代器失效)](#3.insert (迭代器失效))

3.1迭代器失效------野指针(具体看迭代器类型)

[4.erase (迭代器失效)](#4.erase (迭代器失效))

4.1迭代器失效

[4.2迭代器失效的检查:对比vs平台(强制检查) 和 g++平台(不检查)](#4.2迭代器失效的检查:对比vs平台(强制检查) 和 g++平台(不检查))

5.insert和erase迭代器失效的解决办法

6.拷贝构造函数(传统,现代)

6.1现代写法

6.2更现代的写法

7.operator(传统,现代)

7.1现代写法

8.深度理解资源管理------更深层次的浅拷贝


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函数,直接交换他们的底层指针指向的资源:


感谢支持!

相关推荐
chilavert3181 小时前
技术演进中的开发沉思-231 Ajax:页面内容修改
开发语言·前端·javascript
一只小bit1 小时前
Qt 信号与槽:信号产生与处理之间的重要函数
前端·c++·qt·cpp·页面
wuk9981 小时前
基于MATLAB的混合动力汽车(HEV)简单整车模型实现
开发语言·matlab·汽车
偶像你挑的噻1 小时前
1.Qt-编译器基本知识介绍
开发语言·qt
天天进步20151 小时前
拒绝“玄学”Bug:C++ 多线程调试指南与 ThreadSanitizer 实战
开发语言
观音山保我别报错1 小时前
变量作用域
开发语言·python
透明的玻璃杯1 小时前
VS2015 +QT5.9.9 环境问题注意事项
开发语言·qt
say_fall1 小时前
C语言编程实战:每日一题:用队列实现栈
c语言·开发语言·redis
董世昌411 小时前
前端跨域问题:原理、8 种解决方案与实战避坑指南
开发语言·前端·javascript