实用调试技巧与案例分析

本文介绍了调试的基本步骤,包括Debug和Release版本的区别,常用调试快捷键如F5、F9、F10、F11等,并通过两个案例深入分析了如何通过调试找出并修复代码错误。文中还强调了编写易于调试的代码的重要性,如使用assert和const关键字,以及避免编程陷阱。
摘要由CSDN通过智能技术生成

目录

调试(Debug):

调试的基本步骤:

Debug和Release的介绍:

几个常用的快捷键:

案例一:

案例二:

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

案例一:

1.assert用法

2.const用法

案例二:

编程常见的错误


调试(Debug):

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

调试的基本步骤:

  1. 发现程序错误的存在;
  2. 以隔离,消除等方式对错误进行定位;
  3. 确定错误产生的原因;
  4. 提出纠正错误的解决办法;
  5. 对程序错误予以改正,重新测试

Debug和Release的介绍:

Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好的使用。

二者区别:

  1. 从生成文件的大小来看:Debug的文件比Release的文件大;
  2. Debug可以用于代码的调试,而Release不能;
  3. Debug不对代码进行优化,而Release则对代码进行优化

几个常用的快捷键:

  1. F5:启动调试,经常用来直接跳到下一个断点处,常和F9搭配使用;
  2. F9:创建断点和取消断点。断点的作用:可以在程序的任意位置设置断点。
  3. F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句;
  4. F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们进入函数内部,这也是最长用的一个快捷键;
  5. CTRL + F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

案例一:

实现代码:求1!+2!+3!+...+n!的值

先思考如何用代码实现求n!

int main()
{
	int n = 0;
	printf("请输入一个正整数n:");
	scanf("%d",&n);

	int i = 0;
	int ret = 1;

	for (i = 1; i<=n; i++)
	{
		ret = ret * i;
	}

	printf("%d\n",ret);

	return 0;
}

 通过调试可以发现,ret=1*2*3=6,代码运行正确,程序逻辑没有问题

接着我们试着去求1!+2!+3!+...+n!

int main()
{
	int n = 0;
	scanf("%d",&n);

	int i = 0;
	int j = 0;
	int sum = 0;
	int ret = 1;

	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}

		sum += ret;
	}

	printf("%d\n",sum);

	return 0;
}

当n=4时,通过运行程序可以发现结果却不等于33,那问题出在哪?通过进一步调试可以发现,当我们在求3!的阶乘时,ret的值本该等于6,但是最后的结果却是12,可想而知,问题应该是出现在求一个数的阶乘上。当我们在求2!时,ret的值等于2,但是当n++变为3的时候,我们发现ret的依旧等于2,此时的j=1<i=3,所以ret会继续*1*2*3,也就是将值变为了2*1*2*3=12,所以这才是ret=12的由来。可见,在求一个数的阶乘时,ret的值并没有初始化为1,而是将上一次运行时的结果累乘到本次运算上,进而导致问题的出现。我们只要在对某个数求阶乘之前,将ret初始化为1即可避免问题的发生。

int main()
{
	int n = 0;
	scanf("%d",&n);

	int i = 0;
	int j = 0;
	int sum = 0;
	int ret = 1;

	for (i = 1; i <= n; i++)
	{
		ret = 1;
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}

		sum += ret;
	}

	printf("%d\n",sum);

	return 0;
}

改进版:

int main()
{
	int n = 0;
	scanf("%d",&n);

	int i = 0;
	int sum = 0;
	int ret = 1;

	for (i = 1; i <= n; i++)
	{
		ret *= i;
		sum += ret;
	}

	printf("%d\n",sum);

	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("hehe\n");
	}

	return 0;
}

通过调试可以发现,数组中的元素是按地址连续存放的,即使是越界的三个元素,也是连续存放的。同时,我们可以发现变量i和arr[12]的地址竟是相同的,说明它们是共用一块内存空间。因此我们猜测,在对i的值进行修改时也会导致arr[12]的变化。

数组下标即便是越界的,在运行到arr[10]和arr[11]时, 同样会将其初始化为0。另外,我们发现,arr[12]的值确实是随着i进行变化的,说明我们的猜测是正确的。

当i=12时,将arr[12]的值也为12。但是,当运行到arr[12]=0时,i的值也在此刻同时变化为0。此时i的值为0依旧满足条件,所以又将进行下一轮循环。

这里面是有原因的,当然也有一定程度的巧合

  1. i 和arr 是局部变量,而栈区一般存放的是局部变量以及函数参数
  2. 栈区的使用习惯:先使用高地址处的空间,再使用低地址处的空间
  3. 数组随着下标的增长,地址是由低到高变化的 

所以arr和i在栈区的空间布局 ,应该是如下图所示:

那如何避免死循环的发生?

  1. 先定义arr数组再定义 i;
  2. 控制循环次数,i<=9

需要注意的是,不同编译器下局部变量 i 和 arr 在内存中的布局是不同的:

  1. 在vc6.0中i和arr数组之间没有空隙,即i<=10就陷入死循环
  2. 在gcc中i和arr数组之间空出一个int空间,即i<=11就陷入死循环

最后, Release会对代码进行优化使之不会进入死循环。所以,上面的程序在Release下运行是不会陷入死循环的,它会直接打印13遍“hehe”。

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

优秀的代码:

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

常见的coding技巧:

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

案例一:

模拟实现库函数strcpy

头文件:string.h

原型声明:char *strcpy(char* dest, const char *src)

功能:把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间(连同字符串串结束标志’\0’也一并拷贝)

初阶版:

void my_strcpy(char* dest, char* src)
{
	while (*src!='\0')
	{
		*dest = *src;
		src++;
		dest++;
	}
	*dest = *src;//把\0拷贝进去
}

改进版:

char* my_strcpy(char* dest,const char* src)
{
	assert(dest&&src);//断言:如果表达式dest != NULL为假就会报错
	
	char* ret = dest;//保存目标空间的地址
	//注意:字符'\0'就是数组0
	while (*dest++ = *src++)
	{
		;
	}

	return ret;//返回目标空间的地址
}

通过比对初阶版和改进版的区别,对知识点进行下列总结:

1.assert用法

assert是个宏,并非是个函数。assert 宏的原型定义在 assert.h 中,其作用是如果它的条件返回错误,则终止程序执行。

#include "assert.h" 
void assert( int expression );

assert 的作用是计算表达式 expression ,如果其值为假(即为0),那么它先向 stderr 打印一条出错信息,然后通过调用 abort 来终止程序运行。

使用 assert 的缺点是,频繁的调用会极大的影响程序的性能,增加额外的开销。

assert是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为false(0),程序将报告错误并终止执行。如果表达式不为0,则继续执行后面的语句。这个宏常用来判断程序中是否出现了明显非法的数据,如果出现了则终止程序以免导致严重后果,同时也便于查找错误。

assert只有在 Debug 版本中才有效,如果编译为 Release 版本则被忽略。

2.const用法

const放在*的左边(const int* p)
const修饰的是*p,表示p指向的对象不能通过p来修改,但是p变量中的地址是可以改变的
const int*p=int const*p

int main()
{
	const int num = 10;

	const int* p = &num;//加上const之后,值就不能进行修改
	//int const* p = &num;

	int n = 100;
	p = &n;//可以修改
	//*p = 20;//不能进行修改
}

const放在*的右边(int* const p)
const修饰的是p,表示p的内容不能被改变,但是p指向的对象是可以通过p来改变的

int main()
{
	const int num = 10;

	int* const p = &num;//const限制的是p,p不可以进行修改

	*p = 200;//可以修改

	int n = 100;
	//p = &num;//不能修改
}

案例二:

模拟实现库函数strlen

#include<assert.h>
int my_strlen(const char* str)
{
	int count = 0;
	assert(str);
	while (*str != '\0')
	{
		count++;
		str++;
	}
	return count;
}

int main()
{
	int len = my_strlen("abcdef");

	printf("%d\n",len);
	return 0;
}

编程常见的错误:

常见的错误分类:

  1. 编译型错误:直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。
  2. 链接型错误:看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不存在或者拼写错误。
  3. 运行时错误:借助调试,逐步定位问题。最难搞。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值