使用 STL 容器发生异常的常见原因分析与总结

news2024/11/16 10:31:32

目录

1、概述

2、使用STL列表中的元素越界

3、遍历STL列表删除元素时对迭代器自加处理有问题引发越界

4、更隐蔽的遍历STL列表删除元素时引发越界的场景

5、多线程同时操作STL列表时没有加锁导致冲突

6、对包含STL列表对象的结构体进行memset操作导致STL列表对象内存出异常

7、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html开源组件及数据库技术(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)icon-default.png?t=N7T8https://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++软件调试与异常排查从入门到精通系列文章汇总icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931

本专栏根据近几年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的实战问题分析实例,带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!

专栏中的文章均是通过项目实战总结出来的(通过项目实战积累了大量的异常排查素材和案例),有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!

专栏2: 

C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html

以多年的开发实战为基础,总结并讲解一些的C/C++基础与进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域的多个方面的内容,同时给出C/C++及网络方面的常见笔试面试题,并详细讲述Visual Studio常用调试手段与技巧!

专栏3: 

开源组件及数据库技术icon-default.png?t=N7T8https://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++虚函数调用的汇编代码实现icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/121046234       再比如,有些类实现的比较复杂,例如STL列表,类内部会有额外的变量去维护管理较复杂的内部内存结构,如果对这样的类对象进行memset操作,将维护内部内存结构的变量的值都只为NULL了,结果是不可预料(unexpected)的。 

         对STL列表执行memset操作引发异常的问题,我们在项目中遇到了两次,都是新人写的代码,两次的表现现象是不太一样的:

1)一次是在遍历STL列表读取列表中元素发现元素值异常。

2)一次是操作STL列表时产生了异常,直接将当前函数余下的代码都跳过了,导致该执行的代码没有执行,导致后续代码执行时逻辑出现异常。

       其实C++中的大部分问题都是内存问题,在分析这些问题时要从内存的角度去看。很多问题,从内存的角度去看,就好理解多了!变量本质上就是一块内存,给变量赋值就是向内存中写入内容,读变量的值就是读取内存中的内容,这一点从汇编代码上可以清晰地看出来!

7、最后

       在分析和解决问题时,大家要主动地多想想多思考,多倒腾倒腾里面的细节问题,必要的时候可以进行归纳与总结。很多细节知识点,都是相通的,搞懂一个细节,相关的细节点可能很快就能明白了!   

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1541250.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

操作符的属性:优先级、结合性

操作符的属性&#xff1a;优先级、结合性 优先级结合性 C语言的操作符有2个重要的属性&#xff1a;优先级、结合性&#xff0c;这两个属性决定了表达式求值的计算顺序。 优先级 优先级指的是&#xff0c;如果⼀个表达式包含多个运算符&#xff0c;哪个运算符应该优先执行。各…

RabbitMQ3.x之一_WindowServer2019中安装RabbitMQ详细教程

RabbitMQ3.x之一_WindowServer2019中安装RabbitMQ详细教程 文章目录 RabbitMQ3.x之一_WindowServer2019中安装RabbitMQ详细教程1. 安装环境说明1. WindowServer20192. ErLang与RabbitMQ对应版本 2 安装Erlang1. 安装Erlang2. ErLnag环境变量配置3. 查看是否安装成功 3. 安装Rab…

数据结构面试常见问题之串的模式匹配(KMP算法)系列-大师改进

&#x1f600;前言 KMP算法是一种改进的字符串匹配算法&#xff0c;由D.E.Knuth&#xff0c;J.H.Morris和V.R.Pratt提出&#xff0c;因此人们称它为克努特—莫里斯—普拉特操作&#xff08;简称KMP算法&#xff09; KMP算法的优势: 提高了匹配效率&#xff0c;时间复杂度为O(m…

【C++】用哈希桶模拟实现unordered_set和unordered_map

目录 一、哈希介绍1.1 哈希概念1.2 哈希冲突解决1.2.1 闭散列1.2.2 开散列 二、哈希桶2.1 实现哈希桶2.1.1 构造节点和声明成员变量2.1.2 构造与析构2.1.3 仿函数2.1.4 查找2.1.5 插入2.1.6 删除 2.2 kv模型哈希桶源代码 三、改造哈希桶3.1 beginend3.2 迭代器3.2.1 前置 3.3 改…

Linux_ubuntu中进行断点调试

文章目录 一、安装gdb调试器&#xff1a;二、使用gcc编译程序&#xff1a;三、使用gdb对程序进行调试&#xff1a;1.设置断点&#xff1a;使用break命令或简写为b来设置断点2.调试运行——run&#xff1a;3.继续执行——continue/c&#xff1a;4.单步执行&#xff1a;5.监视变量…

6.windows ubuntu 子系统 测序数据质量控制。

上一个分享&#xff0c;我们对测序数据进行了质量评估&#xff0c;接下来我们需要对数据进行数据质量控制。 数据预处理&#xff08;Data Preprocessing&#xff09;&#xff1a;包括去除接头序列&#xff08;adapter trimming&#xff09;、去除低质量序列&#xff08;qualit…

【Spring Boot 源码学习】共享 MetadataReaderFactory 上下文初始化器

《Spring Boot 源码学习系列》 共享 MetadataReaderFactory 上下文初始化器 一、引言二、往期内容三、主要内容3.1 源码初识3.2 CachingMetadataReaderFactoryPostProcessor3.2.1 register 方法3.2.1 configureConfigurationClassPostProcessor 方法 3.3 ConfigurationClassPos…

SpringMVC结合设计模式:解决MyBatisPlus传递嵌套JSON数据的难题

&#x1f389;&#x1f389;欢迎光临&#xff0c;终于等到你啦&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;持续更新的专栏《Spring 狂野之旅&#xff1a;从入门到入魔》 &a…

Learn OpenGL 24 点光源阴影

点光源阴影 上个教程我们学到了如何使用阴影映射技术创建动态阴影。效果不错&#xff0c;但它只适合定向光&#xff0c;因为阴影只是在单一定向光源下生成的。所以它也叫定向阴影映射&#xff0c;深度&#xff08;阴影&#xff09;贴图生成自定向光的视角。 本节我们的焦点是…

Java进阶—GC回收(垃圾回收)

1. 什么是垃圾回收 垃圾回收(Garbage Collection&#xff0c;GC)是Java虚拟机(JVM)的一项重要功能&#xff0c;用于自动管理程序中不再使用的内存。在Java中&#xff0c;程序员不需要手动释放内存&#xff0c;因为GC会自动检测并回收不再使用的对象&#xff0c;从而减少内存泄…

Java基础【上】韩顺平(反射、类加载、final接口、抽象类、内部类)

涵盖知识点&#xff1a;反射、类加载、单例模式、final、抽象类、接口、内部类&#xff08;局部内部类、匿名内部类、成员内部类、静态内部类&#xff09; P711 反射机制原理 创建如下目录结构&#xff0c;在模块下创建src文件夹&#xff0c;文件夹要设置为Sources文件夹&…

Git使用:实现文件在不同设备之间进行同步

一、注册Gitee&#xff0c;创建远程仓库 注册网址&#xff1a;登录 - Gitee.com 打开Gitee&#xff0c;注册完进行登录&#xff0c;点击右上角【】创建一个仓库 新建仓库&#xff1a; 点击创建&#xff0c;仓库创建完毕。 二、下载Git安装包&#xff0c;并创建本地仓库 下载网…

正则表达式具体用法大全~持续更新

# 正则表达式&#xff1a; ## 单字符匹配&#xff1a; python # 匹配某个字符串&#xff1a; # text "abc" # ret re.match(b,text) # print(ret.group()) # 点&#xff08;.&#xff09;&#xff1a;匹配任意的字符(除了\n)&#xff1a; # text "\nabc&quo…

day02_mysql-DDLDMLDQL_课后练习 - 参考答案

文章目录 day02_mysql_课后练习第1题第2题 day02_mysql_课后练习 第1题 案例&#xff1a; 1、创建数据库test02_library 2、创建表格books 字段名字段说明数据类型b_id书编号int(11)b_name书名varchar&#xff08;50&#xff09;authors作者varchar(100)price价格floatpub…

【C语言】——指针四:字符指针与函数指针变量

【C语言】——指针四&#xff1a;字符指针与函数指针变量 一、字符指针二、函数指针变量2.1、 函数指针变量的创建2.2、两段有趣的代码 三、typedef关键字3.1、typedef的使用3.2、typedef与define比较 四、函数指针数组 一、字符指针 在前面的学习中&#xff0c;我们知道有一种…

FaceBook广告账号验证教程

1.登录facebook账号,点击左边的ads manager。 2.点击Create ad创建广告。 3.选择广告投放意向。 4.填写广告信息。 5.创建广告后选择付款方式&#xff0c;这里我是使用信用卡付款。这里我是使用Fomepay的虚拟卡进行绑定的。 6.填写信用卡的持卡人姓名 卡号 有效期 安全码 7.填写…

【Jenkins】群晖 配置 ssh over push 插件

群晖 配置 ssh over push 插件 前提 部署好 Jenkins 且 安装 好 ssh over push 插件 开启 群晖 ssh 服务 及 SFTP 服务 配置 Jenkins Jenkins ——系统管理——publish over ssh 测试下&#xff1a; 遇到的问题&#xff1a; jenkins.plugins.publish_over.BapPublishe…

数据容器-序列-集合-Python

师从黑马程序员 序列 序列的常用操作-切片 切片&#xff1a;从一个序列中&#xff0c;取出一个子序列 语法&#xff1a;序列[起始下标:结束下标&#xff0c;步长] 注&#xff1a;此操作不会影响序列本身&#xff0c;而是会得到一个新的序列 my_list[0.1,2,3,4,5,6] result1…

LeetCode-热题100:79. 单词搜索

题目描述 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c;其中“相邻”单元格是那些水平相…

解决长尾问题,BEV-CLIP:自动驾驶中复杂场景的多模态BEV检索方法

解决长尾问题&#xff0c;BEV-CLIP&#xff1a;自动驾驶中复杂场景的多模态BEV检索方法 理想汽车的工作&#xff0c;原文&#xff0c;BEV-CLIP: Multi-modal BEV Retrieval Methodology for Complex Scene in Autonomous Driving 链接&#xff1a;https://arxiv.org/pdf/2401.…