实用调试的技巧,VS编译器常用调试详解
Posted 小赵小赵福星高照~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实用调试的技巧,VS编译器常用调试详解相关的知识,希望对你有一定的参考价值。
实用调试技巧
- 什么是bug?
- 调试是什么?重要吗?
- debug和release的介绍
- windows环境调试介绍
- 一些调试实例
- 如何写出好的代码
- const
什么是bug?
为马克2号(Harvard Mark II)编制程序的葛丽丝·霍波(Grace Hopper)是一位美国海军准将及计算机科学家,同时也是世界最早的一批程序设计师之一,有一天,她在调试设备时出现故障,拆开继电器后,发现有只飞蛾被夹扁在触点中间,从而“卡”住了机器的运行。于是,霍波诙谐的把程序故障统称为BUG(飞虫),把排除程序故障叫DEBUG,而这奇怪的“称呼”,竟成为后来计算机领域的专业行话。这是历史上第一次程序的bug。
调试是什么?有多重要?
你必须掌握的一种重要的技能就是调试。虽然调试经常会有挫败感,但是它仍旧是编程工作中最有趣、有挑战性和有价值的部分。调试就像侦探工作。你掌握的是各种线索,你必须据此猜测导致你所见的结果的过程与事件。调试又像实验科学。一旦你觉得可以猜测到出问题的地方,你会修改你的程序,再次运行。如果你的想法是正确的,那么你可以预测这次修改的结果,这样就向正确的程序迈进了一步;如果是错的,你必须再次提出新的想法。就像夏洛克福尔摩斯说的那样:“当你排除了所有不可能,无论剩下的是什么,不管看起来多么不可能,那都是真相。
一名优秀的程序员是一名出色的侦探
拒绝迷信式调试!!!
试来试去结果运行成功了,但是不知道为什么。
调式的基本步骤
-
发现程序错误的存在
1.程序员自己发现并解决
2.软件测试人员 - 测试软件
3.用户发现问题
-
以隔离、消除等方式对错误进行定位
-
确定错误产生的原因
-
提出纠正错误的解决办法
-
对程序错误予以改正,重新测试
Debug和Release的介绍
Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release称为发布版本,它往往进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
那他们两有什么区别呢?看个例子
int main()
{
int arr[10]={0};
int i=0;
for(i=0;i<10;i++)
{
arr[i]=i;
}
return 0;
}
Debug版本我们可以通过调试一步一步的来观察监视程序
而当我们换成Release版本时:
这时就不能进行调试了
我们在分别运行Debug版本和Realease版本时能在文件路径找到Debug文件和Realease文件
我们分别进去Debug文件和Release文件里面有个可执行程序文件(.exe)
仔细对比发现,两个的大小不一样,是因为Debug版本我们含有调试信息,不作任何优化;而Release不含有调试信息,还会进行优化,大小更小一些。
windows环境调试介绍
1.调试环境的准备
确保环境切换为Debug
2.学会快捷键
常用快捷键
- F5
启动调试,经常用来直接调到下一个断点处。
- F9
创建断点和取消断点 断点的重要作用,可以在程序的任意位置设置断点。这样就可以使得程序在
想要的位置随意停止执行,继而一步步执行下去。
- F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
- F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是
最长用的)。
3.调试的时候查看程序当前信息
查看临时变量的值
查看内存信息
查看汇编信息
查看调用堆栈
通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。前面的是被后面的调用的。
查看寄存器
一些调试的实例
实例一:
求1!+2!+3!+4!+5!
int main()
{
int i=0;
int j=0;
int ret=1;
int n=0;
int sum=0;
scanf("%d",n);
for(i=1;i<=n;i++)
{
for(j=1;j<i;j++)
{
ret*=j;
}
sum+=ret;
}
return 0;
}
1!+2!+3!这个代码有问题,如果我们实在想不明白,我们进行调试
通过一步一步认真调试监视发现,ret没有重置为1,所以结果会有错误
int main()
{
int i=0;
int j=0;
int ret=1;
int n=0;
int sum=0;
scanf("%d",n);
for(i=1;i<=n;i++)
{
for(j=1;j<i;j++)
{
ret*=j;
}
sum+=ret;
ret=1;
}
return 0;
}
实例二:
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("haha\\n");
}
return 0;
}
运行结果是啥?为什么?
运行后会进入死循环打印haha,数组明明越界了,为什么不报错呢?是因为它陷进了死循环,停不下来,所以不会报错。
我们进行调试,调试后发现arr[12]恰好就是i,改变arr[12]时也改变i,所以进行了死循环。
原因如图解释所示:
i和arr是局部变量,局部变量是放在栈区上的
栈区的使用习惯是:先使用高地址空间,再使用低地址。
我们在将程序换成release版本运行
程序会停下来,那是因为release版本是对代码进行优化过的,代码大小和运行速度都是最优的,这里在release版本下,将局部变量i放在了低地址处,看下图
debug版本
i在高地址
release版本
i到了低地址
所以在release版本底下,代码进行了优化,不会死循环
如何写出好的代码
优秀的代码:
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
常见的coding技巧:
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱
模拟实现库函数:strcpy - 字符串拷贝
void my_strcpy(char arr2[], char arr1[])
{
while (1)
{
*arr2 = *arr1;
if (*arr2 =='\\0')
{
break;
}
arr1++;
arr2++;
}
}
int main()
{
char arr1[] = "hello world";
char arr2[20] = { 0 };
my_strcpy(arr2,arr1);
printf("%s", arr2);
return 0;
}
void my_strcpy(char arr2[], char arr1[])
{
while (*arr1 != '\\0')
{
*arr2 = *arr1;//hello的拷贝
arr1++;
arr2++;
}
*arr2 = *arr1;//拷贝字符'\\0'
}
int main()
{
char arr1[] = "hello";
char arr2[20] = { 0 };
my_strcpy(arr2,arr1);
printf("%s", arr2);
return 0;
}
进行优化
void my_strcpy(char arr2[], char arr1[])
{
while (*arr1 != '\\0')
{
*arr2++ = *arr1++;//hello的拷贝
}
*arr2 = *arr1;//拷贝字符'\\0'
}
int main()
{
char arr1[] = "hello";
char arr2[20] = { 0 };
my_strcpy(arr2,arr1);
printf("%s", arr2);
return 0;
}
可以继续可以优化,下面代码很妙!
void my_strcpy(char arr2[], char arr1[])
{
while (*arr2++ = *arr1++)//'0'->0,既拷贝了\\0,又使得循环停止
{
;
}
}
int main()
{
char arr1[] = "hello";
char arr2[20] = { 0 };
my_strcpy(arr2,arr1);
printf("%s", arr2);
return 0;
}
循环判断条件是*arr2++ = *arr1++,首先我们要知道,a=b这个表达式的值是a的值,我们将arr1中的每个字符赋值给arr2中时,其实整个表达式的值在每次循环时的值分别为’h’e’l’l’o’\\0’字符的ASSIC码值,当我们把\\0赋值到arr2中时,同时整个表达式的值也为0了,所以退出循环。
但是我们发现如果传过去的是空指针,那解引用不是有问题嘛,所以我们继续优化
在这之后我们还可以优化
因为在使用拷贝函数的人传进来个空指针,那我们是不能对一个空指针解引用的,所以我们使用断言
断言
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。
使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为FALSE时中断当前操作的话,可以使用断言。
//断言
#include<assert.h>
void my_strcpy(char arr2[], char arr1[])
{
//if (arr2 == NULL && arr1 == NULL)
//{
// return;
//}//不好
assert(arr2 != NULL);//为假会报错
assert(arr1 != NULL);
while (*arr2++ = *arr1++)
{
;
}
}
int main()
{
char arr1[] = "hello world";
char arr2[20] = { 0 };
my_strcpy(arr2, arr1);
printf("%s", arr2);
return 0;
}
当我们传入空指针时,断言会提示你哪里错了,是不是很美妙,所以学习良好的编程习惯是对别人的友好,更是对自己的友好。
最终最优满分代码
我们会发现库函数strcpy,拷贝源头加了const,函数返回类型为char*。
为什么这样做呢,就是因为我们有时候可能会把拷贝源头和拷贝目的地写反,就会报错,因为两个字符串大小不一样的话,就有问题了
加上const时,当我们写反时,会有以下报错
strcpy库函数返回的是目标空间的起始空间
#include<assert.h>
char* my_strcpy(char arr2[], const char arr1[])
{
char* ret=arr2;
//if (arr2 == NULL && arr1 == NULL)
//{
// return;
//}//不好
assert(arr2 != NULL);//为真会报错
assert(arr1 != NULL);
while (*arr2++ = *arr1++)
{
;
}
return arr2;
}
int main()
{
char arr1[] = "hello world";
char arr2[20] = { 0 };
//my_strcpy(arr2, arr1);
printf("%s", my_strcpy(arr2, arr1););//函数返回的是目标的起始地址, 这是链式访问
return 0;
}
给arr1加上const的原因是,arr1是拷贝源头,是不能改变的
const
const 修饰变量,这个变量就会被称为常变量,不能被修改,但本质上还是变量
int main()
{
const int num=10;
int *p=#//限制*p
*p=20;
printf("%d\\n",num);
return 0;
}
我们不想改变num,但是num把地址给了p,p能够把num给改变了,此时如果我们不想以任何方式改变,就要给指针变量加const修饰
const修饰指针
int main()
{
const int num=10;
int n=20;
const int *p=#//限制*p
//*p=20;不ok
p=&n;//ok
printf("%d\\n",num);
return 0;
}
int main()
{
const int num=10;
int n=20;
int *const p=#//限制*p
*p=20;//ok
//p=&n;//不ok
printf("%d\\n",num);
return 0;
}
int main()
{
const int num=10;
int n=20;
int const *const p=#//限制*p
//const 修饰指针的时候
//const放在*的左边,表示指针指向的内容(*p)不能通过指针(p)来改变,但是指针本身(p)是可以修改的
//const放在*的右边,表示指针指向的内容(*p)能通过指针(p)来改变,但是指针本身(p)是不可以修改的
*p=20;//ok
//p=&n;//不ok
printf("%d\\n",num);
return 0;
}
总结
const放在*的左边,表示指针指向的内容(*p)不能通过指针(p)来改变,但是指针本身(p)是可以修改的
const放在*的右边,表示指针指向的内容(*p)能通过指针(p)来改变,但是指针本身(p)是不可以修改的*
模拟实现strlen 求字符串长度
#include<stdio.h>
#include<assert.h>
int my_strlen(const char *str)
{
assert(str!=NULL);
if(*str!=\\0)
{
return 1+my_strlen(str+1);
}
return 0;
}
int main()
{
char arr[]="hello";
my_strlen(arr);
return 0;
}
欢迎大家交流!
以上是关于实用调试的技巧,VS编译器常用调试详解的主要内容,如果未能解决你的问题,请参考以下文章