目录
3.1.1.切换断点、新建断点快捷键 F9 / 开始调试快捷键F5
5.2.示例(※)
1. 什么是bug
第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误。
2. 调试是什么,有多重要
所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。
一名优秀的程序员是一名出色的侦探。
每一次调试都是尝试破案的过程
2.1. 调试是什么
调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
2.2.调试的基本步骤
·发现程序错误的存在·以隔离、消除等方式对错误进行定位·确定错误产生的原因·提出纠正错误的解决办法·对程序错误予以改正,重新测试
第一步:发现程序错误的存在
1.程序员->可以发现错误
2.测试(开发)工程师->也可以发现程序的错误 报bug
----------------程序发布----------------
3.用户->也可以发现(可能就有风险了!)
第二步:以隔离、消除等方式对错误进行定位
第三步:确定错误产生的原因
第四步:提出纠正错误的解决办法
第五步:对程序错误予以改正,重新测试
2.3. Debug和Release的介绍
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。(bebug版本可以调试)Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。(Release版本是无法调试的)
注:
1.当程序选择debug或release时ctrl+F5进行编译,这时文件夹里就会有debug或release文件夹,文件夹里放的就是debug调试版本或release发布版本。
编译前文件夹:
编译:
编译后文件夹:
2.可以看出debug调式版本里的.exe运行文件要比release发布版本里的.exe运行文件大很多(因为debug调式版本里包含调试信息)
3. Windows环境调试介绍
3.1.学会快捷键
3.1.1.切换断点、新建断点快捷键 F9 / 开始调试快捷键F5
F9 切换断点、新建断点
F5 开始调试(通常使用F5跳到想要的断点处)
断点:程序执行到断点处就会主动停下来
1.F9和F5是配合使用的:当我们想从某一行代码开始调试时(前面的代码自动运行完),在该行代码设置断点,然后F5进行调试,然后再按F10往下调试即可
2.F5是向后执行代码,到下一个逻辑上的断点,如下图,断点设在循环内,每按一次F5代码 ,循环都会执行一次并且执行到这个断点处停止,因此我们可以直观的看到按一次F5,右边监视窗口i的值增加1
3.1.2.逐过程 F10 / 逐语句 F11
F10 逐过程(每按一次,执行一条语句)(粗劣进行每一行执行)
F11 逐语句 (每按一次,执行一条语句,遇见函数时,可进入函数内部执行)(若可细分,进行精细执行)
3.1.3.开始执行(不调试) ctrl+F5
ctrl+F5 开始执行(不调试)
3.1.4.vs编译器中更多的快捷键
(40条消息) VS中常用的快捷键_MrLisky的博客-CSDN博客_vs快捷键
3.2.调试的时候查看程序当前信息
3.2.1.自动窗口
vs编译器自动获取程序上下文环境中的某些变量,进行展示
自动展示某些变量自动隐藏展示的变量,不方便,一般不进行使用
3.2.2.局部变量
会把程序执行过程中上下文环境中的局部变量进行列举展示
3.2.3.监视
可以把想要观察的数据自己加进去
3.2.4.内存
输入地址可以观察到内存地址中存的数据值
3.2.5.反汇编
可以观察到程序在执行过程中,其汇编代码的变化
(也可以在代码界面点击鼠标右键,点击反汇编)
3.2.6.寄存器
可以观察到各类寄存器的变化状态(在监视窗口输入想要观察的寄存器名字也可以观察到)
3.2.7.调用堆栈
可以观察到函数调用的逻辑
4.调试的实例
实例1:
实现代码:求 1 ! +2 ! +3 ! ...+ n! ;不考虑溢出。调试下面代码,发现代码中的bug
#include<stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int ret = 1;
int j = 0;
int sum = 0;
for (j=1; j<=n; j++)
{
for (i = 1; i <= j; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
调试的过程:
1. 我们观察到当j=3时算3!应该是6,而代码算出来的是ret=12,此时代码已经走过去了,我们想退回去从j=3重新开始进行观察。
2.我们从j=3重新开始进行观察,我们在i的for循环处设置断点,鼠标放在断点红圈处单机鼠标右键,选择条件,条件改成j==3,关闭后,断点变成了条件断点,此时按F5调试可以观察到是从j=3开始的。
3.此时我们可以看到当j=3算3的阶乘时,ret的初始值并不是1,而是2。此时我们便发现了问题
4.修改代码,写出正确代码如下
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int ret = 1;
int j = 0;
int sum = 0;
for (j=1; j<=n; j++)
{
ret = 1;
for (i = 1; i <= j; i++)
{
ret *= i;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
实例2:
研究下面程序死循环的原因
int main()
{
int i = 0;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
调试的过程:
1.我们看到直到i=11,即使越界访问,代码都会对数据进行更改变成0
2.而当i=12,对arr[12]进行更改时,arr[12]可以变成0,而当arr[12]变成0时,i的值也跟着变成0(细心的话我们可以看到前面arr[12]里面的值总是会与i同时变化),此时我们要对其进行分析。
3.我们对其进行分析,我们发现arr[12]里面的值总是和i同时变化,那么arr[12]里面的值是否就是i的值呢?也就是arr[12]的地址是否与i的地址相同呢,我们可以进行观察,下图中我们看到arr[12]的地址与i的地址完全相同,因此我们可以得出当i=12时,arr[i]=0其实就是又将i赋值成0,循环结束条件永远无法实现,因此死循环。
注:
1.i和arr是局部变量,局部变量是放在栈区上的,栈区上内存的使用习惯是:先使用高地址处的空间,再使用低地址处的空间
2.数组随着下标的增长,地址是由低到高变化的
3.如果i和arr之间的空间适合的话,就有可能使用的arr数组向后越界就访问到了i,造成循环变量i的改变,最终死循环
4.编译器不一样,数组后面空余的空间数也不一样,所以该代码是严格依赖环境的
vs编译器 空2个空间
vc6.0 不空空间
gcc 空1个空间
5.我们发现当我们改成release发布版本时再进行编译,该代码便不会死循环了,如下图所示,这是因为在release版本中将代码中的i=0放在了代码arr[10]={1,2,3,4,5,6,7,8,9,10}的后面(也就是将i的地址放在了arr地址的前面)
5.如何写出好(易于调试)的代码
5.1.优秀的代码
1. 代码运行正常2. bug很少3. 效率高4. 可读性高5. 可维护性高6. 注释清晰7. 文档齐全
1. 使用assert2. 尽量使用const3. 养成良好的编码风格4. 添加必要的注释5. 避免编码的陷阱。
5.2.示例(※)
1.模拟实现库函数:strcpy
strcpy函数介绍:
由下代码知,strcpy会将字符串中的'\0'也拷贝过去。
代码1:(该代码有bug)
代码2:
代码3:
代码4:(模拟strcpy函数返回值功能)
代码5:(完全模拟strcpy函数功能,加了const)
注:
1.代码1不好,因为如果传的指针是空指针的话,对空指针解引用程序就会有问题
2.assert是断言的意思,assert函数若括号里的内容为假,就会报错,如下图
3.assert函数需要头文件assert.h
4. 代码3,'\0'的ASCII码值为0因此可以用'\0'跳出循环
5.代码4,strcpy函数其实是有返回值的,返回的是目标字符串的地址
6.strcpy函数传参时形参的源字符串前有const修饰,以防下面while循环处将源字符串地址赋值给目标字符串地址时两地址写反(加了const就使得此处写反,系统无法运行),此处const是希望限制*p不能被修改,所以应该放在*的前面。
7.const修饰变量,该变量变成常变量,常变量无法被修改(但用指针变量对其进行修改时,该变量会被改变)
const修饰指针变量,当const放在*的左边(const int * p=&n 或 int const * p),修饰的是指针指向的内容,表示指针指向的内容不能通过指针来改变(不能对*p也就是p的解引用进行改变,可以对指针变量p进行改变,也就是p可以取其他地址)。当const放在*的右边(int * const p=&n),修饰的是指针变量本身,表示指针变量的内容不能被改变(可以对*p也就是p的解引用进行改变,不能对指针变量p进行改变,也就是p不能取其他地址)。当*的左右两边都有const,那么既限制了*p又限制了p。
拓展思考:const修饰二级级指针(有三种情况)
2.模拟实现库函数:strlen
代码:
6.编程常见的错误
6.1.编译型错误
编译错误:都是语法问题引起的直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单
6.2.链接型错误
一般是标识符名不存在或者拼写错误看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。双击错误提示信息没有反应,需要自己去程序中找错误的标识符搜索框快捷键:ctrl+F跳转到哪一行快捷键:ctrl+G
6.3.运行时错误
借助调试,逐步定位问题。最难搞