实用调试的技巧,VS编译器常用调试详解

Posted 小赵小赵福星高照~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实用调试的技巧,VS编译器常用调试详解相关的知识,希望对你有一定的参考价值。

实用调试技巧

  • 什么是bug?
  • 调试是什么?重要吗?
  • debug和release的介绍
  • windows环境调试介绍
  • 一些调试实例
  • 如何写出好的代码
  • const

什么是bug?

为马克2号(Harvard Mark II)编制程序的葛丽丝·霍波(Grace Hopper)是一位美国海军准将及计算机科学家,同时也是世界最早的一批程序设计师之一,有一天,她在调试设备时出现故障,拆开继电器后,发现有只飞蛾被夹扁在触点中间,从而“卡”住了机器的运行。于是,霍波诙谐的把程序故障统称为BUG(飞虫),把排除程序故障叫DEBUG,而这奇怪的“称呼”,竟成为后来计算机领域的专业行话。这是历史上第一次程序的bug。

img

调试是什么?有多重要?

你必须掌握的一种重要的技能就是调试。虽然调试经常会有挫败感,但是它仍旧是编程工作中最有趣、有挑战性和有价值的部分。调试就像侦探工作。你掌握的是各种线索,你必须据此猜测导致你所见的结果的过程与事件。调试又像实验科学。一旦你觉得可以猜测到出问题的地方,你会修改你的程序,再次运行。如果你的想法是正确的,那么你可以预测这次修改的结果,这样就向正确的程序迈进了一步;如果是错的,你必须再次提出新的想法。就像夏洛克福尔摩斯说的那样:“当你排除了所有不可能,无论剩下的是什么,不管看起来多么不可能,那都是真相。

一名优秀的程序员是一名出色的侦探

R7d977f465262891752a71d77475f05e9
查看源图像

拒绝迷信式调试!!!

试来试去结果运行成功了,但是不知道为什么。

调式的基本步骤

  • 发现程序错误的存在

    1.程序员自己发现并解决

    2.软件测试人员 - 测试软件

    3.用户发现问题

  • 以隔离、消除等方式对错误进行定位

  • 确定错误产生的原因

  • 提出纠正错误的解决办法

  • 对程序错误予以改正,重新测试

Debug和Release的介绍

Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。

Release称为发布版本,它往往进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

image-20210506193439476

那他们两有什么区别呢?看个例子

int main()
{
    int arr[10]={0};
    int i=0;
    for(i=0;i<10;i++)
    {
        arr[i]=i;
    }
    return 0;
}

image-20210506193613754

Debug版本我们可以通过调试一步一步的来观察监视程序

而当我们换成Release版本时:

image-20210506193713381

这时就不能进行调试了

我们在分别运行Debug版本和Realease版本时能在文件路径找到Debug文件和Realease文件

image-20210506193945094

我们分别进去Debug文件和Release文件里面有个可执行程序文件(.exe)

image-20210506194258703

仔细对比发现,两个的大小不一样,是因为Debug版本我们含有调试信息,不作任何优化;而Release不含有调试信息,还会进行优化,大小更小一些。

windows环境调试介绍

1.调试环境的准备

image-20210506195143840

确保环境切换为Debug

2.学会快捷键

常用快捷键

image-20210506202121096

  • F5

启动调试,经常用来直接调到下一个断点处。


  • F9

创建断点和取消断点 断点的重要作用,可以在程序的任意位置设置断点。这样就可以使得程序在
想要的位置随意停止执行,继而一步步执行下去。


  • F10

逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。


  • F11

逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是
最长用的)。

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

查看临时变量的值

image-20210507144736515

查看内存信息

image-20210507143748893

image-20210507144203646

查看汇编信息

image-20210507144338191

image-20210507144957968

查看调用堆栈

q

通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。前面的是被后面的调用的。

查看寄存器

image-20210507145750471

一些调试的实例

实例一:

求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;
}

image-20210514151149173

1!+2!+3!这个代码有问题,如果我们实在想不明白,我们进行调试

image-20210514151011047

image-20210514211837742

通过一步一步认真调试监视发现,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;
}

运行结果是啥?为什么?

image-20210514185731165

运行后会进入死循环打印haha,数组明明越界了,为什么不报错呢?是因为它陷进了死循环,停不下来,所以不会报错。

image-20210514212203824

我们进行调试,调试后发现arr[12]恰好就是i,改变arr[12]时也改变i,所以进行了死循环。

原因如图解释所示:

image-20210514161326828

i和arr是局部变量,局部变量是放在栈区上的
栈区的使用习惯是:先使用高地址空间,再使用低地址。

我们在将程序换成release版本运行

image-20210522092221173

程序会停下来,那是因为release版本是对代码进行优化过的,代码大小和运行速度都是最优的,这里在release版本下,将局部变量i放在了低地址处,看下图

debug版本

i在高地址

image-20210522093300371

release版本

i到了低地址

image-20210522093414009

所以在release版本底下,代码进行了优化,不会死循环

如何写出好的代码

优秀的代码:

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

常见的coding技巧:

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

模拟实现库函数: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;
}

当我们传入空指针时,断言会提示你哪里错了,是不是很美妙,所以学习良好的编程习惯是对别人的友好,更是对自己的友好。

image-20210516092142740

最终最优满分代码

我们会发现库函数strcpy,拷贝源头加了const,函数返回类型为char*。

为什么这样做呢,就是因为我们有时候可能会把拷贝源头和拷贝目的地写反,就会报错,因为两个字符串大小不一样的话,就有问题了

加上const时,当我们写反时,会有以下报错

image-20210516093155681

strcpy库函数返回的是目标空间的起始空间

image-20210516092723151

#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=&num;//限制*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=&num;//限制*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=&num;//限制*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=&num;//限制*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编译器常用调试详解的主要内容,如果未能解决你的问题,请参考以下文章

实用调试技巧下篇

C语言VS2017 - 实用调试技巧

继续分享 5 个实用的 vs 调试技巧

VS2017实用调试技巧

vs环境——C语言实用调试技巧

Chrome 实用调试技巧