一、 什么是bug?

在计算机编程领域,bug指的是程序中存在的错误或缺陷。当程序无法按照预期的方式运行,或者产生意料之外的结果时,通常会被认为是有bug。bug可以导致程序崩溃、产生错误的输出、不正确的行为或不一致性。bug可以是由代码错误、逻辑错误、算法问题、输入错误、外部环境因素等引起的。发现和修复bug是软件开发中的重要环节,通常需要进行调试和测试来定位和解决问题。
在这里插入图片描述
第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。
注:参考链接


二、调试是什么?有多重要?

调试(英语:Debugging / Debug):又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。

🍁 调试的基本步骤

  1. 发现程序错误的存在
  2. 以隔离、消除等方式对错误进行定位
  3. 确定错误产生的原因
  4. 提出纠正错误的解决办法
  5. 对程序错误予以改正,重新测试

三、Debug和Release版本的介绍。

Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

代码:

#include <stdio.h>
int main()
{
 char *p = "hello world.";
 printf("%s\n", p);
 return 0;
}

上述代码在Debug环境的结果展示:
在这里插入图片描述
上述代码在Release环境的结果展示:
在这里插入图片描述
Debug和Release反汇编展示对比:
在这里插入图片描述
所以我们说调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程。


四、Windows环境调试介绍

  • Debug为调试版本,一般在开发完成后发布工程前,调试代码都是在Debug模式下进行的。
  • Release版本是不能调试的,一般都是在Debug版本下调试的,Release版本一般编译器会进行大量的优化,删除无用的代码,指令的次序调整等,让其速度更快。

在这里插入图片描述
在环境中选择 debug 选项,才能使代码正常调试。

🍁常用快捷键

  • F5 --> 启动调试,经常用来直接跳到下一个断点处。一般不会单独使用,而是配合F9一起使用。
  • F9 --> 创建断点和取消断点。断点的重要作用:可以在程序的任意位置设置断点。这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
  • F10 --> 逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。(遇到函数直接执行完整个函数,不进入函数内部)
  • F11 --> 逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最 常用的)
  • CTRL + F5 --> 开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

想知道更多快捷键?点我


🍁调试的时候查看程序当前信息

1. 查看临时变量的值:在调试开始之后,用于观察变量的值。
监视窗口:
在这里插入图片描述

2. 查看内存信息:在调试开始之后,用于观察内存信息。
内存窗口:
在这里插入图片描述

3. 查看调用堆栈:通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。
调用堆栈窗口:
在这里插入图片描述

4. 查看汇编信息:在调试开始之后,有两种方式转到汇编:
(1)第一种方式:右击鼠标,选择【转到反汇编】:
在这里插入图片描述

(2)第二种方式:调试窗口找到反汇编
在这里插入图片描述

5. 查看寄存器信息:可以查看当前运行环境的寄存器的使用信息。
在这里插入图片描述

多多动手,尝试调试,才能有进步。

一定要熟练掌握调试技巧, 初学者可能80%的时间在写代码,20%的时间在调试。但是一个程序员可能20%的时间在写 程序,但是80%的时间在调试。


五、调试实例

🍁 实例1

实现代码:求 1!+2!+3! …+ n! ;不考虑溢出。
在这里插入图片描述代码改正:

#include<stdio.h>
int main()
{
	int i = 0;
	int sum = 0;//保存最终结果
	int n = 0;
	scanf("%d", &n);
	for (i = 1; i <= n; i++)
	{
		int ret = 1;//保存n的阶乘
		int j = 0;
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);
	return 0;
}

🍁 实例2

在这里插入图片描述
注意:此代码非常依赖环境,在vs2022 debug版本x86环境下才是死循环的,而release版本下此代码会被优化,改变内存布局,不会死循环。
在这里插入图片描述


六、如何写出好(易于调试)的代码

优秀代码的特点:

  1. 代码运行正常
  2. bug很少
  3. 效率高
  4. 可读性高
  5. 可维护性高
  6. 注释清晰
  7. 文档齐全

常见的coding技巧:

  1. 使用assert
  2. 尽量使用const
  3. 养成良好的编码风格
  4. 添加必要的注释
  5. 避免编码的陷阱。

🍁assert的使用

assert 函数是一种在代码中用于测试和调试的工具,它用于在运行时检查一个条件是否为真。如果条件为真,则 assert 什么也不做,程序继续执行。但如果条件为假,assert 会引发一个异常(通常是 AssertionError),中断程序的执行。
示例:

模拟实现库函数:strcpy

/*
库函数strcpy
1.描述
C 库函数 char *strcpy(char *dest, const char *src) 把 src 所指向的字符串复制到 dest。
需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况。
2.声明
下面是 strcpy() 函数的声明。
char *strcpy(char *dest, const char *src)
3.参数
dest -- 指向用于存储复制内容的目标数组。
src -- 要复制的字符串。
4.返回值
该函数返回一个指向最终的目标字符串 dest 的指针。           */
char *my_strcpy(char * dst, const char * src)
{
        char * cp = dst;
        assert(dst && src);//断言,防止传进来的是空指针或野指针
        while( *cp++ = *src++ )
        { 
             ;
        }     /* Copy src over dst */
        return dst;
}

🍁空指针和野指针的危害

1.空指针(Null Pointer):

空指针是指不指向任何有效内存位置的指针,通常用空值(NULL)表示。空指针通常表示指针尚未初始化或不引用任何有效的内存。访问空指针通常会导致程序崩溃或未定义的行为。主要危害有:

  • 程序崩溃:访问空指针可能会导致程序直接崩溃,因为操作系统会捕获到这种无效的内存访问并终止程序。
  • 未定义行为:C语言标准规定对空指针的解引用是未定义行为,这意味着不同的编译器和平台可能会表现出不同的行为,包括奇怪的运行时行为和数据损坏。
  • 安全问题:攻击者可以利用空指针漏洞来执行恶意代码,从而造成系统的安全问题。

2.野指针(Dangling Pointer):
野指针是指在指针指向的内存位置被释放或无效后,仍然保持了该指针的值。在访问野指针时,可能会读取到无效的数据或者修改其他内存区域,导致未定义的行为。主要危害有:

  • 数据损坏: 野指针可能会导致数据损坏,因为程序可能会误用已经释放或者不再有效的内存位置。
  • 难以调试: 由于野指针可能导致未定义行为,程序可能会表现出奇怪的错误,这会使调试变得非常困难。

🍁const的作用

const修饰指针变量的时候:

1. const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。

#include <stdio.h>

int main() {
    int num = 5;
    const int* ptr = &num;  // const在*的左边,指针本身可以变,但是指向的内容不可变
    // *ptr = 10;  // 这里会产生编译错误,因为不能通过ptr修改num的值
    num = 10;  // 可以通过num直接修改值
    printf("num: %d\n", num);  // 输出:num: 10

    int another_num = 20;
    ptr = &another_num;  // 可以将ptr指向另一个整数

    return 0;
}

在上述示例中,ptr 是一个指向 const int 的指针,这意味着不能通过 ptr 来修改它指向的内容,但可以通过修改 num 的值来间接地修改指针所指向的内容。另外,可以改变 ptr 指向其他整数。

2. const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

#include <stdio.h>

int main() {
    int num = 5;
    int* const ptr = &num;  // const在*的右边,指针本身不可变,但是指向的内容可以改变
    *ptr = 10;  // 可以通过ptr修改num的值
    printf("num: %d\n", num);  // 输出:num: 10

    // 以下操作是不允许的,因为ptr已经被声明为const,不能指向其他内存
    //int another_num = 20;
    // ptr = &another_num;  // 编译错误

    return 0;
}

在上述示例中,ptr 是一个指向 num 的常量指针,这意味着不能通过 ptr 来改变它指向的位置,但可以通过 *ptr 来修改它指向的内容。


练习:

模拟实现一个strlen函数

#include <stdio.h>
int my_strlen(const char *str)
{
    int count = 0;
    assert(str != NULL);//断言,也可以写成 assert(str);
    while(*str)//判断字符串是否结束
   {
        count++;
        str++;
   }
    return count;
}
int main()
{
    const char* p = "abcdef";
    //测试
    int len = my_strlen(p);
    printf("len = %d\n", len);
    return 0;
}

七、编程常见的错误

  1. 编译型错误
  • 产生原因:编译型错误是在编译阶段发生的错误,通常是由于语法错误、类型错误、未定义的标识符等造成的。编译器无法正确解析代码,因此无法生成可执行文件。
  • 解决方法:仔细检查代码,确保语法正确、类型匹配,并确保使用的标识符在正确的作用域中定义。查看编译器的错误信息和警告,逐一修复问题。相对来说简单。
  1. 链接型错误
  • 产生原因:链接型错误发生在链接阶段,当编译器尝试将多个源文件组合成一个可执行文件时。常见的链接错误包括重复定义、未定义的符号等。
  • 解决方法:确保不同源文件中的函数和变量只有一次定义,避免重复定义。如果遇到未定义的符号错误,检查是否缺少某个库文件的链接,或者确保函数定义在正确的源文件中。一般是标识符名不存在或者拼写错误。
  1. 运行时错误
  • 产生原因:运行时错误发生在程序执行阶段,可能由于无效的内存访问、除以零、类型不匹配等引起。这些错误可能导致程序崩溃、产生未定义行为或者不正确的结果。
  • 解决方法:使用合理的错误处理机制来捕获和处理运行时错误。例如,对于可能导致除以零的情况,可以在执行之前进行条件检查。使用异常处理机制(例如C++ 中的 try-catch)来捕获异常情况并进行适当的处理。确保指针的正确初始化和检查,以避免空指针或野指针问题。要习惯借助调试,逐步定位问题。最难搞。

🔥今天的分享就到这里,如果觉得博主的文章还不错的话,请👍三连支持一下博主哦🤞
在这里插入图片描述

Logo

纵情码海钱塘涌,杭州开发者创新动! 属于杭州的开发者社区!致力于为杭州地区的开发者提供学习、合作和成长的机会;同时也为企业交流招聘提供舞台!

更多推荐