在编程实践中,空指针引起的错误屡见不鲜,指针解引用时遇到了空指针,说明程序有严重的错误,底层一般会通过某种机制通知上层模块,抛出空指针异常就是一种常见的方式。比如,在Java语言中就有空指针异常,如果程序在运行过程中,对空指针进行了访问,JVM就会抛出一个 NullPointerException 类型的异常,表示程序访问了空指针。如果程序不对这个异常进行处理,程序一般会崩溃,导致JVM进程终止。因此,在编程时为了代码安全,防止程序崩溃,并且能够从中恢复正常的话,一般会捕捉这个空指针异常并进行恢复处理。

比如下面就是一段捕获 NullPointerException 异常的 Java 代码片段:

try {// 业务处理
} catch (NullPointerException e) {e.printStackTrace();// 进行恢复操作
}

try语句块中的代码访问到空指针后,会抛出 NullPointerException ,随后在 catch 语句块捕获这个异常,并在语句块中进行处理。因为代码捕获了这个异常,从而避免了程序发生崩溃。

我们知道, C++ 也支持异常,那么如果遇到了空指针,能否像 Java 那样捕获空指针异常呢?先编写一段代码测试一下:

static void catch_null_except() {int *p = nullptr;try {int x = 100 + *p; } catch (...) {puts("catch exception!");}
}int main() {catch_null_except();
}

因为p是一个空指针,对它进行解引用,肯定会导致内存违法访问。如果编译后并运行,会发现程序会崩溃:Segmentation fault (core dumped)。可见,语句块 catch (...) 并没有捕获到任何异常,由此可见,在 C++ 中是无法捕获空指针异常的!

这是为什么呢?

我们知道,空指针实际上指向的是虚拟内存地址为 0 的位置,它是一个特殊的位置,操作系统内核是不会为应用程序在这个 0 地址上分配物理内存页的。因此当应用进程访问这个位置时,内核不会像访问常规内存那样:发现该处地址没有分配物理页面,会产生一个缺页异常,然后异常处理程序为它分配一个物理页,并建立页表项,而是直接向进程抛出一个内存段错误的信号:SIGSEGV。我们知道,这个信号的缺省处理是终止进程并生成 coredump 文件,因此,当程序访问空指针时,内核会直接终止进程,也就是应用程序根本不会有抛出异常的机会,实际上应用程序压根就不知道它访问了空指针,因为它自己判断不了,抛异常也就无从谈起,所以尽管上述 C++ 程序使用了 catch 语句块,也没有异常可捕捉。同样,如果程序访问一个指向不属于进程地址空间的指针(也可以说是野指针,通常是编程错误造成的),它所指向的内存位置是无效线性地址,同样操作系统内核也会直接产生一个 SIGSEGV 信号,终止进程。

我们不妨做个实验,在 Linux 环境下编写一个信号处理函数来处理 SIGSEGV 信号,并修改前面的 main 函数,看看会发生什么?代码如下:

static void handler(int signo) {std::cout << "signal no:" << signo << std::endl;exit(-1);
}int main() {signal(SIGSEGV, handler);catch_null_except();
}

程序运行时会输出:signal no:11,编号为11的信号正是SIGSEGV#define SIGSEGV 11。可见,尽管 C++ 无法捕获空指针异常,可以借助于信号机制来判断是否发生了空指针引起的段错误异常。

但仅仅判断是否访问了空指针还不够,还得要想法让程序从内存违例的异常中恢复正常才有意义。

我们先看一下常规操作是怎么规避空指针风险的,为了便于说明问题,可以设想这样一个例子,假设有一个函数,它的功能是统计一个整型指针数组的各个数组成员,并计算它们所指向的整数值的和。
如下所示:

int sum(int **array, size_t num) {int i = -1;int sum = 0;while (++i < num) {if (array[i] == NULL) {continue;}sum += *array[i];}return sum;
}int main() {int x=1, y=2, z=3;int *array[4];array[0] = &x;array[1] = NULL; // 空指针 array[2] = &y;array[3] = &z;int s = sum(array, 4);printf("sum=%d\n", s);puts("exit main");
}

sum() 的参数 array 数组,它里面存放的数据成员是整型指针,因为它是作为输入参数由外面传入进来的,不能保证里面没有空指针,为了加固程序的健壮性,一般会进行防御性编程,比如在每次解引用指针前,先判断是否是空指针,如果是,就忽略不计。

因此,在 sum() 函数内的 while 循环中,需要每次判断从数组取得的元素是否是空指针。

if (array[i] == NULL) {continue;
}

虽然传入的数组参数包含空指针的概率极低,但为了安全起见,这个过程仍不得不进行。概率很低,但又不得不用,就像一块狗皮膏药一样贴在那儿,而且几乎就是全程在做无用功,那么,在保证代码安全的前提下,有没有方法来去掉这个发生概率很低的逻辑判断?

如果要达到这个目的,一个是要能够检测到空指针,显然可以使用前面介绍的 SIGSEGV 信号处理机制来实现,另一个是检测到空指针之后,能让程序跳过这个指针,让程序不再访问它就行了。当然,检查空指针不能在while循环中, 如果每次循环都有额外的开销,还不如直接使用if语句判断呢!

可见,方案的关键使用一定的方法跳过这条空指针,即如何在信号 handler() 中来通知函数 sum() 遇到了空指针,在下一次循环时跳过这个空指针?Linux 系统中为信号机制提供了一对函数:siglongjmp() 和 sigsetjmp(),它们实现了信号处理程序的流程进行非局部跳转的功能(所谓非局部跳转是指可以从一个函数内直接跳转到另一个函数的内部某处位置)。可以在检测到空指针时,使用它们让程序指令跳转到预定的目标地址,从而跳过那段访问空指针的代码,接着使用 C++ 的异常机制,通过 throw 一个异常的方式通知上层调用模块。

修改代码如下:

class invalid_ptr {const char *message;
public:invalid_ptr(const char *msg) : message(msg) {}const char *what() const {return message;}
};static sigjmp_buf jmpbuf;
static volatile sig_atomic_t jumpok = 0;static void handler(int signo) {if (jumpok == 0) return;puts("meet the invalid ptr");siglongjmp(jmpbuf, 1);puts("nerver print this message");return;
}int sum(int **array, size_t num) {typedef void (*sighandler_t)(int);sighandler_t old = signal(SIGSEGV, handler);if (sigsetjmp(jmpbuf, 1)) {puts("return to the main loop after skip the invalid pointer");signal(SIGSEGV, old);throw invalid_ptr("exception: dereference a invalid pointer!");} elsejumpok = 1;int sum = 0;for (int i = 0; i < num; i++) {sum += *array[i];}signal(SIGSEGV, old); // 恢复旧的处理方法return sum;
}int main() {int x=1, y=2, z=3;int *array[4];array[0] = &x;array[1] = NULL; // 空指针 array[2] = &y;array[3] = &z;try {int s = sum(array, 4);printf("sum=%d\n", s);} catch (const invalid_ptr &ex) {cout << ex.what() << endl;}puts("exit main");
}

程序运行结果如下:

meet the invalid ptr
return to the main loop after skip the invalid pointer
exception: dereference a invalid pointer!
exit main

该程序的关键在于siglongjmp() 和 sigsetjmp()的组合使用,sigsetjmp(jmpbuf, 1) 用来设置程序跳转的目标处,调用它时,会把此处的上下文信息保存在 jmpbuf 参数中,并返回 0,说明不是从 siglongjmp() 跳过来的。当发生 SIGSEGV 异常时,调用信号处理程序 handler,它调用 siglongjmp(jmpbuf, 1) 时不再从handler中返回,而是直接跳转到 jmpbuf 参数保存的目标地址处,也就是 sigsetjmp() 的返回处,同时让第二个参数1从 sigsetjmp() 处作为它的返回值,程序流程转到此处继续运行,因为返回值不为 0,说明是从 handle r跳过来的,即发生了 SIGSEGV 异常,抛出 invalid_ptr 异常。

上述例子虽然程序遇到异常没有让程序崩溃,只是粗暴的放弃,比较生硬,并没有任何恢复的逻辑操作。有没有更好的方案呢?比如检测到空指针之后,不是直接抛异常,而是能让程序跳过这个指针,从下一个数组成员开始,显然这是比较好的一种方案,也就是能从异常中恢复正常。

修改代码如下:

int sum(int **array, size_t num) {typedef void (*sighandler_t)(int);sighandler_t old = signal(SIGSEGV, handler);volatile int i=-1;volatile int sum = 0;if (sigsetjmp(jmpbuf, 1))puts("return to the main loop after skip the invalid pointer");elsejumpok = 1;while (++i < num) {sum += *array[i];}signal(SIGSEGV, old); // 恢复旧的处理方法return sum;
}int main() {int x=1, y=2;int *array[4];array[0] = &x;array[1] = NULL; // 空指针 array[2] = &y;array[3] = (int *)(0x12345678); // 模拟一个野指针 int s = sum(array, 4);printf("sum=%d\n", s);puts("exit main");
}

编译并执行这段代码,输出的 log 如下:

meet the invalid ptr
return to the main loop after skip the invalid pointer
meet the invalid ptr
return to the main loop after skip the invalid pointer
sum=3
exit main

根据前面的分析,当发生了 SIGSEGV 异常之后,程序流程跳转到 sigsetjmp() 的返回位置,然后继续执行,此时,数组索引再次加1之后,刚好跳过了空指针的位置,也就忽略了此处的空指针。

在main()函数中,还专门模拟了一个“野指针”,即array数组中的第四项:array[3] = (int *)(0x12345678); 加上第二项空指针array[1] = NULL;,程序运行时会发生两次内存段错误,一次空指针,一次野指针,经过信号处理程序接收 SIGSEGV 信号和 siglongjmp 跳转,可以跳过这两个错误项。从输出的 log 中也可以看出,程序先后两次遇到了无效指针的错误,都被程序正确处理了,两个有效的数组元素参与了计算,计算结果 sum=3,正好是这两个元素所指整数的和。可见,该方案同第一个方案相比,它的功能还有所增强,可以判断出某种形式的野指针,并忽略它;同第二个方案相比,遇到错误不是粗暴地把结果丢弃,而是忽略无效的数据,显然这样更符合函数的目的,达到了第一种方案的目的。其次,不依赖于特定语言的异常机制,像C语言也可以使用此方案,调用者对发生异常是无感的,不像前面,还得要使用 try…catch 来捕捉。

程序的核心功能是 while 循环块,代码非常利索,没有了判断空指针的逻辑,去掉了那块狗皮膏药,也没有增加任何开销。当然准备和收尾工作还是有开销的,但它们只执行一次,而开销最大的核心功能部分降低了的开销,但前面的方案,不管有没有空指针,每次循环时都有进行一次空指针判断的开销。如果不发生内存段错误事件,每次循环操作它都会痛痛快快地执行完,毫不拖泥带水,中间没有任何逻辑判断,也就没有跳转指令来打断指令流水线,只有当发生了内存段错误(当然概率很低),才会有额外的开销。如果发生了内存段错误,会触发信号的处理机制,并通过 siglongjmp 跳转到一个正确的位置继续执行,相当于程序在运行过程中发生异常后跑飞了,siglongjmp 又把它拉回到正常的 sigsetjmp 轨道上,避免了程序崩溃。

当然,为了说明问题,这个例子中有许多指针在循环中遍历,如果只有一个指针被访问,直接使用 if 语句来判断空指针显然是最简单的方案。何况这个例子使用了 sigsetjmp 和 siglongjmp,让程序从一个函数的内部直接跳转到另一个函数的内部,有点黑科技的味道,违反了结构化编程,代码让人不易理解,而且容易出现 bug。大家可能也注意到了它的局部变量 i 和 sum 都使用volatile修饰了,使用voaltile修饰局部变量在一定程度上有性能损失,如果不加以修饰,编译时如果打开优化选项,如-O2,这些局部变量可能会优化掉,替换为寄存器,当使用 longjmp 跳转时,会用 jmpbuf 里面存放的寄存初始值来设置这些寄存器,导致程序状态不一致。其次,移植性也不好,在Windows、Linix、Unix平台的信号处理机制有一些差异性。因此,并不提倡使用,在本例中,使用逻辑判断空指针是最简单也最容易理解的方法,当然,如果能够保证程序没有错误,在某些应用场合也不失为一种优雅的解决方案。…

当C++遇到空指针异常......相关推荐

  1. 面试官:你写的单例模式有空指针异常,请你用Volatile改一下。我愣了五分钟...

    1 单例模式 大家对单例模式并不会陌生,当创建一个对象需要消耗比较多资源时,例如创建数据库连接和消息服务端等等,这时我们选择只创建一份这种类型的对象并在进程内共享. 但是单例模式想要写好并不容易,我们 ...

  2. 使用Java 8 Optional避免空指针异常

    2019独角兽企业重金招聘Python工程师标准>>> Optional可以让你的代码具有可读性,且会避免出现空指针异常. 都说没有遇到过空指针异常的程序员不是Java程序员,nul ...

  3. android 拍照空指针,空指针异常时嵌入照片中的Android

    分贝的android我是做一个应用程序在SQLite数据库中插入数据和ii有一个空指针异常时记录添加到数据库空指针异常时嵌入照片中的Android 这个初学者代码得到的ImageView的PIC,并将 ...

  4. getBytes等空指针异常

    在使用getBytes这个方法进行字符编码转换的时候回发生空指针异常,困扰了我近30分钟.最终得出如下解决方案,直接扔代码,相信各位兄台一定能看懂. String flag2 = req.getPar ...

  5. 教你怎么样在 Java8 中优雅的避开空指针异常

    2019独角兽企业重金招聘Python工程师标准>>> 要说 Java 编程中哪个异常是你印象最深刻的,那 NullPointerException 空指针可以说是臭名昭著的.不要说 ...

  6. 将Integer赋值给int(空指针异常)

    将Integer赋值给int(空指针异常) 参考文章: (1)将Integer赋值给int(空指针异常) (2)https://www.cnblogs.com/shuaiandjun/p/956892 ...

  7. spring-boot框架dubbo在controlle中r注解@Reference注入service,但是调用方法时候,service报null空指针异常

    spring-boot框架dubbo在controlle中r注解@Reference注入service,但是调用方法时候,service报null空指针异常 参考文章: (1)spring-boot框 ...

  8. 轻松搞定项目中的空指针异常Caused by: java.lang.NullPointerException: null

    轻松搞定项目中的空指针异常Caused by: java.lang.NullPointerException: null 参考文章: (1)轻松搞定项目中的空指针异常Caused by: java.l ...

  9. BigDecimal空指针异常——个人应用

    BigDecimal空指针异常--个人应用 参考文章: (1)BigDecimal空指针异常--个人应用 (2)https://www.cnblogs.com/sun7897/p/10103352.h ...

  10. Android NullPointerException解决方法(空指针异常)

    不知道你在开发中遇到Android空指针异常NullPointerException相关的问题,这类问题的可能性比较多,常规的错误如下 一.刚刚升级了Android SDK,没有对emulator做清 ...

最新文章

  1. “误用姓名”,前哈佛教授炮轰中国学者“碰瓷”:“整件事都让人讨厌!
  2. PS调出通透唯美阳光外景女生照片
  3. 012_HttpServletResponse响应乱码
  4. Shorten command line 解决方案
  5. exception:Deleted row information cannot be accessed through the row处理
  6. 活动目录实战系列七(降级主DC为成员服务器)
  7. java mouselistener,Java MouseListener接口
  8. Android 适配(一)
  9. java quartz spring_JavaLib-quartz | 基于Spring Boot Quartz开发的定时任务
  10. 与指定数字相同的数的个数(信息学奥赛一本通-T1102)
  11. GTK的.NET的函数库 GTK#
  12. SPRING IN ACTION 第4版笔记-第四章ASPECT-ORIENTED SPRING-008-带参数的ADVICE
  13. FeignClient与RestTemplate的区别
  14. 金典 SQL笔记(2)
  15. Sublime Text for Mac如何支持GBK编码
  16. 计算机投诉信英语作文,一封投诉信英语作文(精选5篇)
  17. uniapp:轮播里如何加入视频
  18. 关于C语言中自增自减
  19. python侯先生爬楼梯问题_python的算法
  20. 离散型均匀分布 连续型均匀分布

热门文章

  1. Bootstrap基本结构
  2. html 纵向滚动条,css设置纵向滚动条
  3. CDA数据分析师认证辅导课
  4. Linux驱动开发中的中间件:设备树
  5. 看完了小米12发布会,连夜写下这篇JavaScript笔记
  6. 雷迪9000使用说明_雷迪司UPS监控软件使用说明中文
  7. PHP全站pjax影响收录,zblogPHP增加pjax功能,大写的一个“帅”字 - 胡言乱语
  8. App首次进入引导界面
  9. 您的美团账户,美团互助未经客户同意自动扣费0.01元是什么情况
  10. hub_probe()