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

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

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

1. 变量未初始化

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

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

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

#include <stdio.h>
#include <stdlib.h>int main()
{int i, j, k;int numbers[5];int *array;puts("These variables are not initialized:");printf("  i = %d\n", i);printf("  j = %d\n", j);printf("  k = %d\n", k);puts("This array is not initialized:");for (i = 0; i < 5; i++) {printf("  numbers[%d] = %d\n", i, numbers[i]);}puts("malloc an array ...");array = malloc(sizeof(int) * 5);if (array) {puts("This malloc'ed array is not initialized:");for (i = 0; i < 5; i++) {printf("  array[%d] = %d\n", i, array[i]);}free(array);}/* done */puts("Ok");return 0;
}

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

These variables are not initialized:i = 0j = 0k = 32766
This array is not initialized:numbers[0] = 0numbers[1] = 0numbers[2] = 4199024numbers[3] = 0numbers[4] = 0
malloc an array ...
This malloc'ed array is not initialized:array[0] = 0array[1] = 0array[2] = 0array[3] = 0array[4] = 0
Ok

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

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

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

These variables are not initialized:i = 0j = 1074k = 3120
This array is not initialized:numbers[0] = 3106numbers[1] = 1224numbers[2] = 784numbers[3] = 2926numbers[4] = 1224
malloc an array ...
This malloc'ed array is not initialized:array[0] = 3136array[1] = 3136array[2] = 14499array[3] = -5886array[4] = 219
Ok

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

2. 数组越界

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

别问,问就是我写过……

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

#include <stdio.h>
#include <stdlib.h>int main()
{int i;int numbers[5];int *array;/* test 1 */puts("This array has five elements (0 to 4)");/* initalize the array */for (i = 0; i < 5; i++) {numbers[i] = i;}/* oops, this goes beyond the array bounds: */for (i = 0; i < 10; i++) {printf("  numbers[%d] = %d\n", i, numbers[i]);}/* test 2 */puts("malloc an array ...");array = malloc(sizeof(int) * 5);if (array) {puts("This malloc'ed array also has five elements (0 to 4)");/* initalize the array */for (i = 0; i < 5; i++) {array[i] = i;}/* oops, this goes beyond the array bounds: */for (i = 0; i < 10; i++) {printf("  array[%d] = %d\n", i, array[i]);}free(array);}/* done */puts("Ok");return 0;
}

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

This array has five elements (0 to 4)numbers[0] = 0numbers[1] = 1numbers[2] = 2numbers[3] = 3numbers[4] = 4numbers[5] = 0numbers[6] = 4198512numbers[7] = 0numbers[8] = 1326609712numbers[9] = 32764
malloc an array ...
This malloc'ed array also has five elements (0 to 4)array[0] = 0array[1] = 1array[2] = 2array[3] = 3array[4] = 4array[5] = 0array[6] = 133441array[7] = 0array[8] = 0array[9] = 0
Ok

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

3. 字符串溢出

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

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

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

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

#include <stdio.h>
#include <string.h>int main()
{char name[10];                       /* Such as "Beijing" */int var1 = 1, var2 = 2;/* show initial values */printf("var1 = %d; var2 = %d\n", var1, var2);/* this is bad .. please don't use gets */puts("Where do you live?");gets(name);/* show ending values */printf("<%s> is length %d\n", name, strlen(name));printf("var1 = %d; var2 = %d\n", var1, var2);/* done */puts("Ok");return 0;
}

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

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

var1 = 1; var2 = 2
Where do you live?
Beijing
<Beijing> is length 7
var1 = 1; var2 = 2
Ok

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

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

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

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

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

4. 内存重复释放

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

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

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

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

#include <stdio.h>
#include <stdlib.h>int main()
{int *array;puts("malloc an array ...");array = malloc(sizeof(int) * 5);if (array) {puts("malloc succeeded");puts("Free the array...");free(array);}puts("Free the array...");free(array);puts("Ok");
}

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

malloc an array ...
malloc succeeded
Free the array...
Free the array...
free(): double free detected in tcache 2
Aborted (core dumped)

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

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

5. 使用无效的文件指针

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

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

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

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

#include <stdio.h>int main()
{FILE *pfile;int ch;puts("Open the FILE.TXT file ...");pfile = fopen("FILE.TXT", "r");/* you should check if the file pointer is valid, but we skipped that */puts("Now display the contents of FILE.TXT ...");while ((ch = fgetc(pfile)) != EOF) {printf("<%c>", ch);}fclose(pfile);/* done */puts("Ok");return 0;
}

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

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

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

小结

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

上面介绍的 5 种常见错误,你都写过哪些 Bug 呢?

人人都写过的5个Bug!相关推荐

  1. 10万点击率的“干货”,其实人人都写得出来

    如何写一篇分享性质的文章 微博上看过一篇文章,讲的是我们身边的朋友,即使是渣渣也有吊炸天的一面: 英语只知道WTF可能画画很厉害 跟人说话就脸红可能写代码是高手 成绩一塌糊涂,但是确实汽车知识的行家 ...

  2. 阿里程序员写了一个新手都写不出的低级bug,被骂惨了。

    你知道的越多,你不知道的越多 点赞再看,养成习惯 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎Star和 ...

  3. python编程可视化小程序_人人都可以写的可视化Python小程序第二篇:旋转的烟花...

    兴趣是最好的老师 枯燥的编程容易让人放弃,兴趣才是最好的老师.无论孩子还是大人,只有发现这件事情真的有趣,我们才会非常执着的去做这件事,比如打游戏.如果编程能像玩游戏一样变得有趣,我相信很多人就特别愿 ...

  4. 使用python完成的一个烟花小程序-人人都可以写的可视化Python小程序第二篇:旋转的烟花...

    兴趣是最好的老师 枯燥的编程容易让人放弃,兴趣才是最好的老师.无论孩子还是大人,只有发现这件事情真的有趣,我们才会非常执着的去做这件事,比如打游戏.如果编程能像玩游戏一样变得有趣,我相信很多人就特别愿 ...

  5. 【总结整理】写给非技术产品经理的技术能力要求----摘自《人人都是产品经理》...

    写给非技术产品经理的技术能力要求 人人都是产品经理  订阅专栏 产品经理和运营人的学习社区 2018-10-23 2.4万 159 73 从零开始学运营,10年经验运营总监亲授,2天线下集训+1年在线 ...

  6. 人人都可以写的可视化Python小程序第二篇:旋转的烟花

    兴趣是最好的老师 枯燥的编程容易让人放弃,兴趣才是最好的老师.无论孩子还是大人,只有发现这件事情真的有趣,我们才会非常执着的去做这件事,比如打游戏.如果编程能像玩游戏一样变得有趣,我相信很多人就特别愿 ...

  7. python画静态烟花_人人都可以写的可视化Python小程序第二篇:旋转的烟花

    兴趣是最好的老师 枯燥的编程容易让人放弃,兴趣才是最好的老师.无论孩子还是大人,只有发现这件事情真的有趣,我们才会非常执着的去做这件事,比如打游戏.如果编程能像玩游戏一样变得有趣,我相信很多人就特别愿 ...

  8. 人人都爱写总结,却少有人做计划

    每到岁末,人们都会不约而同地.自发地写各式各样的总结.上个月,也就是 2018 年的最后一个月,园子里各处散落着无数篇年终总结贴.看过一些篇章,都写得挺好的. 转眼 2019 年已经运行了大半个月了, ...

  9. android 不通过数据线打印日志_人人都可写代码-Android零基础编程-开发调试、APK编译04...

    欢迎来到人人都可写代码,大家好,我是杨晓华,今天我们的课程内容是,讲解Android开发中如何调试代码程序,开发完后如何打包成可发布的apk文件. 开发调试 在开发中,难免我们会遇到BUG,快速定位问 ...

最新文章

  1. Java网页数据采集器[下篇-数据查询]【转载】
  2. 如何更改linux文件目录拥有者及用户组
  3. C语言(CED)从键盘输入一些整数,求出它们的平均值。数据的个数事先不确定,在输入过程中以ctrl+z键(EOF)作为输入结束的标记。
  4. 电容降压LED驱动电路
  5. strchr,wcschr 及strrchr, wcsrchr,_tcschr,_tcsrchr函数
  6. mysql linux 手动安装mysql服务_Linux手动安装Mysql
  7. PAT-BASIC-1001-害死人不偿命的(3n+1)猜想
  8. Ros系统配置DNS服务器,ros设置dns服务器
  9. 进项税额和销项税额介绍
  10. 安卓微信支付回调出现白页面
  11. 2014年第五届蓝桥杯C++B组第四题:史丰收速算
  12. 服务器端查询客户端cookie时的拙计
  13. Android| failed to connect to /10.0.2.2 (port 80) after 10000ms
  14. 全球首个CTLA-4抑制剂逸沃在中国上市;全球首个原发性轻链型淀粉样变治疗药物兆珂速在华获批 | 医药健闻...
  15. 报表控件Stimulsoft报告中的数据矩阵条形码介绍
  16. Java 来判断手机号码是否已经存在例子
  17. scp 传目录_scp拷贝文件及文件夹
  18. CSS的浮动属性,详细学习指南
  19. Playwright + Python爬虫
  20. 高校教师开计算机培训中心,计算中心

热门文章

  1. python实现gauss-seidel迭代公式_python实现高斯(Gauss)迭代法的例子
  2. php如何操作文本框,php如何清除文本框
  3. 语言输出换行符号是什么_世界上最难的5种编程语言
  4. 详解vue 路由跳转四种方式 (带参数)
  5. 新手学stm32学f1还是f4,stm32初学者最好买哪种开发板?
  6. SQLAlchemy实现插入数据(使用前端页面)
  7. vue中常碰见的坑_Vue 与 Vuex 的第一次接触遇到的坑
  8. BZOJ 2135 刷题计划(贪心,求导,二分)【BZOJ 修复工程】
  9. 【网络流】解题报告:luogu P3376 【模板】网络最大流
  10. 邮件内容会留在服务器吗,outlook的邮件最长可以保存多久,去