人人都写过的五个Bug!

开发 后端
本文就盘点一下学习或使用 C 语言过程中,非常容易出现的 5 个 Bug,以及如何规避这些 Bug。

[[431448]]

 大家好,我是良许。

计算机专业的小伙伴,在学校期间一定学过 C 语言。它是众多高级语言的鼻祖,深入学习这门语言会对计算机原理、操作系统、内存管理等等底层相关的知识会有更深入的了解,所以我在直播的时候,多次强调大家一定要好好学习这门语言。

但是,即使是最有经验的程序员也会写出各种各样的 Bug。本文就盘点一下学习或使用 C 语言过程中,非常容易出现的 5 个 Bug,以及如何规避这些 Bug。

这篇文章主要面向初学者,老鸟可以忽略哈(其实不少老鸟依然还会犯这些低级错误哦)~

1. 变量未初始化

当程序启动时,系统会给它自动分配一块内存,程序可以用它来存储数据。所以如果你在定义一个变量时,在未初始化的情况下,它的值有可能是任意的。

但这也不是绝对的,有些环境就会在程序启动时自动将内存「清零」,因此每个变量默认值都是零。考虑到可移植性,最好要将变量进行初始化,这是一名合格软件工程师应该养成的好习惯。

我们来看下下面这个使用几个变量和两个数组的示例程序: 

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. int main()  
  4.  
  5.   int i, j, k;  
  6.   int numbers[5];  
  7.   int *array;  
  8.   puts("These variables are not initialized:");  
  9.   printf("  i = %d\n", i);  
  10.   printf("  j = %d\n", j);  
  11.   printf("  k = %d\n", k);  
  12.   puts("This array is not initialized:");  
  13.   for (i = 0; i < 5; i++) {  
  14.     printf("  numbers[%d] = %d\n", i, numbers[i]);  
  15.   }  
  16.   puts("malloc an array ...");  
  17.   array = malloc(sizeof(int) * 5);  
  18.   if (array) {  
  19.     puts("This malloc'ed array is not initialized:");  
  20.     for (i = 0; i < 5; i++) {  
  21.       printf("  array[%d] = %d\n", i, array[i]);  
  22.     }  
  23.     free(array);  
  24.   }  
  25.   /* done */  
  26.   puts("Ok");  
  27.   return 0; 
  28.  

这段程序没有对变量进行初始化,所以变量的值有可能是随机的,不一定是零。在我的电脑上它的运行结果如下 : 

  1. These variables are not initialized:  
  2.   i = 0  
  3.   j = 0  
  4.   k = 32766  
  5. This array is not initialized:  
  6.   numbers[0] = 0  
  7.   numbers[1] = 0  
  8.   numbers[2] = 4199024  
  9.   numbers[3] = 0  
  10.   numbers[4] = 0  
  11. malloc an array ...  
  12. This malloc'ed array is not initialized:  
  13.   array[0] = 0  
  14.   array[1] = 0  
  15.   array[2] = 0  
  16.   array[3] = 0  
  17.   array[4] = 0  
  18. Ok 

从结果可以看出,i 和 j 的值刚好是 0,但 k 值为 32766。在 numbers 数组中,大多数元素也恰好是零,除了第三个(4199024)。

在不同的操作系统上编译这段相同的程序,运行的结果有可能又是不一样的。所以千万不要觉得你的结果就是正确唯一的,一定要考虑可移植性。

例如,这是在 FreeDOS 上运行的相同程序的结果: 

  1. These variables are not initialized:  
  2.   i = 0  
  3.   j = 1074  
  4.   k = 3120  
  5. This array is not initialized:  
  6.   numbers[0] = 3106  
  7.   numbers[1] = 1224  
  8.   numbers[2] = 784  
  9.   numbers[3] = 2926  
  10.   numbers[4] = 1224  
  11. malloc an array ...  
  12. This malloc'ed array is not initialized:  
  13.   array[0] = 3136  
  14.   array[1] = 3136  
  15.   array[2] = 14499  
  16.   array[3] = -5886  
  17.   array[4] = 219  
  18. Ok 

可以看出来,运行的结果跟上面几乎是天差地别。所以,对变量进行初始化将为你省去很多不必要的麻烦,也便于将来的调试。

2. 数组越界

在计算机世界里,都是从 0 开始计数,但总有人有意无意忘记这点。比如一个数组长度为 10 ,想要获取最后一个元素的值,总有人用 array[10] ……

别问,问就是我写过……

新手朋友犯这种低级错误特别多。我们来看下数组越界会发生什么。 

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. int main()  
  4.  
  5.   int i;  
  6.   int numbers[5];  
  7.   int *array;  
  8.   /* test 1 */  
  9.   puts("This array has five elements (0 to 4)");  
  10.   /* initalize the array */  
  11.   for (i = 0; i < 5; i++) {  
  12.     numbers[i] = i;  
  13.   }  
  14.   /* oops, this goes beyond the array bounds: */  
  15.   for (i = 0; i < 10; i++) {  
  16.     printf("  numbers[%d] = %d\n", i, numbers[i]);  
  17.   }  
  18.   /* test 2 */  
  19.   puts("malloc an array ...");  
  20.   array = malloc(sizeof(int) * 5);  
  21.   if (array) {  
  22.     puts("This malloc'ed array also has five elements (0 to 4)");  
  23.     /* initalize the array */  
  24.     for (i = 0; i < 5; i++) {  
  25.       array[i] = i;  
  26.     }  
  27.     /* oops, this goes beyond the array bounds: */  
  28.     for (i = 0; i < 10; i++) {  
  29.       printf("  array[%d] = %d\n", i, array[i]);  
  30.     }  
  31.     free(array);  
  32.   }  
  33.   /* done */  
  34.   puts("Ok");  
  35.   return 0;  

请注意,程序初始化了数组 numbers 所有元素的值(0~4),但是越界读取了第 0~9 元素的值。可以看出来,前五个值是正确的,但之后鬼都不知道这些值会是什么: 

  1. This array has five elements (0 to 4)  
  2.   numbers[0] = 0  
  3.   numbers[1] = 1  
  4.   numbers[2] = 2  
  5.   numbers[3] = 3  
  6.   numbers[4] = 4  
  7.   numbers[5] = 0  
  8.   numbers[6] = 4198512  
  9.   numbers[7] = 0  
  10.   numbers[8] = 1326609712  
  11.   numbers[9] = 32764  
  12. malloc an array ...  
  13. This malloc'ed array also has five elements (0 to 4)  
  14.   array[0] = 0  
  15.   array[1] = 1  
  16.   array[2] = 2  
  17.   array[3] = 3  
  18.   array[4] = 4  
  19.   array[5] = 0  
  20.   array[6] = 133441  
  21.   array[7] = 0  
  22.   array[8] = 0  
  23.   array[9] = 0  
  24. Ok 

所以大家在写代码过程中,一定要知道数组的边界。像这种数据读取的还好,如果一旦对这些内存进行写操作,直接就 core dump !

3. 字符串溢出

在 C 编程语言中,字符串是一组 char 值,也可以将其视为数组。因此,你也需要避免超出字符串的范围。如果超出,则称为字符串溢出。

为了测试字符串溢出,一种简单方法是使用 gets 函数读取数据。gets 函数非常危险,因为它不知道接收它的字符串中可以存储多少数据,只会天真地从用户那里读取数据。

如果用户输入字符串比较短那很好,但如果用户输入的值超过接收字符串的长度,则可能是灾难性的。

下面我们来演示一下这个现象: 

  1. #include <stdio.h>  
  2. #include <string.h>  
  3. int main()  
  4.  
  5.   char name[10];                       /* Such as "Beijing" */  
  6.   int var1 = 1, var2 = 2;  
  7.   /* show initial values */  
  8.   printf("var1 = %d; var2 = %d\n", var1, var2);  
  9.   /* this is bad .. please don't use gets */  
  10.   puts("Where do you live?");  
  11.   gets(name);  
  12.   /* show ending values */  
  13.   printf("<%s> is length %d\n", name, strlen(name));  
  14.   printf("var1 = %d; var2 = %d\n", var1, var2);  
  15.   /* done */  
  16.   puts("Ok");  
  17.   return 0;  

在这段代码里,接收数组的长度为 10 ,所以当输入数据长度小于 10 的话,程序运行就没问题。

例如,输入城市 Beijing ,长度为 7 : 

  1. var1 = 1; var2 = 2  
  2. Where do you live?  
  3. Beijing  
  4. <Beijing> is length 7 
  5. var1 = 1; var2 = 2  
  6. Ok 

威尔士小镇 Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch 是世界上名字最长的城市,这个字符串有 58 个字符,远远超出了 name 变量中可保留的 10 个字符。

如果输入这个字符串,其结果是程序运行内存的其它位置,比如 var1和var2 ,都有可能被波及: 

  1. var1 = 1; var2 = 2  
  2. Where do you live?  
  3. Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch  
  4. <Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch> is length 58  
  5. var1 = 2036821625var2 = 2003266668  
  6. Ok  
  7. Segmentation fault (core dumped) 

在中止之前,程序使用长字符串覆盖内存的其他部分。请注意,var1 和 var2 不再是它们的起始值 1 和 2 。

所以我们需要使用更安全的方法来读取用户数据。例如,getline 函数就是一个不错的选择,它将分配足够大的内存来存储用户输入,因此用户不会因输入太长字符串而意外溢出。

4. 内存重复释放

良好的 C 编程规则之一是,如果分配了内存,就一定要将其释放。

我们可以使用 malloc 函数为数组和字符串申请内存,系统将开辟一块内存并返回一个指向该内存起始地址的指针。内存使用完毕后,我们一定要记得使用 free 函数释放内存,然后系统将该内存标记为未使用。

但是,这个过程中,你只能调用 free 函数一次。如果你第二次调用 free 函数,将导致意外行为,而且可能会破坏你的程序。

下面我们举个简单的例子: 

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. int main()  
  4.  
  5.   int *array;  
  6.   puts("malloc an array ...");  
  7.   array = malloc(sizeof(int) * 5);  
  8.   if (array) {  
  9.     puts("malloc succeeded");  
  10.     puts("Free the array...");  
  11.     free(array);  
  12.   }  
  13.   puts("Free the array...");  
  14.   free(array);  
  15.   puts("Ok");  

运行此程序会导致第二次调用 free 函数时出现 core dump 错误: 

  1. malloc an array ...  
  2. malloc succeeded  
  3. Free the array...  
  4. Free the array...  
  5. free(): double free detected in tcache 2  
  6. Aborted (core dumped) 

那么怎么避免多次调用 free 函数呢?一个最简单的方法就是将 malloc 和 free 语句放在一个函数里。

如果你将 malloc 放在一个函数里,而将 free 放在另一个函数里,那么,在使用的过程中,如果逻辑设计不恰当,都有可能出现 free 被调用多次的情况。

5. 使用无效的文件指针

文件是操作系统里一种非常常见的数据存储方式。例如,您可以将程序的配置信息存储在名为 config.dat 文件里,程序运行时,就可以调用这个文件,读取配置信息。

因此,从文件中读取数据的能力对所有程序员都很重要。但是,如果你要读取的文件不存在怎么办?

在 C 语言中,要读取文件一般是先使用 fopen 函数打开文件,然后该函数返回指向文件的流指针。

如果您要读取的文件不存在或您的程序无法读取,则 fopen 函数将返回 NULL 。在这种情况下,我们仍然对其进行操作,会发生什么情况?我们一起来看下: 

  1. #include <stdio.h>  
  2. int main()  
  3.  
  4.   FILE *pfile;  
  5.   int ch;  
  6.   puts("Open the FILE.TXT file ..."); 
  7.   pfile = fopen("FILE.TXT", "r");  
  8.   /* you should check if the file pointer is valid, but we skipped that */  
  9.   puts("Now display the contents of FILE.TXT ...");  
  10.   while ((ch = fgetc(pfile)) != EOF) {  
  11.     printf("<%c>", ch);  
  12.   }  
  13.   fclose(pfile);  
  14.   /* done */ 
  15.   puts("Ok");  
  16.   return 0;  

当你运行这个程序时,如果 FILE.TXT 这个文件不存在,那么 pfile 将返回 NULL。在这种情况下我们还对 pfile 进行写操作的话,会立刻导致 core dump : 

  1. Open the FILE.TXT file ...  
  2. Now display the contents of FILE.TXT ...  
  3. Segmentation fault (core dumped) 

所以,我们要始终检查文件指针是否有效。例如,在调用 fopen 函数打开文件后,使用 if (pfile != NULL) 以确保指针是可以使用的。

小结

再有经验的程序员都有可能犯错误,所以写代码的时候我们要严谨再严谨。但是,如果你养成一些良好的习惯,并添加一些额外的代码来检查这五种类型的错误,则可以避免严重的 C 编程错误。 

 

责任编辑:庞桂玉 来源: 良许Linux
相关推荐

2013-03-12 13:52:56

编程

2010-08-25 10:35:31

微软

2010-08-26 17:24:47

2016-08-23 01:03:17

2021-12-04 23:10:02

Java代码开发

2019-05-23 09:30:22

网络框架数据

2021-11-18 23:33:17

API 抽象桌面

2009-03-19 10:16:06

2015-01-23 10:04:56

bug程序员

2011-08-25 22:57:42

惠普喷墨打印机

2024-08-02 16:32:15

2015-12-15 09:42:52

TCP网络协议

2019-02-14 13:24:02

大数据人工智能医疗

2015-03-13 10:40:37

2014-11-14 14:03:17

微软安全漏洞bug

2014-05-21 16:11:53

2021-09-13 15:54:01

程序员技能开发者

2023-10-23 12:28:45

模型研究

2012-09-05 10:18:11

可视化编程工具程序员

2022-02-21 09:20:27

谷歌漏洞修复
点赞
收藏

51CTO技术栈公众号