目录
1、概述
2、使用STL列表中的元素越界
3、遍历STL列表删除元素时对迭代器自加处理有问题引发越界
4、更隐蔽的遍历STL列表删除元素时引发越界的场景
5、多线程同时操作STL列表时没有加锁导致冲突
6、对包含STL列表对象的结构体进行memset操作导致STL列表对象内存出异常
7、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_2276111.html 有了STL容器之后,我们基本不再需要再去设计链表等数据结构了,STL中的vector、list、map等容器基本就能满足我们的需求了。但我们在使用STL列表可能会因为编码不规范遇到这样那样的问题,导致软件出现异常,特别是新人会因为缺乏经验容易犯一些错误。正好前段时间在技术群中有粉丝就遇到使用STL容器的问题,于是问我可能是哪些原因导致的。在此,我结合以前项目中遇到的多个问题场景,给大家做个详细的总结,以供借鉴或参考。
1、概述
在项目代码中,我们会大量地使用C++标准库给我们提供的vector、list、map等STL标准模板库(Standard Template Library)容器。这些使用模版实现的容器列表很灵活、很好用,给我们日常编码带来了很大的便利。但在使用STL列表时可能会因为经验不足遇到这样或那样的问题。
本文根据多年的项目问题排查经验以及遇到的多个问题场景,大概地总结一下操作STL列表时发生异常的常见原因,如下:
1)使用数组下标(仅能操作vector)或者使用迭代器直接去访问STL列表中的元素,但这些元素不存在,导致越界访问。一般是因为没有判断STL列表为空,或者没有判断要访问的元素是否存在。
2)在遍历STL列表删除元素时迭代器自加处理的有问题。
3)多线程同时操作STL列表时没有加锁保护,导致访问冲突。一般是一个线程在遍历STL列表,另一个线程在增删STL列表中的元素,导致访问冲突,出现异常崩溃。
4)结构体某个成员是STL列表对象,对结构体对象进行memset操作,破坏了STL内部的内存结构,导致访问STL列表时出异常。大家在定义结构体对象时,习惯性地进行了memset操作。但能进行memset的前提是,结构体中所有成员都必须是基本类型的!一旦结构体中包含C++类对象,是严禁进行memset操作的。
在这里,给大家重点推荐一下我的几个热门畅销专栏:
专栏1:(该专栏订阅量已达到400个,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据近几年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的实战问题分析实例,带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
专栏中的文章均是通过项目实战总结出来的(通过项目实战积累了大量的异常排查素材和案例),有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域的多个方面的内容,同时给出C/C++及网络方面的常见笔试面试题,并详细讲述Visual Studio常用调试手段与技巧!
专栏3:
开源组件及数据库技术https://blog.csdn.net/chenlycly/category_12458859.html
以多年的开发实战为基础,分享一些开源组件及数据库技术!
2、使用STL列表中的元素越界
比如我们在使用vector列表时,常使用数组下标的方式去访问列表中的元素,有时可能没有判断列表中元素个数,直接就使用下标去访问了,比如:
// 设备信息
struct TDeviceInfo
{
char szDeviceId[64]; // 设备id
char szDeviceName[64]; // 设备名称
char szIp[32]; // 设备ip
unsigned short uPort; // 设备端口
int nDevType; // 设备类型
};
vector<TDeviceInfo> vtDevList;
int nDevType = vtDevlist[3].nDevType;
可能当前列表vtDevlist中的元素个数少于3个,但我们用数组下标的方式去强行访问了,导致Out of range访问越界了。
我们之所以通过数组下标去访问vector列表元素,不会平白无故地去访问,而是与我们的业务场景关联的。
还有一种情况是使用迭代器去越界访问的:
vector<TDeviceInfo>::iterator iter = vtDevList.begin();
int nDevType = iter->nDevType;
没有判断列表是否为空,强行去访问列表中的begin元素的。没有判断当前迭代器是否为end的,即
if (itor != vtDevList.end() )
{
}
这个问题场景,我们在项目中多次遇到过。
3、遍历STL列表删除元素时对迭代器自加处理有问题引发越界
这是新人比较容易犯的错误。比如删除设备列表中设备类型等于1的所有设备,有问题的代码如下:
vector<TDeviceInfo> vtDevList;
vector<TDeviceInfo>::iterator iter = vtDevList.begin();
for (; iter != vtDevList.end(); iter++)
{
if (iter->nDevType == 1)
{
vtDevList.erase(iter);
}
}
如果有元素被删除,在Debug下调试时,会报如下的错误:
正确的写法是:
for (; iter != vtDevList.end(); )
{
if (iter->nDevType == 1)
{
iter = vtDevList.erase(iter);
}
else
{
iter++;
}
}
当执行erase操作后,迭代器iter原先的指向就失效了,不能再对其进行++操作,否则就越界了。对于迭代器变量iter,要分开处理,如果有删除元素,则将erase接口返回的迭代器赋值给iter;如果没有删除元素,直接对迭代器进行++操作。
新人在写遍历STL列表删除元素的代码时可能会犯上面的错误,没有区分对待。
4、更隐蔽的遍历STL列表删除元素时引发越界的场景
这个问题场景和上面讲的遍历STL列表删除元素的场景类似,但本场景更有隐蔽性,这在以前的项目中遇到过。
这个问题是同事那边遇到的,在调试程序时老是在遍历STL列表时出现异常,于是找我过去帮忙排查一下。
问题场景是这样子的,在遍历STL列表的for循环体中调了一个函数fun1,因为业务比较复杂,涉及了多个函数的调用,fun1函数内部调用了fun2函数,fun2函数中调了fun3函数,fun3函数中调用了fun4函数,问题就出在fun4函数中,fun4函数中居然将当前最上面正在遍历的STL列表中的元素删除了,演示代码如下:
vector<TDeviceInfo> vtDevList;
vector<TDeviceInfo>::iterator iter = vtDevList.begin();
for (; iter != vtDevList.end(); iter++)
{
// 调用fun1
fun1();
}
void fun2()
{
// 调用fun3
fun3();
}
void fun3()
{
// 调用fun4
fun4();
}
void fun4()
{
// 此处删除了STL列表vtDevList中的内容
}
从而出现在遍历STL列表时删除列表中元素的场景。应该像上面那个场景中那样,处理代码:
for (; iter != vtDevList.end(); )
{
if (iter->nDevType == 1)
{
iter = vtDevList.erase(iter);
}
else
{
iter++;
}
}
但实际上,因为当前问题场景中函数调用层次很深,不适合这样处理,只能将这种在遍历STL列表时删除元素的操作给取缔掉,重新编写代码控制逻辑。正是因为函数调用层次比较深,所以这个问题有很强的隐蔽性。
当时排查这个问题时,凭着经验,首先排除多线程同时操作同一个STL列表的场景,当时觉得大概率是遍历STL列表过程中有地方删除了STL列表中的元素导致的。然后沿着这个思路,最终在某个函数中找到了删除STL列表元素的代码。
5、多线程同时操作STL列表时没有加锁导致冲突
多线程同时操作某个STL列表时产生问题的典型场景是,一个线程在遍历STL列表,另一个线程在对STL列表中的元素进行增删操作。一般在多线程操作共享资源时都要加锁,但加锁有个原则,尽量较小锁的范围,在锁的范围的代码要尽快执行完,不能影响其他线程的执行效率。
这个问题,我们在项目中遇到不止一次了。有时可能比较具体隐蔽性,比如在另一个线程中调用某个函数接口,在该函数内部操作了一个公用的STL列表,然后出现了多线程同时操作STL列表的场景,有时较难发现这里面隐藏的多线程操作共享资源的问题,特别是在维护他人编写的代码模块时。
6、对包含STL列表对象的结构体进行memset操作导致STL列表对象内存出异常
大家在定义结构体对象时,习惯性地在使用之前对结构体对象进行了memset操作。如果结构体中某个成员是STL列表对象,执行memse操作后,会破坏STL对象内部的内存结构,导致访问STL列表时出现异常。这个问题在项目中出现过,当时是一个新人写的代码(新人考虑问题不够全面,缺乏安全意识),因为缺乏经验,直接对一个包含STL列表对象的结构体进行memset操作,比如:(结构体TWindowInfo中包含了一个STL list成员)
typedef struct tagWindowInfo
{
public:
HWND hWnd; // 当前窗口句柄
char szWndName[256]; // 当前窗口名称
RECT m_rcWnd; // 当前窗口矩形坐标(在屏幕坐标系中)
std::list<tagWindowInfo> childWndList; // 当前窗口所有子窗口信息列表
}TWindowInfo;
// 结构体TWindowInfo中包含了一个list列表成员,下面直接对结构体对象执行了memset操作
TWindowInfo tWndInfo;
memset(&tWndInfo, 0, sizeof(tWndInfo));
导致程序在访问这个STL列表时出现了各种莫名其妙的错误,比如列表中的元素数据不对,执行STL列表类的成员函数发生异常崩溃。当时这个问题我查了很久,后来才发现是因为对结构体进行memset操作导致的。
注意,能进行memset的前提是,结构体中所有成员都必须是基本类型的!一旦结构体中包含C++类对象,是严禁进行memset操作的。为什么不能对类对象进行memset操作呢?比如类中有虚函数,就会内置一个虚函数表指针,这个虚函数表指针中存放的是存放虚函数地址(代码段地址)的虚函数表,在调用虚函数时会用到这个虚函数表。如果对类对象进行memset操作,会将虚函数表指针中的值置为NULL,这样在调用虚函数时出现问题。对于虚函数调用的过程与机制,需要从汇编代码的角度去看,涉及到两次寻址,具体过程可以参见我之前写的文章:
几秒读懂C++虚函数调用的汇编代码实现https://blog.csdn.net/chenlycly/article/details/121046234 再比如,有些类实现的比较复杂,例如STL列表,类内部会有额外的变量去维护管理较复杂的内部内存结构,如果对这样的类对象进行memset操作,将维护内部内存结构的变量的值都只为NULL了,结果是不可预料(unexpected)的。
对STL列表执行memset操作引发异常的问题,我们在项目中遇到了两次,都是新人写的代码,两次的表现现象是不太一样的:
1)一次是在遍历STL列表读取列表中元素发现元素值异常。
2)一次是操作STL列表时产生了异常,直接将当前函数余下的代码都跳过了,导致该执行的代码没有执行,导致后续代码执行时逻辑出现异常。
其实C++中的大部分问题都是内存问题,在分析这些问题时要从内存的角度去看。很多问题,从内存的角度去看,就好理解多了!变量本质上就是一块内存,给变量赋值就是向内存中写入内容,读变量的值就是读取内存中的内容,这一点从汇编代码上可以清晰地看出来!
7、最后
在分析和解决问题时,大家要主动地多想想多思考,多倒腾倒腾里面的细节问题,必要的时候可以进行归纳与总结。很多细节知识点,都是相通的,搞懂一个细节,相关的细节点可能很快就能明白了!