8.调试
8.1 什么是bug?
程序错误,即英文的Bug,也称为缺陷、臭虫,是指在软件运行中因为程序本身有错误而造成的功能不正常、死机、数据丢失、非正常中断等现象。
史上的第一只 "Bug" ,真的是因为一只飞蛾意外走入一电脑而引致故障,因此Bug从原意为臭虫引申为程序错误。
8.2 调试是什么?有多重要?
8.2.1 调试概念
调试(英语:Debugging/Debug):又称排错,将编制的程序投入实际运行前,用手工或编译程序等方法进行测试,修正语法错误和逻辑错误的过程。是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
8.2.2 基本步骤
- 发现错误程序的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
8.3 debug和release的介绍
8.3.1 Debug
调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序。并且程序中只有包含了额外的辅助信息才可以进行调试。
8.3.2 Release
发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好的使用。
8.3.3 总结
Release版本相较于Debug版本的可执行文件,空间更小,运行更快。
8.4 windows环境调试介绍
Linux系统中使用gdb(GNU symbolic debugger)
8.4.1 快捷键
快捷键 | 操作 |
---|---|
Ctrl + Alt + P | 附加到进程 |
F10 | 调试单步执行 |
F5 | 开始调试 |
Shift + F5 | 停止调试 |
Ctrl + Alt + Q | 添加快捷匹配 |
F9 | 设置或删除断点 |
VisualStudio2019快捷键汇总_Dahlin哥's 博客-CSDN博客_vs快捷键
8.4.2 窗口
重点:某些窗口只有当调试起来(快捷键 F10
和 F11
)之后才会显示。
8.4.3 断点
设置断点,如果开始执行(不调试),程序直接执行结束,并不会在断点处暂停。所以使用断点必须调试执行才能生效。断点之前的程序正常执行,到断点处暂停。使用断点窗口,可以方便的查看多个源文件中的断点位置。
添加条件
- 在循环中设置断点的条件,可以循环到一定的次数之后进行暂停。
其他
- 使用
F5
移动到下一个断点。
8.4.4 监视
监视各个变量值的变化。可以自己添加需要监视的标识符和不需要监视的标识符。
此外还有自动监视和局部变量窗口。自动监视窗口可以自动检测需要监视的变量。局部变量窗口显示局部变量的变化。
8.4.5 调用堆栈
函数的调用逻辑。
8.4.6 其他
内存窗口,可以设置每行显示的列数。
8.5 调试实例
8.5.1 阶乘之和
找出代码中的bug
int main()
{
int n = 3;
int sum = 0;
int mul = 1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= i; j++)
{
mul *= j;
}
sum += mul;
}
printf("%d\n", sum);
return 0;
}
// bug原因,mul变量在每次循环之后并没有重置为1
8.5.2 数组越界死循环
寻找原因
int main()
{
int i = 0;
int arr[10] = { 0 };
for ( i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("Hello\n");
}
return 0;
}
// vs2013~2019出现死循环
// vs2022中正常循环12次,并没有出现数组访问越界错误
// 下图为vs2022运行结果图
8.6 如何写出好(易于调试)的代码
8.6.1 优秀的代码
- 代码正常运行
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
8.6.2 常见的coding技巧
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱
8.6.3 实例
模拟实现库函数strcpy
介绍
- 声明
char *strcpy(char *dest, const char *src)
-
参数
- dest: 用于存储复制内容的目标数组
- src: 要复制的字符串
-
返回值:返回指向目标字符串的指针
代码 -
库函数
#include <string.h> int main() { char arr1[20] = { 0 }; char arr2[] = "Hello World!"; strcpy(arr1, arr2); printf(arr1); return 0; } // 需要在源文件最开始加入#define _CRT_SECURE_NO_WARNINGS
-
自实现
void my_strcpy(char* dest, char* sour) { while (*sour != '\0') { *dest = *sour; dest++; sour++; } *dest = *sour; } int main() { char arr1[20] = { '\0'}; char arr2[] = "Hello"; my_strcpy(arr1, arr2); printf(arr1); return 0; }
-
优化1
void my_strcpy(char* dest, char* sour) { while (*sour != '\0') { *dest++ = *sour++; } *dest = *sour; }
-
优化2
void my_strcpy(char* dest, char* sour) { while (*sour) { *dest++ = *sour++; } *dest = *sour; }
-
优化3
void my_strcpy(char* dest, char* sour) { while (*dest++ = *sour++); // 先将*sour赋值给*dest // 获得整个表达式的结果 // 并判断整个表达式的结果是否为假 // 如果为真 再对dest和sour进行自增 }
-
优化4
void my_strcpy(char* dest, char* sour) { assert(src != NULL); // 断言 assert(dest != NULL); // 断言 while (*dest++ = *sour++); } // assert的使用需要引用头文件<assert.h> // assert中的表达式类似于if中的表达式
-
优化5
void my_strcpy(char* dest, const char* sour) { assert(src != NULL); assert(dest != NULL); while (*dest++ = *sour++); } // const char* sour // sour指向的对象的内容不能被修改 // 防止将dest中的内容复制到sour中 // 即使发生了问题,发生的也只是语法错误,也容易找出并解决问题 // 另外const char* sour中的const只需要在*前面,并不一定要在char前面 // 即:char const * sour 但一般char*是一种类型,不拆开
-
常量指针和指针常量
int main() { int m = 100; int n = 10; //const int* p = &n; // 指针变量指向的对象不能改变 //*p = 100; // × //p = &m; // √ //int* const p = &n; // 指针变量的值不能改变 //p = &m; // × //*p = m; // √ //const int* const p = &n; // 指针变量和指针变量的值都不能改变 //p = &m; // × //*p = m; // × //const int* p; // 称为常量指针 //int* const p; // 称为指针常量 return 0; }
-
-
优化6
char* my_strcpy(char* dest, const char* sour) { assert(src != NULL); assert(dest != NULL); char* ret = dest; while (*dest++ = *sour++); return ret; // 返回目标空间的其实地址(为了更好的看到目标地址发生的变化) } // 链式访问功能
模拟实现库函数strlen
-
自实现
size_t my_strlen(const char *s) { assert(s); size_t len = 0; while (*s++)len++; return len; } // size_t 相当于 unsigned int int main() { char s[] = "hello"; int len = my_strlen(s); printf("%d\n", len); return 0; }
-
参考代码
size_t __cdecl strlen ( const char * str ) { const char *eos = str; while( *eos++ ) ; return( eos - str - 1 ); } // 参考代码并不代表真实实现 // __cdecl表示函数调用约定,不影响函数使用
8.7 编程常见的错误
错误类型
-
编译型错误
直接看错误提示信息(双击定位出错位置),解决问题。或者凭借经验就可以搞定。相对来说简单。
-
链接型错误
看错误提示信息,主要在代码中找到错误信息的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
-
运行时错误
借助调试信息,逐步定位问题。难度最大。