目录
6、对包含STL列表对象的结构体进行memset操作导致STL列表对象内存出异常
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 有了C++标准库中的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操作。但能进行memset的前提是,结构体中所有成员都必须是基本类型!一旦结构体中包含C++类对象,是严禁进行memset操作的。严禁对C++对象进行memset操作的!比如C++类中包含虚函数指针、C++类中包含STL容器的成员变量,如果直接进行memset,则会将虚函数表指针置为NULL,会将STL容器对象的内存给破坏掉!
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:(该精品技术专栏的订阅量已达到500多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795
常用的C++软件辅助分析工具有SPY++、PE工具、Dependency Walker、GDIView、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏3:(本专栏涵盖了多方面的内容,是当前重点打造的专栏,专栏文章已经更新到400多篇,持续更新中...)
C/C++实战进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、C++11及以上新特性(不仅看开源代码会用到,日常编码中也会用到部分新特性,面试时也会涉及到)、常用C++开源库的介绍与使用、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(排查软件异常的手段与方法、分析C++软件异常的基础知识、常用软件分析工具使用、实战问题分析案例等)、设计模式、网络基础知识与网络问题分析进阶内容等。
专栏4:
VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:
Windows C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了Windows C++ 应用软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
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,要分开处理:
1)如果有删除元素,则将erase接口返回的迭代器赋值给iter;
2)如果没有删除元素,直接对迭代器进行++操作。
新人在写遍历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、最后
在分析和解决问题时,大家要主动地多想想多思考,多倒腾倒腾里面的细节问题,必要时将之前遇到的多个相关案例问题串起来,进行归纳与总结。很多细节点是相通的,搞懂一个细节,相关的细节点可能很快就弄明白了!