【C++STL :vector类 (二) 】攻克 C++ Vector 的迭代器失效陷阱:从源码层面详解原理与解决方案

🔥艾莉丝努力练剑:个人主页

专栏传送门:《C语言》《数据结构与算法》C/C++干货分享&学习过程记录Linux操作系统编程详解笔试/面试常见算法:从基础到进阶

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬艾莉丝的简介:


🎬艾莉丝的C++专栏简介:


目录

C++的两个参考文档

[4 ~> vector底层:源码怎么看?](#4 ~> vector底层:源码怎么看?)

[4.1 还在被冗长源码"劝退"?试试这款阅读神器Source Insight,让你效率翻倍!](#4.1 还在被冗长源码“劝退”?试试这款阅读神器Source Insight,让你效率翻倍!)

[4.2 如何高效阅读源码?一套从"骨架"到"血肉"的实战方法](#4.2 如何高效阅读源码?一套从“骨架”到“血肉”的实战方法)

[4.3 庖丁解牛:如何高效阅读一段源码?](#4.3 庖丁解牛:如何高效阅读一段源码?)

[4.3.1 迭代器](#4.3.1 迭代器)

[4.3.2 注意](#4.3.2 注意)

[4.3.3 其他补充](#4.3.3 其他补充)

[4.4 攻克STL源码:构建你的系统性阅读路线图与方法论](#4.4 攻克STL源码:构建你的系统性阅读路线图与方法论)

[5 ~> 告别vector:vector 迭代器失效问题](#5 ~> 告别vector:vector 迭代器失效问题)

[5.1 insert失效问题](#5.1 insert失效问题)

[5.1.1 insert](#5.1.1 insert)

[5.1.2 插入数据](#5.1.2 插入数据)

[5.2 指定位置元素的删除操作------erase失效问题](#5.2 指定位置元素的删除操作——erase失效问题)

[5.2.1 erase](#5.2.1 erase)

[5.2.2 删除所有的偶数](#5.2.2 删除所有的偶数)

[5.3 Linux下,g++编译器对迭代器失效的检测不严格](#5.3 Linux下,g++编译器对迭代器失效的检测不严格)

[5.3.1 扩容之后,迭代器已经失效了](#5.3.1 扩容之后,迭代器已经失效了)

[5.3.2 erase删除任意位置代码后,linux下迭代器并没有失效](#5.3.2 erase删除任意位置代码后,linux下迭代器并没有失效)

[5.3.3 erase删除的迭代器如果是最后一个元素,删除之后it已经超过end](#5.3.3 erase删除的迭代器如果是最后一个元素,删除之后it已经超过end)

[5.3.4 与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效](#5.3.4 与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效)

本文代码完整展示

(一)vector.h

(二)Test.c:

结尾


C++的两个参考文档

老朋友(非官方文档):cplusplus

官方文档(同步更新):cppreference
vector容器文档链接:vector



4 ~> vector底层:源码怎么看?

4.1 还在被冗长源码"劝退"?试试这款阅读神器Source Insight,让你效率翻倍!

链接:Source Insight

4.2 如何高效阅读源码?一套从"骨架"到"血肉"的实战方法

一句话:底层用到了再去学,用不到,看得懂功能就行。

4.3 庖丁解牛:如何高效阅读一段源码?

源码还是太吃操作了,主包还是个小马喽,所以博主就简单带大家来看一段吧!

4.3.1 迭代器

我们直接来看源码的这一段------

这里就直接看迭代器:begin()、end()就可以了------

4.3.2 注意

调内存池(没有初始化,只是开空间)。

这里不是malloc、new出来的,没有初始化,只是开了个空间,所以如果直接赋值会出问题。

4.3.3 其他补充

4.4 攻克STL源码:构建你的系统性阅读路线图与方法论


5 ~> 告别vector:vector 迭代器失效问题

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对 指针进行了封装,比如:vector的迭代器就是原生态指针T*。因此迭代器失效,实际就是迭代器 底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即 如果继续使用已经失效的迭代器,程序可能会崩溃)。

下面,我们会罗列一些对于vector可能会导致其迭代器失效的操作。
我们先写好这个框架------

5.1 insert失效问题

5.1.1 insert

会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、
assign、push_back等。

cpp 复制代码
//会引起其底层空间改变的操作,都有可能是迭代器失效
#include <iostream>
using namespace std;
#include <vector>
int main()
{
	vector<int> v{ 1,2,3,4,5,6 };

	auto it = v.begin();

	// 将有效元素个数增加到100个,多出的位置使用8填充,操作期间底层会扩容
	// v.resize(100, 8);

	// reserve的作用就是改变扩容大小但不改变有效元素个数,操作期间可能会引起底层容量改变
		// v.reserve(100);

		// 插入元素期间,可能会引起扩容,而导致原空间被释放
		// v.insert(v.begin(), 0);
		// v.push_back(8);

		// 给vector重新赋值,可能会引起底层容量改变
		v.assign(100, 8);

	   /*
	   出错原因:以上操作,都有可能会导致vector扩容,也就是说vector底层原理旧空间被释
   放掉,而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块
   已经被释放的空间,而引起代码运行时崩溃。
	   解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给
   it重新赋值即可。
	   */
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}

5.1.2 插入数据

5.2 指定位置元素的删除操作------erase失效问题

5.2.1 erase

cpp 复制代码
//指定位置元素的删除操作--erase
#include <iostream>
using namespace std;
#include <vector>

int main()
{
	int a[] = { 1, 2, 3, 4 };
	vector<int> v(a, a + sizeof(a) / sizeof(int));
	// 使用find查找3所在位置的iterator
	vector<int>::iterator pos = find(v.begin(), v.end(), 3);
	// 删除pos位置的数据,导致pos迭代器失效。
	v.erase(pos);
	cout << *pos << endl; // 此处会导致非法访问
	return 0;
}

erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理 论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end 的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素 时,vs就认为该位置迭代器失效了。 以下代码的功能是删除vector中所有的偶数,请问那个代码是正确的,为什么?

cpp 复制代码
//以下代码的功能是删除vector中所有的偶数,请问那个代码是正确的,为什么?
#include <iostream>
using namespace std;
#include <vector>

int main()
{
	vector<int> v{ 1, 2, 3, 4 };
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
			v.erase(it);
		++it;
	}

	return 0;
}

int main()
{
	vector<int> v{ 1, 2, 3, 4 };
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
			it = v.erase(it);
		else
			++it;
	}
	return 0;
}

5.2.2 删除所有的偶数

我们的解决方案就是:重置 + 更新------

5.3 Linux下,g++编译器对迭代器失效的检测不严格

友友们注意------Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。

5.3.1 扩容之后,迭代器已经失效了

扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了**------**

cpp 复制代码
// 1. 扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了
int main()
{
	vector<int> v{ 1,2,3,4,5 };
	for (size_t i = 0; i < v.size(); ++i)
		cout << v[i] << " ";
	cout << endl;
	auto it = v.begin();
	cout << "扩容之前,vector的容量为: " << v.capacity() << endl;
	// 通过reserve将底层空间设置为100,目的是为了让vector的迭代器失效    
	v.reserve(100);
	cout << "扩容之后,vector的容量为: " << v.capacity() << endl;

	// 经过上述reserve之后,it迭代器肯定会失效,在vs下程序就直接崩溃了,但是linux下不会
		// 虽然可能运行,但是输出的结果是不对的
		while (it != v.end())
		{
			cout << *it << " ";
			++it;
		}
	cout << endl;
	return 0;
}

运行结果------

bash 复制代码
//程序输出:
1 2 3 4 5
//扩容之前,vector的容量为: 5
//扩容之后,vector的容量为 : 100
0 2 3 4 5 409 1 2 3 4 5

5.3.2 erase删除任意位置代码后,linux下迭代器并没有失效

因为空间还是原来的空间,后序元素往前搬移了,it 的位置还是有效的。

cpp 复制代码
// 因为空间还是原来的空间,后序元素往前搬移了,it的位置还是有效的
#include <vector>
#include <algorithm>
int main()
{
	vector<int> v{ 1,2,3,4,5 };
	vector<int>::iterator it = find(v.begin(), v.end(), 3);
	v.erase(it);
	cout << *it << endl;
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	return 0;
}

运行结果------

程序可以正常运行,并打印------

bash 复制代码
4
4 5

5.3.3 erase删除的迭代器如果是最后一个元素,删除之后it已经超过end

此时迭代器是无效的,++it导致程序崩溃------

cpp 复制代码
// 此时迭代器是无效的,++it导致程序崩溃
int main()
{
	vector<int> v{ 1,2,3,4,5 };
	// vector<int> v{1,2,3,4,5,6};
	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
			v.erase(it);
		++it;
	}
	for (auto e : v)
		cout << e << " ";
	cout << endl;
	return 0;
}

运行结果------

bash 复制代码
========================================================
// 使用第一组数据时,程序可以运行
[sly@VM - 0 - 3 - centos 20220114]$ g++ testVector.cpp - std = c++11
[sly@VM - 0 - 3 - centos 20220114]$ . / a.out
1 3 5
======================================================== =
// 使用第二组数据时,程序最终会崩溃
[sly@VM - 0 - 3 - centos 20220114]$ vim testVector.cpp
[sly@VM - 0 - 3 - centos 20220114]$ g++ testVector.cpp - std = c++11
[sly@VM - 0 - 3 - centos 20220114]$ . / a.out
Segmentation fault

5.3.4 与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效

cpp 复制代码
#include <string>
void TestString()
{
	string s("hello");
	auto it = s.begin();
	// 放开之后代码会崩溃,因为resize到20会string会进行扩容
	// 扩容之后,it指向之前旧空间已经被释放了,该迭代器就失效了
	// 后序打印时,再访问it指向的空间程序就会崩溃
	//s.resize(20, '!');
	while (it != s.end())
	{
		cout << *it;
		++it;
	}
	cout << endl;
	it = s.begin();
	while (it != s.end())
	{
		it = s.erase(it);
		// 按照下面方式写,运行时程序会崩溃,因为erase(it)之后
		// it位置的迭代器就失效了
		// s.erase(it);  
		++it;
	}
}

迭代器失效解决办法:在使用前,对迭代器重新赋值即可。


本文代码完整展示

(一)vector.h

cpp 复制代码
#pragma once

#include<assert.h>

namespace jqj
{
	template<class T>
	class vector
	{
	public:
		/*typedef T* iterator;*/
		using iterator = T*;
		using const_iterator = const T*;
		//using这里是复用了展开命名空间的关键词,关键词复用在C++很常见

		//迭代器
		iterator begin()
		{
			return _start;
		}

		//迭代器
		iterator end()
		{
			return _finish;
		}

		//const修饰的情况
		const_iterator begin() const
		{
			return _start;
		}
		
		//const修饰的情况
		const_iterator end() const
		{
			return _finish;
		}

		//构造
		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storqge(nullptr)
		{ }

		//析构
		~vector()
		{
			if (_start)
			{
				delete[] _start;
				_start = _finish = _end_of_storge = nullptr;
			}
		}

		//bool
		bool empty() const
		{
			return _start == _finish;
		}

		//扩容
		void reserve(size_t n)
		{
			if (n > capacity())//这里是调用capacity的函数
			{
				size_t sz = size();//记录size的值
				T* tmp = new T[n];
				if (_start)
				{
					memcpy(tmp, _start, sizeof(T) * sz);
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;//这里加size会出问题
				_end_of_storqge = _start + n;
			}
		}

		//容量
		size_t capacity() const
		{
			return _end_of_storge - _start;
		}

		//有效长度
		size_t size() const
		{
			return _finish - _start;
		}

		T& operator[](size_t i)
		{
			assert(i < size());

			return _start[i];
		}

		const T& operator[](size_t i) const
		{
			assert(i < size());

			return _start[i];
		}

		void push_back(const T& x)
		{
			if (_finish == _end_of_storge)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);//扩两倍
			}

			*_finish = x;
			++_finish;
		}

		void pop_back()
		{
			assert(!empty());//判空
			--_finish;
		}

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);

			if (_finish == _end_of_storage)
			{
				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;
			}

			//如果是malloc,这里就不能这么写了
			*pos = x;
			++_finish;

			return pos;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				++it;
			}

			--_finish;

			return pos;
		}

		private:
			iterator _start;
			iterator _finish;
			iterator _end_of_storage;
	};
}

(二)Test.c:

cpp 复制代码
#include"vector.h"

namespace jqj
{
	void Print(const vector<int>& v)
	{
		//范围for
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		//for (size_t i = 0; i < v.size(); i++)
		//{
		//	//v[0]++;//不行
		//	cout << v[i] << " ";
		//}
		//cout << endl;
	}

	void Test_vector1()
	{
		jqj::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);
		v.push_back(5);

		v[0]++;

		Print(v);
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	//insert迭代器失效
	void Test_vector2()
	{
		jqj::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		//v.push_back(5);
		Print(v);

		v.insert(v.begin(), 0);
		Print(v);

		auto it = v.begin() + 3;//第4个位置
		//insert以后,it是否失效?
		//it失效了,也就意味着,insert以后,it失效了,it就不能使用了
		v.insert(it, 30);
		Print(v);
	}

	//erase迭代器失效
	void Test_vector3()
	{
		jqj::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		Print(v);

		v.erase(v.begin());
		Print(v);

		auto it = v.begin() + 2;

		//it是否失效?失效,不能访问,访问结果未定义
		v.erase(it);
		Print(v);
	}

	void Test_vector4()
	{
		jqj::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		v.push_back(6);
		//Print(v);//不能用Print

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		////删除所有的偶数
		//auto it = v.begin();
		//while (it != v.end())
		//{
		//	if (*it % 2 == 0)
		//	{
		//		v.erase(it);
		//	}

		//	++it;
		//}

		auto it = v.begin();
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{
				it = v.erase(it);//重置更新
			}
			else
			{
				++it;
			}
		}

		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}

int main()
{
	//jqj::Test_vector1();
	//jqj::Test_vector2();
	//jqj::Test_vector3();
	jqj::Test_vector4();

	return 0;
}

结尾

往期回顾:

【C++STL :vector类 (一) 】详解vector类的使用层&&vector实践:算法题练习

**结语:** 看到这里啦!那请大佬不要忘记给博主来个"一键四连"哦!

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡

૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
hope_wisdom2 小时前
C/C++数据结构之用数组实现栈
c语言·数据结构·c++·数组·
知白守黑2672 小时前
访问控制、用户认证、https
linux·服务器·前端
紧迫感2 小时前
在Debian系统上修改开源软件源代码制作patch
linux
一张假钞2 小时前
Mac OS远程执行Shell命令技巧
linux·运维·服务器
熊猫不是猫QAQ3 小时前
绿联 NAS 部署 “快递式” 文件分享应用,轻量便捷秒传文件!
经验分享
能掐会算两元一位3 小时前
中医理论、学派
经验分享
牛奶咖啡133 小时前
解决keepalived的主备服务器都持有VIP——出现脑裂现象
linux·运维·服务器·vrrp·脑裂·keepalived主备·高可用主备都持有vip
每天更新3 小时前
Linux 内核空间 并发竞争处理 共享资源&线程同步
linux
QUST-Learn3D3 小时前
C++单头文件实现windows进程间通信(基于命名管道)
c++·windows·单片机