前言
开发人员都知道,没有什么比程序运行崩溃更让人头疼的了,特别是发生崩溃的场景比较随机时,更是很难排查。
本篇文章就介绍一下C语言程序发生运行崩溃的几种典型场景,希望对大家排查问题有所帮助。
一、运算除0
运算除0分为两种情况,整型数除以0和浮点数除以0,很多人不知道这个结果其实是有差异的。
1、整形除0
整形数除以0,程序将直接运行崩溃。如果你在代码中直接写“/0”,一般来说编译就会直接报错;如果代码中除以一个变量,在运行过程中,该变量等于0,那么程序将直接运行崩溃。
2、浮点除0
与整形数除以0不同的是,浮点数除以0,一般来说程序不会运行崩溃,得到的运算结果会是INF(正无限大)、或-INF(负无限大),各位可以在你的编译环境下试一试。
当然,虽然不会让程序运行崩溃,但是这样的运算结果肯定不是我们想要的。所以,在进行除法运算时,增加除零保护代码是非常重要的。
例如:
针对整型数:
if(int_test_num == 0)
{
int_result = 0;
}
else
{
int_result = int_numerator / int_test_num;
}
针对浮点数:
if(fabs(float_test_num) < 0.000001)
{
float_result = 0;
}
else
{
float_result = float_numerator / float_test_num;
}
二、非法指针
1、空指针
例如:
int *ptr = NULL; //初始化为空指针
printf("%d", *ptr); //看运行环境,这里只是读取引用,不一定会运行崩溃
*ptr = 100; //对空指针赋值写入,一定运行崩溃
2、野指针
例如:
int *ptr; //不初始化,野指针
printf("%d", *ptr); //不一定会运行崩溃,要看野指针具体指向的地址是否合法,若指向非法地址,则运行崩溃
*ptr = 100; //不一定会运行崩溃,要看野指针具体指向的地址是否合法,若指向非法地址,则运行崩溃
三、越界访问
1、数组越界
程序在读写数组元素超出了数组的合法索引范围时,不一定会造成程序运行崩溃,若越界后的地址合法,只是会造成读写结果不符合预期,或者异常改写其他内存数据;但是若超出后访问到非法地址,则会导致程序运行崩溃。
例如:
int a[10];
a[100]=123;
2、指针越界
指针越界和数组越界类似,不一定会造成程序运行崩溃,若越界后的地址合法,只是会造成读写结果不符合预期,或者异常改写其他内存数据;但是若超出后访问到非法地址,则会导致程序运行崩溃。
例如:
int * p;
p=(int *)malloc(10 * sizeof(int));
*(p+100)=123;
四、堆栈溢出
堆栈溢出一般有两个原因:
1、函数递归层次太深
例如:
int test_func(int n)
{
if (n == 1)
return 1;
else
return n + test_func(n - 1);
}
int main(void)
{
printf("%d\n", test_func(100000));
return 0;
}
2、局部变量占用空间太大,超出了栈的范围,造成栈溢出
例如:
int main(void)
{
int a[100000000] = {0};
return 0;
}
五、内存泄露
1、程序在使用动态内存分配函数(如malloc、calloc、realloc)等申请内存空间后,没有及时释放所分配的内存空间。如果内存泄漏的问题较为严重,运行过程中,内存不断飙升,最终内存不足,导致程序运行崩溃。
2、使用已经释放的空间
例如:
int * p;
p=(int *)malloc(5 * sizeof(int));
free(p);
*p=10;
六、非法引用
1、使用未定义的变量、函数
如果某个变量或者函数只有声明,没有定义的原型,那么在引用时将导致程序运行崩溃。(大部分编译器在编译时能发现并报错,有些编译器不一定会报错,可能只是告警,需要注意)
例如:
extern void test_fun(void);
extern int test_var;
int main(void)
{
test_var = 0;// test_var变量没有定义的原型
test_fun();// test_fun函数没有定义的原型
}
2、试图读取一个不存在的文件
例如:
fp=fopen(filename,"r");
fno=fileno(filename);
fsize=filelength(fno);
printf("%s文件打开成功,文件大小%dBytes\n",filename,fsize);
如果filename指定的文件不存在,那么后续的文件操作将会运行崩溃。所以在调用fopen后,我们应该先判断一下fp是否为NULL。
七、修改只读区
1、对只读文件进行写入
例如:
char test_str[10];
FILE *fp;
fp=fopen("e:\\test.txt","r");
fscanf(fp,"%s", test_str);
2、改写常量
一般来说,const修饰的变量为只读,不可更改,编译器一般能检测报错。
需要注意的是,字符串常量,如“char *ptr = “hello world!”;”,也是不可改写的,有些编译器不会报错。
八、陷入死循环
程序在某个地方出现了死循环,使得程序无法继续向下执行。不同于前面的类型,陷入死循环的程序其实并没有运行崩溃,但是无法正常执行其他的正常功能,对于单个任务的软件系统而言,让人感觉和运行崩溃是一样的状态。
总结
以上便是比较常见的C语言程序运行崩溃的原因,可以发现其实大多数都是由于编码习惯不好引起的。建议大家在编码后,借助静态测试工具(如qac、klocwork、testbed、c++test等)进行代码扫描,能在早期就检测出大部分问题,避免在软件提交到测试或者甚至发布后才发现这样严重的问题。以testbed为例,可以参考我的文章《LDRA Testbed系列(一)Testbed软件静态分析_操作指南》。