🍁博客主页:江池俊的博客
💡代码仓库:江池俊的代码仓库
🎪我的社区:GeekHub
🍁 如果觉得博主的文章还不错的话,请点赞👍收藏🌟 三连支持一下博主💞
一、 什么是bug?
在计算机编程领域,bug指的是程序中存在的错误或缺陷。当程序无法按照预期的方式运行,或者产生意料之外的结果时,通常会被认为是有bug。bug可以导致程序崩溃、产生错误的输出、不正确的行为或不一致性。bug可以是由代码错误、逻辑错误、算法问题、输入错误、外部环境因素等引起的。发现和修复bug是软件开发中的重要环节,通常需要进行调试和测试来定位和解决问题。
第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。
注:参考链接
二、调试是什么?有多重要?
调试(英语:Debugging / Debug):又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
🍁 调试的基本步骤
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
三、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版本下此代码会被优化,改变内存布局,不会死循环。
六、如何写出好(易于调试)的代码
优秀代码的特点:
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
常见的coding技巧:
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱。
🍁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 = # // 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 = # // 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; }
七、编程常见的错误
- 编译型错误
- 产生原因:编译型错误是在编译阶段发生的错误,通常是由于语法错误、类型错误、未定义的标识符等造成的。编译器无法正确解析代码,因此无法生成可执行文件。
- 解决方法:仔细检查代码,确保语法正确、类型匹配,并确保使用的标识符在正确的作用域中定义。查看编译器的错误信息和警告,逐一修复问题。相对来说简单。
- 链接型错误
- 产生原因:链接型错误发生在链接阶段,当编译器尝试将多个源文件组合成一个可执行文件时。常见的链接错误包括重复定义、未定义的符号等。
解决方法:确保不同源文件中的函数和变量只有一次定义,避免重复定义。如果遇到未定义的符号错误,检查是否缺少某个库文件的链接,或者确保函数定义在正确的源文件中。一般是标识符名不存在或者拼写错误。
- 运行时错误
- 产生原因:运行时错误发生在程序执行阶段,可能由于无效的内存访问、除以零、类型不匹配等引起。这些错误可能导致程序崩溃、产生未定义行为或者不正确的结果。
解决方法:使用合理的错误处理机制来捕获和处理运行时错误。例如,对于可能导致除以零的情况,可以在执行之前进行条件检查。使用异常处理机制(例如C++ 中的 try-catch)来捕获异常情况并进行适当的处理。确保指针的正确初始化和检查,以避免空指针或野指针问题。要习惯借助调试,逐步定位问题。最难搞。
🔥今天的分享就到这里,如果觉得博主的文章还不错的话,请👍三连支持一下博主哦🤞