人人都写过的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)

大家都在看

  • IDEA快捷键总结

    一、关于IDEA工具的快捷键 1.1、字体设置  file –> settings –> 输入font –> 设置字体样式以及字号大小​1.1、快速生成ma…

    Linux 2023年6月7日
    084
  • CentOS 文本编辑器

    Linux 终端的文本编辑器中,较著名的有:Nano、Vim、Emacs。其它文本编辑器还有 Gedit、Sublime,Atom 等等。 1.1、基础命令 nano:打开 nan…

    Linux 2023年6月8日
    0110
  • 深入分析JVM执行引擎

    程序和机器沟通的桥梁 一、闲聊 相信很多朋友在出国旅游,或者与外国友人沟通的过程中,都会遇到语言不通的烦恼。这时候我们就需要掌握对应的外语或者拥有一部翻译机。而笔者只会中文,所以需…

    Linux 2023年6月14日
    098
  • python爬虫_入门_翻页

    写出来的爬虫,肯定不能只在一个页面爬,只要要爬几个页面,甚至一个网站,这时候就需要用到翻页了 其实翻页很简单,还是这个页面http://bbs.fengniao.com/forum…

    Linux 2023年6月6日
    074
  • Python递归遍历目录下所有文件

    递归遍历目录下所有文件 方法一 import os def gci(filepath): #遍历filepath下所有文件,包括子目录 files = os.listdir(fil…

    Linux 2023年6月13日
    0111
  • 兼容各种浏览器的上下滚动代码

    直接切入正题 红色的表示为要注意统一的。 蓝色是表示要更改的。 内容高度一定要大于box1的高度否则不会滚动,本框架用的是phpcms,大家可根据自己的框架更改循环。 | {pc:…

    Linux 2023年6月13日
    086
  • LAXCUS授权开源协议

    LAXCUS 授权许可证 第1版 本许可证仅针对LAXCUS分布式操作系统和衍生子版本,围绕LAXCUS分布式操作系统设计开发的硬件、硬件驱动程序、应用软件不受本许可证约束。 您对…

    Linux 2023年6月6日
    0118
  • 【6】2022年8月

    8月21日 OMG!!我真的是懒骨头!不到最后一刻丝毫不紧张!! 兄弟,八月底了阿!! 你为了明年的计划,要想同一时间内赚5万和成功上岸,这太不可思议了! 你压力好大的,别到最后放…

    Linux 2023年6月13日
    087
  • 面试连环炮系列(二十️五):RocketMQ怎么保证消息不丢失

    A. 从Producer的视角来看:如果消息未能正确的存储在MQ中,或者消费者未能正确的消费到这条消息,都是消息丢失。 B. 从Broker的视角来看:如果消息已经存在Broker…

    Linux 2023年6月6日
    0143
  • windows下安装virtualenv并且配置指定环境

    下面是在windows下通过virtualenv创建虚拟环境, 包括 : 安装virtualenv(使用pip可直接安装) 使用virtualenv创建指定版本的虚拟环境 进入虚拟…

    Linux 2023年6月6日
    0100
  • 启动springboot项目很慢的解决方案-InetAddress.getLocalHost().getHostName()

    https://blog.csdn.net/qq_39595769/article/details/119573111 Original: https://www.cnblogs….

    Linux 2023年6月13日
    091
  • 【Python】AttributeError: ‘Rotation’ object has no attribute ‘from_dcm’

    报错的代码如下: from scipy.spatial.transform import Rotation def dcm2euler(mats: np.ndarray, seq:…

    Linux 2023年6月13日
    067
  • linux自动备份mysql数据库

    备份脚本记录一下–(单个数据库) 2021-11-15 1.新建shell脚本:vim **.sh #!/bin/bashCKUP=/data/backup/db #获…

    Linux 2023年5月27日
    0114
  • 聊聊支付流程的设计与实现逻辑

    新手打怵老手头疼的业务; 一、业务背景 通常在业务体系中,都会或多或少的涉及到支付相关的功能;对于一些经验欠缺同学来说,最紧张的就是面对这类支付结算的逻辑,因为流程中的任何细节问题…

    Linux 2023年6月14日
    083
  • MySQL(一)——查看密码与修改

    查看数据库密码,策略与修改 RPM安装: 源码安装: 进入:数据库 进入数据库后第一步设置密码: 查看密码策略 修改密码策略,长度 0宽容模式 混合模式,0关闭大小写 特殊字符 O…

    Linux 2023年6月13日
    081
  • Chrome的强大搜索功能

    前言 前几天一个好朋友求助我,大概问题是他的电脑QQ啥都能上网,就浏览器上不了网不是IE而是chrome,我第一反应可能是dns问题。后来发甩过来一张图,好家伙把我吓得,类似于下面…

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