| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> 当C++遇到空指针异常...... -> 正文阅读 |
|
[Java知识库]当C++遇到空指针异常...... |
在编程实践中,空指针引起的错误屡见不鲜,指针解引用时遇到了空指针,说明程序有严重的错误,底层一般会通过某种机制通知上层模块,抛出空指针异常就是一种常见的方式。比如,在Java语言中就有空指针异常,如果程序在运行过程中,对空指针进行了访问,JVM就会抛出一个 NullPointerException 类型的异常,表示程序访问了空指针。如果程序不对这个异常进行处理,程序一般会崩溃,导致JVM进程终止。因此,在编程时为了代码安全,防止程序崩溃,并且能够从中恢复正常的话,一般会捕捉这个空指针异常并进行恢复处理。 比如下面就是一段捕获 NullPointerException 异常的 Java 代码片段:
当 我们知道, C++ 也支持异常,那么如果遇到了空指针,能否像 Java 那样捕获空指针异常呢?先编写一段代码测试一下:
因为p是一个空指针,对它进行解引用,肯定会导致内存违法访问。如果编译后并运行,会发现程序会崩溃:Segmentation fault (core dumped)。可见,语句块 这是为什么呢? 我们知道,空指针实际上指向的是虚拟内存地址为 0 的位置,它是一个特殊的位置,操作系统内核是不会为应用程序在这个 0 地址上分配物理内存页的。因此当应用进程访问这个位置时,内核不会像访问常规内存那样:发现该处地址没有分配物理页面,会产生一个缺页异常,然后异常处理程序为它分配一个物理页,并建立页表项,而是直接向进程抛出一个内存段错误的信号: 我们不妨做个实验,在 Linux 环境下编写一个信号处理函数来处理 SIGSEGV 信号,并修改前面的 main 函数,看看会发生什么?代码如下:
程序运行时会输出:signal no:11,编号为11的信号正是 但仅仅判断是否访问了空指针还不够,还得要想法让程序从内存违例的异常中恢复正常才有意义。 我们先看一下常规操作是怎么规避空指针风险的,为了便于说明问题,可以设想这样一个例子,假设有一个函数,它的功能是统计一个整型指针数组的各个数组成员,并计算它们所指向的整数值的和。
sum() 的参数 array 数组,它里面存放的数据成员是整型指针,因为它是作为输入参数由外面传入进来的,不能保证里面没有空指针,为了加固程序的健壮性,一般会进行防御性编程,比如在每次解引用指针前,先判断是否是空指针,如果是,就忽略不计。 因此,在 sum() 函数内的 while 循环中,需要每次判断从数组取得的元素是否是空指针。
虽然传入的数组参数包含空指针的概率极低,但为了安全起见,这个过程仍不得不进行。概率很低,但又不得不用,就像一块狗皮膏药一样贴在那儿,而且几乎就是全程在做无用功,那么,在保证代码安全的前提下,有没有方法来去掉这个发生概率很低的逻辑判断? 如果要达到这个目的,一个是要能够检测到空指针,显然可以使用前面介绍的 SIGSEGV 信号处理机制来实现,另一个是检测到空指针之后,能让程序跳过这个指针,让程序不再访问它就行了。当然,检查空指针不能在while循环中, 如果每次循环都有额外的开销,还不如直接使用if语句判断呢! 可见,方案的关键使用一定的方法跳过这条空指针,即如何在信号 handler() 中来通知函数 sum() 遇到了空指针,在下一次循环时跳过这个空指针?Linux 系统中为信号机制提供了一对函数:siglongjmp() 和 sigsetjmp(),它们实现了信号处理程序的流程进行非局部跳转的功能(所谓非局部跳转是指可以从一个函数内直接跳转到另一个函数的内部某处位置)。可以在检测到空指针时,使用它们让程序指令跳转到预定的目标地址,从而跳过那段访问空指针的代码,接着使用 C++ 的异常机制,通过 throw 一个异常的方式通知上层调用模块。 修改代码如下:
程序运行结果如下: meet the invalid ptr 该程序的关键在于siglongjmp() 和 sigsetjmp()的组合使用,sigsetjmp(jmpbuf, 1) 用来设置程序跳转的目标处,调用它时,会把此处的上下文信息保存在 jmpbuf 参数中,并返回 0,说明不是从 siglongjmp() 跳过来的。当发生 SIGSEGV 异常时,调用信号处理程序 handler,它调用 siglongjmp(jmpbuf, 1) 时不再从handler中返回,而是直接跳转到 jmpbuf 参数保存的目标地址处,也就是 sigsetjmp() 的返回处,同时让第二个参数1从 sigsetjmp() 处作为它的返回值,程序流程转到此处继续运行,因为返回值不为 0,说明是从 handle r跳过来的,即发生了 SIGSEGV 异常,抛出 invalid_ptr 异常。 上述例子虽然程序遇到异常没有让程序崩溃,只是粗暴的放弃,比较生硬,并没有任何恢复的逻辑操作。有没有更好的方案呢?比如检测到空指针之后,不是直接抛异常,而是能让程序跳过这个指针,从下一个数组成员开始,显然这是比较好的一种方案,也就是能从异常中恢复正常。 修改代码如下:
编译并执行这段代码,输出的 log 如下: meet the invalid ptr 根据前面的分析,当发生了 SIGSEGV 异常之后,程序流程跳转到 sigsetjmp() 的返回位置,然后继续执行,此时,数组索引再次加1之后,刚好跳过了空指针的位置,也就忽略了此处的空指针。 在main()函数中,还专门模拟了一个“野指针”,即array数组中的第四项: 程序的核心功能是 while 循环块,代码非常利索,没有了判断空指针的逻辑,去掉了那块狗皮膏药,也没有增加任何开销。当然准备和收尾工作还是有开销的,但它们只执行一次,而开销最大的核心功能部分降低了的开销,但前面的方案,不管有没有空指针,每次循环时都有进行一次空指针判断的开销。如果不发生内存段错误事件,每次循环操作它都会痛痛快快地执行完,毫不拖泥带水,中间没有任何逻辑判断,也就没有跳转指令来打断指令流水线,只有当发生了内存段错误(当然概率很低),才会有额外的开销。如果发生了内存段错误,会触发信号的处理机制,并通过 siglongjmp 跳转到一个正确的位置继续执行,相当于程序在运行过程中发生异常后跑飞了,siglongjmp 又把它拉回到正常的 sigsetjmp 轨道上,避免了程序崩溃。 当然,为了说明问题,这个例子中有许多指针在循环中遍历,如果只有一个指针被访问,直接使用 if 语句来判断空指针显然是最简单的方案。何况这个例子使用了 sigsetjmp 和 siglongjmp,让程序从一个函数的内部直接跳转到另一个函数的内部,有点黑科技的味道,违反了结构化编程,代码让人不易理解,而且容易出现 bug。大家可能也注意到了它的局部变量 i 和 sum 都使用volatile修饰了,使用voaltile修饰局部变量在一定程度上有性能损失,如果不加以修饰,编译时如果打开优化选项,如-O2,这些局部变量可能会优化掉,替换为寄存器,当使用 longjmp 跳转时,会用 jmpbuf 里面存放的寄存初始值来设置这些寄存器,导致程序状态不一致。其次,移植性也不好,在Windows、Linix、Unix平台的信号处理机制有一些差异性。因此,并不提倡使用,在本例中,使用逻辑判断空指针是最简单也最容易理解的方法,当然,如果能够保证程序没有错误,在某些应用场合也不失为一种优雅的解决方案。… |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 2:01:56- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |