[Visual Studio]----调试技巧

目录

1. 什么是bug?

2. 调试是什么? 有多重要?

2.1 调试是什么? 

2.2 调试的基本步骤

2.3 Debug和Release的介绍

3. Windows环境调试介绍

3.1 调试环境的准备

3.2 调试快捷键

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

3.3.1 查看临时变量的值

3.3.2 查看内存信息

3.3.3 查看调用堆栈

3.3.4 查看汇编信息

4. 调试实例

4.1 实例1

4.2 实例2

5. 如何写出好(易于调试)的代码

5.1 优秀的代码:

5.2 assert的作用

5.3 const的作用

5.4 优秀代码示范

5.4.1 模拟实现strcpy

5.4.2 模拟实现strlen


1. 什么是bug?

下面这张图片是历史上的第一个bug:
 

bug的本意是虫子的意思,历史上第一台计算机出现问题的原因是计算机中出现了一个虫子,后来我们引申了一下,把计算机中出现的漏洞或者造成程序异常的情况我们叫做bug

 第一次被发现的导致计算机错误的飞蛾,也是第一个计算机程序错误

2. 调试是什么? 有多重要?

所有发生的事情都一定有迹可循,如果问心无愧,就不需要掩盖也就没有迹象了,如果问心有愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越容易顺藤而上,这就是推理的途径。


顺着这条途径顺流而下就是犯罪,逆流而上,就是真相。
 

每一次调试都是尝试破案的过程 

调试不是一眼就能看出来的,需要我们慢慢用心去研究 

[Visual Studio]----调试技巧_第1张图片

2.1 调试是什么? 

我们了解了bug是什么,那么调试又是什么呢 ?

调试(Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。


2.2 调试的基本步骤
 

1.发现程序错误的存在


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


3.确定错误产生的原因


4.提出纠正错误的解决办法


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

 步骤看似简单,实则需要用心体会

2.3 Debug和Release的介绍
 

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

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

[Visual Studio]----调试技巧_第2张图片

 Debug版本可以对程序进行调试,用来发现程序的不足之处

Release版本是发布版本,不能对程序进行调试,而是直接执行程序

这两个版本可以在程序文件下找到,可以发现,Debug版本的程序内存比Release版本的大很多

3. Windows环境调试介绍
 

3.1 调试环境的准备
 

在环境中选择 debug 选项,才能使代码正常调试。

[Visual Studio]----调试技巧_第3张图片

3.2 调试快捷键

在调试中有许多可供选择的调试方式:

[Visual Studio]----调试技巧_第4张图片

F5:

启动调试

经常用来直接跳到下一个断点处(配合断点使用) 

F9:
创建断点和取消断点


断点的重要作用,可以在程序的任意位置设置断点


这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去


断点使用:

我们在想要停止的语句上打断点,再按F5,程序会直接到断点处停止

[Visual Studio]----调试技巧_第5张图片

也可以在循环中使用断点,并给断点附上条件,使断点到条件处停止

例如我们把断点的条件定在i == 5 时,当按F5执行程序时,程序会到断点处(i == 5 时)停止

[Visual Studio]----调试技巧_第6张图片

 [Visual Studio]----调试技巧_第7张图片


F10:

逐过程

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

F11:
逐语句 ( 比逐过程更加细致 )

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

CTRL + F5

开始执行不调试

如果你想让程序直接运行起来而不调试就可以直接使用

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

3.3.1 查看临时变量的值
 

在调试开始之后,用于观察变量的值
 

[Visual Studio]----调试技巧_第8张图片

 可以通过调试中的监视功能查看变量值

[Visual Studio]----调试技巧_第9张图片

3.3.2 查看内存信息
 

在调试开始之后,用于观察内存信息
 

[Visual Studio]----调试技巧_第10张图片

  可以通过调试中的内存功能查看内存所储存的信息

[Visual Studio]----调试技巧_第11张图片

3.3.3 查看调用堆栈
 

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

[Visual Studio]----调试技巧_第12张图片

栈:是一种数据结构,其特点是先进后出,这里的调用函数方式就是一种栈的存储模式,使我们清楚看见函数是被哪些函数调用的

3.3.4 查看汇编信息
 

 通过查看汇编信息,可以深层理解代码结构

第一种方式:

F10调试起来后,鼠标右键选择反汇编

[Visual Studio]----调试技巧_第13张图片

 第二种方式:
F10调试起来后,通过调试窗口进行选择:

[Visual Studio]----调试技巧_第14张图片

总结:多多动手,尝试调试,才能有进步

4. 调试实例

4.1 实例1

 实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出

给出如下有问题的代码:

#include
int main()
{
	int i = 0;
	int sum = 0;//保存最终结果
	int n = 0;
	int ret = 1;//保存n的阶乘
	scanf("%d", &n);
	for (i = 1; i <= n; i++)
	{
		int j = 0;
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);
	return 0;
}

当我们输入3时,3!应该 = 1 + 2 + 6 = 9

 

这时候我们输入3,期待输出9,但实际输出的是15,很显然此时Bug已经出现,这时候就需要我们进行调试

通过调试的监视功能,我们发现当我们调试到计算3的阶乘时,我们发现此时ret的值为12,也就是说程序计算的3的阶乘值是12,这时候错误就已经显现出来了。

原因是我们在计算完2的阶乘之后,ret的值没有重新赋值成1,导致3的阶乘是在2的阶乘上计算出来的,所以会对3的阶乘计算产生错误影响

解决方案:

此时只需要把计算完每次阶乘的时候,把ret赋值成1,错误就会迎面而解

[Visual Studio]----调试技巧_第15张图片

 改进后的代码:

#include
int main()
{
	int i = 0;
	int sum = 0;//保存最终结果
	int n = 0;
	int ret = 1;//保存n的阶乘
	scanf("%d", &n);
	for (i = 1; i <= n; i++)
	{
		int j = 0;
		ret = 1;//每计算完一个数的阶乘后,重置ret的值
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);
	return 0;
}

4.2 实例2

研究程序死循环的原因
 给出下面代码:

#include 
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;
}

我们从表面上看,数组的元素个数为10,,而在for循环中,共循环了12次访问数组内部元素,此时会对其他地方地址产生越界访问,程序因此崩溃

而当我们实际运行起来会发现,程序并没有崩溃,而是死循环打印


[Visual Studio]----调试技巧_第16张图片

当F10调试起来,通过观察当 a[11] = 0 时,i的值还是这个,而当进行arr[12] = 0操作时,此时i的值变为0

arr[11] = 0;

[Visual Studio]----调试技巧_第17张图片

arr[12] = 0; 

[Visual Studio]----调试技巧_第18张图片

而当我们查看i的地址和arr[12]的地址时,我们会发现它们两个的地址相同,所以显而易见,当我们修改arr[12]的值时,会对内存进行非法访问,而非法访问的地址正好也是i的地址,所以会造成i又从0开始,导致程序死循环

(虽然arr[12]不在我们申请的内存中,但它表示arr[9]后面的第3个地址,我们在对arr[12]进行访问,会访问到其他未申请的地址)

总结原因:

i和arr都是局部变量,都存储在栈区中。在栈区中创建局部变量是先使用高地址处的空间 再使用低地址处的空间,所以i的地址在arr的后面,又因为for循环导致数组越界访问,正好arr[12]的地址是i的地址,所以当修改arr[12]的值时,i的值也被修改成0,循环又从头开始,因此导致程序死循环。

[Visual Studio]----调试技巧_第19张图片

i和arr[12]的地址一样


5. 如何写出好(易于调试)的代码
 

5.1 优秀的代码:
 

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

常见的coding技巧:


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

5.2 assert的作用

首先介绍assert函数(头文件 assert.h): 

 assert( ) ,当()内为假时,程序会报错

assert相当于if判断更加严厉

  

5.3 const的作用

由const修饰的变量不能被更改 

[Visual Studio]----调试技巧_第20张图片

 const修饰指针变量的时候

1. const如果放在*的左边修饰的是指针指向的内容保证指针指向的内容不能通过指针来改变但是指针变量本身的内容可变


2. const如果放在*的右边修饰的是指针变量本身保证了指针变量的内容不能修改但是指针指向的内容,可以通过指针改变

5.4 优秀代码示范

5.4.1 模拟实现strcpy

strcpy在拷贝字符串的时,会把原字符串中的'\0'也拷贝过去 

返回值为目标字符串的首地址

char* my_strcpy(char* dest, const char* src)
{
	char* ret = dest;
	assert(dest && src);//断言 引用头文件

	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

5.4.2 模拟实现strlen

int my_strlen(const char* str)
{
	assert(str);
	int count = 0;
	while (*str)
	{
		count++;
		str++;
	}
	return count;
}


 

你可能感兴趣的:(C语言,c语言,学习)