人人都写过的5个Bug!

人人都写过的5个Bug!

大家好,我是良许。

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

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

这篇文章主要是为初学者,老鸟可以忽略呵呵(其实很多老鸟还会犯这些低级错误)~

[En]

This article is mainly for beginners, old birds can ignore ha (in fact, many old birds still make these low-level mistakes) ~

1. 变量未初始化

当程序启动时,系统自动为其分配一块内存,程序可以使用该内存来存储数据。因此,如果您定义一个变量,它的值在没有初始化的情况下可能是任意的。

[En]

When the program starts, the system automatically allocates a piece of memory to it, which the program can use to store data. So if you define a variable, its value may be arbitrary without initialization.

但这并不是绝对的,有些环境会在程序启动时自动将内存置零,因此每个变量的缺省值都是零。为了可移植性,最好是初始化变量,这是一个合格的软件工程师应该养成的好习惯。

[En]

But this is not absolute, some environments will automatically “zero” the memory when the program starts, so the default value of each variable is zero. For portability, it is best to initialize variables, which is a good habit that a qualified software engineer should develop.

让我们看一下使用几个变量和两个数组的以下示例程序:

[En]

Let’s take a look at the following example program that uses several variables and two arrays:

#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;
}
</stdlib.h></stdio.h>

该程序不会初始化变量,因此变量的值可能是随机的,不一定是零。在我的电脑上,结果如下:

[En]

This program does not initialize the variable, so the value of the variable may be random, not necessarily zero. On my computer, the results are as follows:

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

This malloc'ed array is not initialized:
  array[0] = 0
  array[1] = 0
  array[2] = 0
  array[3] = 0
  array[4] = 0
Ok

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

如果在不同的操作系统上编译相同的程序,结果可能会不同。因此,不要认为您的结果是正确和唯一的,并确保考虑可移植性。

[En]

If you compile the same program on different operating systems, the results may be different. So don’t feel that your results are correct and unique, and be sure to consider portability.

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

These variables are not initialized:
  i = 0
  j = 1074
  k = 3120
This array is not initialized:
  numbers[0] = 3106
  numbers[1] = 1224
  numbers[2] = 784
  numbers[3] = 2926
  numbers[4] = 1224
malloc an array ...

This malloc'ed array is not initialized:
  array[0] = 3136
  array[1] = 3136
  array[2] = 14499
  array[3] = -5886
  array[4] = 219
Ok

可以看到,手术的结果几乎与上面的大不相同。因此,初始化变量将为您省去许多不必要的麻烦,并方便以后的调试。

[En]

It can be seen that the results of the operation are almost very different from those above. Therefore, initializing variables will save you a lot of unnecessary trouble and facilitate future debugging.

2. 数组越界

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

别问,问就是我写过……

新手朋友会犯很多这样的低级错误。让我们来看看当数组越过这条线时会发生什么。

[En]

Novice friends make many such low-level mistakes. Let’s take a look at what happens when an array crosses the line.

#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;
}
</stdlib.h></stdio.h>

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

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

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

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

3. 字符串溢出

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

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

如果用户输入的字符串很短,这是可以接受的,但如果用户输入的值超过了接收到的字符串的长度,则可能是灾难性的。

[En]

It’s fine if the user input string is short, but it can be catastrophic if the user enters a value that exceeds the length of the received string.

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

[En]

Let’s demonstrate this phenomenon:

#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;
}
</%s></string.h></stdio.h>

在此代码中,接收数组的长度为10,因此当输入数据的长度小于10时,程序运行将没有问题。

[En]

In this code, the length of the receiving array is 10, so when the length of the input data is less than 10, the program will have no problem running.

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

var1 = 1; var2 = 2
Where do you live?

Beijing
<beijing> is length 7
var1 = 1; var2 = 2
Ok
</beijing>

威尔士小镇 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)
</llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch>

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

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

4. 内存重复释放

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

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

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

让我们举一个简单的例子:

[En]

Let’s give a simple example:

#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");
}
</stdlib.h></stdio.h>

运行此程序会导致第二次调用 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 文件里,程序运行时,就可以调用这个文件,读取配置信息。

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

[En]

Therefore, the ability to read data from files is important to all programmers. But what if the file you want to read doesn’t exist?

在 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;
}
</%c></stdio.h>

当你运行这个程序时,如果 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 呢?留言跟大家交流哦,看看谁是 Bug 王!

Original: https://www.cnblogs.com/yychuyu/p/15474622.html
Author: 良许Linux
Title: 人人都写过的5个Bug!

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/524415/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • NoteOfMySQL-07-索引

    1. 索引概述 创建索引的目的是为了优化数据库的查询速度,不添加索引的情况下需要遍历所有数据才能进行删、查、改等操作。 2. 索引存储类型 存储类型 支持的存储引擎 B型树(BTR…

    Linux 2023年6月14日
    077
  • (十)redis源码解读

    一、redis工作机制 redis是 单线程,所有命令(set,get等)都会加入到队列中,然后一个个执行。 二、为什么redis速度快? 1、基于内存 2、redis协议resp…

    Linux 2023年5月28日
    093
  • 聊一聊如何搭建高性能网站哪一些事

    在开发中经常会遇到网站的性能平静下来,打开慢的情况。我们平常开发中怎么 一步一步排查这些问题并 解决问题呢 在快节奏的时代中,慢是个不容忍受的事情。 一、 为什么会’慢…

    Linux 2023年6月14日
    0113
  • Docker搭建Redis Cluster集群及扩容和收容

    上一篇文章讲解了Redis集群原理及搭建,由于工作中使用docker较多,本文主要讲解使用docker搭建集群及对集群的扩展收容。环境:Centos7.6Docker:20.10….

    Linux 2023年6月13日
    066
  • Linux之Keepalived高可用

    一、高可用介绍 一般是指2台机器启动着完全相同的业务系统,当有一台机器down机了,另外一台服务器就能快速的接管,对于访问的用户是无感知的。 硬件通常使用:F5 软件通常使用:Ke…

    Linux 2023年6月14日
    0128
  • lab 1

    int father[2],son[2]; int son[2]; if (fork() == 0) { int n; char buf[1]; close(0); dup(fat…

    Linux 2023年6月7日
    057
  • Macbook pro 2015-mid 15寸 安装Debian时所需无线网卡驱动

    https://pan.baidu.com/s/1o1oUZhK17fpgxpwH6bBkRQ?pwd=6kpt 把该文件放到u盘的firmware/目录下即可。 给自己留个备份,…

    Linux 2023年6月6日
    0111
  • 根据两个向量计算它们之间的旋转矩阵

    一、简介 本文主要介绍通过给定的两个空间向量,计算出从一个向量旋转到另一个向量的旋转矩阵。 二、步骤 ① 假设两个向量分别为vectorBefore(x1,y1,z1), vect…

    Linux 2023年6月7日
    091
  • PIM-DM 组播路由协议仿真

    目的 理解PIM-DM 的应用场景 掌握PIM-DM 的基本配置 理解PIM-DM 中剪枝和嫁接的原理 理解PIM-DM 中的Assert 机制 原理 Source-Specifi…

    Linux 2023年6月8日
    084
  • RISC-V汇编指南

    The RISC-V Assembly Programmer’s Manual is I think it’s probably better to bee…

    Linux 2023年6月6日
    057
  • K8S部署之VMWare网络拓扑踩坑

    知乎上最近发现一篇好文 图解K8S(01):基于Ubuntu 20.04部署1.23版K8S集群,想着之前 K8S 部署一直不成功,那么就照着这篇文章中说的试一试。结果在实验时遇到…

    Linux 2023年6月13日
    078
  • 我为 Netty 贡献源码 | 且看 Netty 如何应对 TCP 连接的正常关闭,异常关闭,半关闭场景

    欢迎关注公众号:bin的技术小屋,本文图片加载不出来的话可查看公众号原文 本系列Netty源码解析文章基于 4.1.56.Final版本 写在前面….. 本文是笔者肉眼…

    Linux 2023年6月6日
    080
  • SpringBoot中通过AOP整合日志文件

    1.SpringBoot中通过AOP整合日志文件 1. 导入相关的依赖 org.springframework.boot spring-boot-starter org.sprin…

    Linux 2023年6月14日
    080
  • 搭建Nginx(haproxy)+keepalived+Tomcat双主高可用负载均衡

    周末的时候一个正在学Linux的朋友问我,高可用怎么玩?我和他微信了将近三个小时,把Nginx和haproxy双主高可用教给他了,今天突然想把这个给写进博客里,供给那些正在学习Li…

    Linux 2023年6月8日
    058
  • 北京思特奇2023年校招笔试(Java)

    北京思特奇2023年校招笔试(Java) 1、表达式 (short)10/10.2*2 运算后结果是什么类型? 答案:double,浮点数默认是double,自动类型向上转换为浮点…

    Linux 2023年6月14日
    079
  • podman(无根用户管理podman)

    用户操作在允许没有root特权的用户运行Podman之前,管理员必须安装或构建Podman并完成以下配置cgroup V2Linux内核功能允许用户限制普通用户容器可以使用的资源,…

    Linux 2023年6月7日
    073
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球