问题背景
在工作中曾碰到过一个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的调试版迭代器还是很强大,这种防御性编程,能够帮你快速的定位到程序潜在的问题,否则无端的崩溃会让你怀疑人生。