STL迭代器引发的崩溃问题

问题背景

在工作中曾碰到过一个STL容器崩溃的问题,崩溃的地方为如下截图:

经验证,最简案发现场为如下代码:

#include <iostream>
#include <vector>
using namespace std;

class Iterator {
public:
	~Iterator() {}
	virtual void  step() = 0;
	virtual int get() = 0;
};

class IteratorImp : public Iterator {
public:
	IteratorImp(const vector< int>& vec) {
		mite = vec.begin();
	}
	void step() {
		mite++;
	}
	int get() const{
		return *mite;
	}
private:
	vector<int>::iterator mite;
};

int main()
{
	vector<int> ve{ 1,2,3};
	Iterator* pIter = new IteratorImp(ve);
	delete pIter;
	ve.clear();
	return 0;
}

出现崩溃有几个条件:

  • Debug模式下才会崩溃,Release不会
  • msvc编译器才会崩溃,gcc不会
  • 其他容器如map,unordered_map会产生同样的崩溃问题

问题分析

1、初看此代码,有点C++经验的人都能看出,此段代码有一个明显的错误,就是基类析构函数不是虚析构函数,确实,将基类析构函数改为虚析构,此崩溃问题就不复现了。你可能觉得:好了,可以收工了。但是,为什么不是虚析构函数会崩溃呢,崩溃的原因仍然费解?

2、原来错误的代码中,delete基类指针,不会调用子类的析构函数,如果子类有指针成员,可能会造成内存泄漏,但是这里子类没有指针资源,析构函数好像没啥用?

3、迭代器只是一个指向容器元素的泛指针,迭代器析构为啥会影响到容器的内存?

4 、上述虚析构函数的问题,在标准里面是未定义行为(undefined behavior),那崩溃是不是未定义行为导致,毕竟未定义行为发生什么都是能背锅的,但是好像也有点牵强。

秉承着打破砂锅问到底的老司机精神,只能硬着头皮,饱受着STL代码污染眼睛的痛苦,强行阅读调试了一下微软的STL源码,这里不得不吐槽一下:微软STL源码绝对是代码整洁之道的超级反面教材,整个人钻进去之后,就是各种f**k的感受。

崩溃原因分析

经详细的代码调试和分析,找到了问题的原因,先说主要的结论:

1、vector有一个_Container_proxy类会记录迭代器链表,有点类似于引用计数。

2、vector<int>::iterator 的基类的析构函数是non-trival的,会将自己从链表中删除

3、vector的clear()函数中会访问_Container_proxy中的迭代器链表,将迭代器链表原地解散。

4、当通过delete基类指针析构指向子类的对象,而且基类没有虚析构函数的时候,只会调用基类的析构函数,并释放指针指向的内存

当基类有虚析构函数时,会通过虚函数机制,调用子类析构函数,子类析构函数会调用其成员对象的析构函数【这里是编译器实现的】。

所以上面迭代器崩溃的原因是:IteratorImp类的成员miter所在内存被回收,但没有调用析构函数,将自己从迭代器链表中移除,vector的_Container_proxy 上面记录的这个迭代器指针就变成了一个野指针,导致clear()时非法访问内存崩溃。

vector崩溃相关源码剖析

到这里可以收工了吗? 老司机当然不会满足于此,这个迭代器链表是个什么玩意,STL的迭代器在玩什么,这些东西跟自己平时手撸的玩具版Iterator好像不太一样?于是不得不继续硬着头皮,忍受着被微软STL源码辣眼睛的痛苦,继续看vector的实现代码,最终源码之下,了无秘密。(为了不辣读者的眼睛,下面的源码都是略经整理的清爽版本)

STL vector容器和迭代器有一个基类_Container_base和_Iterator_base, 其定义为条件编译, 由_ITERATOR_DEBUG_LEVEL宏控制,在Debug模式下其为_Container_base12与_Iterator_base12

#if _ITERATOR_DEBUG_LEVEL == 0
using _Container_base = _Container_base0;
using _Iterator_base  = _Iterator_base0;
#else // _ITERATOR_DEBUG_LEVEL == 0
using _Container_base = _Container_base12;
using _Iterator_base = _Iterator_base12;
#endif // _ITERATOR_DEBUG_LEVEL == 0

_Iterator_base12有两个成员, _Myproxy, _Mynextiter

struct _Iterator_base12 {
public:
    mutable _Container_proxy* _Myproxy = nullptr;
	mutable _Iterator_base12* _Mynextiter = nullptr;
};

_Container_proxy记录了容器和第一个迭代器, 可以看到,这个结构相当于一个链表结构,记录了所有的迭代器

struct _Container_proxy {
	inline _Container_proxy() noexcept = default;
	inline _Container_proxy(_Container_base12* _Mycont_) noexcept : _Mycont(_Mycont_) {}

	const _Container_base12* _Mycont = nullptr;
	mutable _Iterator_base12* _Myfirstiter = nullptr;
};
//迭代器链表

迭代器的详细代码如下:

struct _Iterator_base12 {
public:
    mutable _Container_proxy* _Myproxy = nullptr;
	mutable _Iterator_base12* _Mynextiter = nullptr;

	inline _Iterator_base12() noexcept = default;

	inline _Iterator_base12(const _Iterator_base12& _Right) noexcept {
		*this = _Right;
	}

	inline _Iterator_base12& operator=(const _Iterator_base12& _Right) noexcept {
		_Assign_locked(_Right);
		return *this;
	}

	inline ~_Iterator_base12() noexcept {
		_Orphan_me_locked_v3();
	}

	inline void _Adopt(const _Container_base12* _Parent) noexcept {
		_Adopt_locked(_Parent);
	}

	inline const _Container_base12* _Getcont() const noexcept {
		return _Myproxy ? _Myproxy->_Mycont : nullptr;
	}

	static constexpr bool _Unwrap_when_unverified = 2 == 0;	

private:
	inline void _Assign_unlocked(const _Iterator_base12& _Right) noexcept {
		if (_Myproxy == _Right._Myproxy) { //已经被迭代器链表记录的迭代器
			return;
		}

		if (_Right._Myproxy) { //将自己添加到目标容器的迭代器链表
			_Adopt_unlocked(_Right._Myproxy->_Mycont);
		}
		else {  //将自己从源容器的迭代器链表中断开
			_Orphan_me_unlocked_v3();
		}
	}

	void _Assign_locked(const _Iterator_base12& _Right) noexcept {
		_Lockit _Lock(3);
		_Assign_unlocked(_Right);
	}

	inline void _Adopt_unlocked(const _Container_base12* _Parent) noexcept {
		if (!_Parent) {
			_Orphan_me_unlocked_v3();
			return;
		}

		_Container_proxy* _Parent_proxy = _Parent->_Myproxy; //目标的proxy
		if (_Myproxy != _Parent_proxy) {
			if (_Myproxy) {
				_Orphan_me_unlocked_v3();
			}
			_Mynextiter = _Parent_proxy->_Myfirstiter;  //将自己置为proxy的 header, 原来的header置为 next
			_Parent_proxy->_Myfirstiter = this;
			_Myproxy = _Parent_proxy;
		}
	}

	void _Adopt_locked(const _Container_base12* _Parent) noexcept {
		_Lockit _Lock(3);
		_Adopt_unlocked(_Parent);
	}

	inline void _Orphan_me_unlocked_v3() noexcept { //将自己从链表中移除
		if (!_Myproxy) {
			return;
		}

		_Iterator_base12** _Pnext = &_Myproxy->_Myfirstiter;
		while (*_Pnext && *_Pnext != this) {
			const auto _Temp = *_Pnext;
			_Pnext = &_Temp->_Mynextiter;
		}
		
		*_Pnext = _Mynextiter;
		_Myproxy = nullptr;
	}

	void _Orphan_me_locked_v3() noexcept {
		_Lockit _Lock(3);
		_Orphan_me_unlocked_v3();
	}
};

迭代器的构造和赋值

我们重点看一下迭代器的拷贝构造和拷贝赋值, 容器每构建一个迭代器,都会被_Myproxy的迭代器链表记录

vector<int> vec{1,2,3,4};
auto iter1 = vec.begin();
auto iter2 = iter1;
auto iter3 = vec.end();

上面示例中,我们有三个迭代器对象,那么_Myproxy的迭代器链表如下:

迭代器的析构

inline ~_Iterator_base12() noexcept {
	_Orphan_me_locked_v3();
}

_Iterator_base12的析构函数是non_trival的,在析构函数中,会将自己从迭代器链表中移除。此析构函数的调用是非常重要的,如果迭代器对象的内存已释放,但是析构函数未运行,则可能会发生访问冲突。

vector的clear()或者析构函数中,会处理迭代器链表,如下代码所示,如果出现上述内存已释放但是迭代器未从链表中移除的场景,将发生非法的内存访问,这也是上述崩溃问题的根本原因。

inline void _Container_base12::_Orphan_all_unlocked_v3() noexcept {
	if (!_Myproxy) {
		return;
	}
	for (auto& _Pnext = _Myproxy->_Myfirstiter; _Pnext; _Pnext = _Pnext->_Mynextiter) {
		_Pnext->_Myproxy = nullptr;
	}
	_Myproxy->_Myfirstiter = nullptr;
}

Visual C++的调试迭代器

Visual C++的迭代器设计具有”调试迭代器“模式,定义了0、1、2三个调试等级,按照设计意图,应该是level0不做检查,level1做简单的越界检查,leve2做更多的迭代器有效性检查,但是较新版本level1和level2合并了(visual studio 2022),都是做比较详细的检查。

#if _HAS_ITERATOR_DEBUGGING
#define _ITERATOR_DEBUG_LEVEL 2
#elif _SECURE_SCL
#define _ITERATOR_DEBUG_LEVEL 1
#else
#define _ITERATOR_DEBUG_LEVEL 0
#endif

Debug模式下编译时,_ITERATOR_DEBUG_LEVEL =2 , Visual C++ 会打开对 STL 迭代器的大量检查。这非常有用,因为迭代器的任何错误都会立即导致异常,并显示有用的异常消息,可以在编程时更早地捕获潜在的错误。通常,调试模式并不慢,但是某些情况下打开所有迭代器检查的调试模式可能非常慢,甚至毫无用处,例如 C++ 代码在 STL 容器上有很多循环并且大量使用迭代器。

_Ty& operator[](const size_type _Pos) noexcept /* strengthened */ {
    auto& _My_data = _Mypair._Myval2;
#if _CONTAINER_DEBUG_LEVEL > 0  //_CONTAINER_DEBUG_LEVEL = 1 if _ITERATOR_DEBUG_LEVEL != 0
    _STL_VERIFY( _Pos < static_cast<size_type>(_My_data._Mylast - _My_data._Myfirst), 
                "vector subscript out of range");
#endif // _CONTAINER_DEBUG_LEVEL > 0
    return _My_data._Myfirst[_Pos];
}

如下为一些常见的迭代器错误使用的场景:

迭代器越界

如果通过索引运算符来访问位于容器边界之外的元素,将会发生运行时错误。

#include <vector>
#include <iostream>
using namespace std;

int main()
{
   vector<int> v;
   v.push_back(1);

   int i = v[0];
   cout << i << endl;
   i = v[1]; //报越界异常
}

迭代器无效

容器的插入操作将会导致调试版的迭代器失效

#include <vector>
#include <iostream>

int main() {
   std::vector<int> v {1, 2, 3};
   std::vector<int>::iterator j = v.end();
   --j;

   std::cout << *j << '\n';
   v.insert(i,4); //老的迭代器都会失效
   std::cout << *j << '\n'; // 使用老的迭代器
}

迭代器未初始化

如果在初始化之前尝试使用迭代器,断言也会发生,如下所示:

#include <string>
using namespace std;

int main() {
   vector<int>::iterator i1, i2;
   i1++;
}

迭代器不兼容

for_each算法的两个迭代器不兼容, 算法会执行检查以确定提供给它们的迭代器是否引用相同的容器。

#include <algorithm>
#include <vector>
using namespace std;

int main()
{
    vector<int> v1 {10, 20};
    vector<int> v2 {10, 20};
    for_each(v1.begin(), v2.end(), [] (int& elem) { elem *= 2; } );
}

迭代器未正确析构

此场景即为文章最开头Bug崩溃的场景,可以总结为如下最简洁形式~

#include <vector>
struct base {  
   // virtual ~base() {}
};

struct derived : base {
   std::vector<int>::iterator m_iter;
   derived( std::vector<int>::iterator iter ) : m_iter( iter ) {}
   ~derived() {}
};

int main() {
   std::vector<int> vec{1,2,3,4};
   base * pb = new derived( vect.begin() );
   delete pb;  
}

总结:

1、写符合C++规范的代码,通过基类指针析构子对象,没有虚析构函数是不符合规范的代码,不符合规范就容易坑你。

2、STL的调试版迭代器还是很强大,这种防御性编程,能够帮你快速的定位到程序潜在的问题,否则无端的崩溃会让你怀疑人生。

  • 29
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值