学习C语言之调试技巧

C语言调试技巧

1、什么是BUG?

        计算机程序错误(来源于第一次被发现的导致计算机错误的飞蛾(bug,虫子))。

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

        2.1、调试是什么?

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

        2.2、调试的基本步骤

        1)发现程序错误的存在

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

        3)确定错误产生的原因

        4)提出纠正错误的解决方法

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

        2.3、Debug和Release的介绍

        在VS2019界面左上角可以选择环境。分为Debug/Release 和 x86/x64 。

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

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

演示如下:

int main()
{
	int i = 0;
	int arr[10] = { 0 };

	for (i = 0; i <= 12; i++)
	{
		arr[i] = 0;
		printf("haha\n");
	}
	//Debug 模式运行,可以运行,但死循环。
	//Release 模式运行,可以运行,不死循环。
	return 0;
}

//进入调试发现,arr[12] 地址和i 地址相同
//arr[12] = 0 即 i = 0,循环重新开始,致死循环

3、Windows环境调试介绍

        3.1、调试环境的准备——在环境中选择debug选项,才能使代码正常调试。

        3.2、学会快捷键

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

        F9——创建断点和取消断点。断点可以设置在程序的任意位置,就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。断点可以通过右击红点设置条件,节省循环时间。

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

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

        CTRL+F5——开始执行(不调试),用于不调试,直接运行程序的情况。

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

                3.3.1、查看临时变量的值

                调试>窗口>监视>监视1(任选一个都行)>输入变量名:在调试开始之后,用于观察变量的值。

                3.3.2、查看内存信息

                调试>窗口>内存>内存1(任选一个都行)>输入地址:在调试开始之后,用于观察内存信息。

                3.3.3、查看调用堆栈

                调试>窗口>调用堆栈:反映函数的调用关系以及当前调用所处的位置。

                3.3.4、查看汇编信息

                调试>窗口>反汇编(或直接鼠标右键>反汇编):可以切换到汇编代码。

                3.3.5、查看寄存器信息

                调试>窗口>寄存器:可以查看当前运行环境的寄存器的使用信息。

4、多多动手,尝试调试,才能有进步

程序员大量时间需要调试代码,有必要熟练掌握调试技巧,多使用快捷键提升效率。

        4.1、实例1

int main()
{
	//求1!+2!+...n!,从1到n的阶乘之和
	int n = 0;
	scanf("%d", &n);	//输入 3 ,预期1-3阶乘之和为9
	int i = 0;
	int j = 0;
	int ret = 1;
	int sum = 0;

	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);	//打印结果 15 。 不符合预期,开始调试。
	//进入调试发现,第三轮阶乘开始时,ret是2,致3的阶乘由1*1*2*3=6,变为2*1*2*3=12。
	//合理推测,出现这一现象,是ret在每次阶乘开始时未置1。
	//修改程序,使每次阶乘开始时ret = 1。

	return 0;
}

调试时先找到原因,然后对症下药,如下:

int main()
{
	//求1!+2!+...n!,从1到n的阶乘之和
	int n = 0;
	scanf("%d", &n);	//输入 3 ,预期1-3阶乘之和为9
	int i = 0;
	int j = 0;
	int ret = 1;
	int sum = 0;

	for (i = 1; i <= n; i++)
	{
		ret = 1;	//每次阶乘时ret置1。
		for (j = 1; j <= i; j++)
		{
			ret *= j;
		}
		sum += ret;
	}
	printf("%d\n", sum);	//打印结果 9 。 符合预期。

	return 0;
}

       4.2、实例2

int main()
{
	//打印13行 "haha" 。
	int i = 0;
	int arr[10] = { 0 };

	for (i = 0; i <= 12; i++) //预期打印13行 haha 。
	{
		arr[i] = 0;
		printf("haha\n");	//死循环打印 haha 。
	}

	//进入调试发现,arr[12] 地址和i 地址相同
	//arr[12] = 0 即 i = 0,循环重新开始,致死循环

	return 0;
}
//死循环原因为越界,致改变i的值,修改条件至不越界。

调试时先找到原因,然后对症下药,如下:

int main()
{
	//打印13行 "haha" 。
	int i = 0;
	int arr[13] = { 0 };	//扩大数组容量。

	for (i = 0; i <= 12; i++) //预期打印13行 haha 。
	{
		arr[i] = 0;
		printf("haha\n");	//死循环打印 haha 。
	}

	//进入调试发现,arr[12] 地址和i 地址相同
	//arr[12] = 0 即 i = 0,循环重新开始,致死循环

	return 0;
}

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

        5.1、优秀的代码

        1)代码运行正常。

        2)bug很少。

        3)效率高

        4)可读性高。

        5)可维护性高。

        6)注释清晰。

        7)文档齐全。

常见技巧:

        1)使用assert(断言)。

        2)尽量使用const。

        3)养成良好的编码风格。

        4)添加必要的注释。

        5)避免编译的陷阱。

模拟实现strcopy 库函数,并逐步优化,演示如下(逐步):

//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{
	while (*src != '\0')	//字符源数组不为 \0 执行。
	{
		*dest = *src;	//字符源数组的首字符赋值给目标字符数组首地址
		dest++;			//目标字符数组地址+1,即指针移动到下一个元素。
		src++;			//字符源数组地址+1,即指针移动到下一个元素。
	}
	*dest = *src;	//字符源数组的字符赋值给目标字符数组地址,这里对应 \0 。
	//循环走到最后, *src = \0 ,不执行循环,跳出后 \0 未拷贝。
}

int main()
{
	char arr1[10] = "XXXXXXXXXX";
	char arr2[] = "hello";

	my_strcopy(arr1, arr2);
	printf("%s\n", arr1);	//打印结果 hello 。
	//拷贝成功,'\0'一起拷贝了。

	return 0;
}

第一步做到代码运行正常,第二步:

//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{
	while (*src != '\0')	//字符源数组不为 \0 执行。
	{
		*dest ++ = *src ++;	//字符源数组的字符赋值给目标字符数组地址,然后各自+1
	}
	*dest = *src;	//字符源数组的字符赋值给目标字符数组地址,这里对应 \0 。
	//循环走到最后, *src = \0 ,不执行循环,跳出后 \0 未拷贝。
}

int main()
{
	char arr1[10] = "XXXXXXXXXX";
	char arr2[] = "hello";

	my_strcopy(arr1, arr2);
	printf("%s\n", arr1);	//打印结果 hello 。
	//拷贝成功,'\0'一起拷贝了。

	return 0;
}

简单优化,减少语句,保持功能同时减少语句,再优化:

//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{
	while (*dest++ = *src++)	
	{
		;	
	}
	//字符源数组的字符赋值给目标字符数组地址,然后各自+1,并判断表达式结果
	//赋值表达式的结果是赋值后左边变量的值
	//字符的值是其对应的ASCII码, \0 的ASCII 码是0。
	//即前面字符判断都不为0,执行循环。
	//最后一次判断时,发现*dest被赋值后为 \0 = 0,跳出循环。
}

int main()
{
	char arr1[10] = "XXXXXXXXXX";
	char arr2[] = "hello";

	my_strcopy(arr1, arr2);
	printf("%s\n", arr1);	//打印结果 hello 。
	//拷贝成功,'\0'一起拷贝了。

	return 0;
}

优化后间接明了,可读性高,继续优化,使用assert(断言)提升可维护性,错误展示:

#include <assert.h>
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{
	assert(dest != NULL);	//断言,不满足条件会报错并注明报错位置。
	assert(src != NULL);	//断言,不满足条件会报错并注明报错位置。
	//将arr2 改成 NULL,会如下报错。
	//Assertion failed: src != NULL, file D:\C Projects\test7_15\test7_15\test.c, line 249
	//上文显示了报错原因,不满足 src != NULL 。
	//上文还显示了报错语句所在的文件、行号,方便管理。

	while (*dest++ = *src++) //循环赋值。
	{
		;	
	}

}

int main()
{
	char arr1[10] = "XXXXXXXXXX";
	char arr2[] = "hello";

	my_strcopy(arr1, NULL);
	printf("%s\n", arr1);	//报错 。

	return 0;
}

正确展示:

#include <assert.h>
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, char* src)
{
	assert(dest != NULL);	//断言,不满足条件会报错并注明报错位置。
	assert(src != NULL);	//断言,不满足条件会报错并注明报错位置。
	while (*src++ = *dest++)	//循环赋值。 若写反 dest 和src,会赋值错。
	{
		;
	}

}

int main()
{
	char arr1[10] = "XXXXX";
	char arr2[] = "hello";

	my_strcopy(arr1, arr2);
	printf("%s\n", arr1);	//打印结果 XXXXX 。

	return 0;
}

使用const,提升健壮性,错误展示:

#include <assert.h>
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest,const char* src)	//const修饰 *src,使其不可被改变。
{
	assert(dest != NULL);	//断言,不满足条件会报错并注明报错位置。
	assert(src != NULL);	//断言,不满足条件会报错并注明报错位置。
	while (*src++ = *dest++)	//循环赋值。 若写反 dest 和src,会直接报错。
	{
		;
	}
	//这里报错 307 行 左值指定 const 对象。意思*src不能被改变,方便检查。

}

int main()
{
	char arr1[10] = "XXXXX";
	char arr2[] = "hello";

	my_strcopy(arr1, arr2);
	printf("%s\n", arr1);	//报错 。

	return 0;
}

正确展示:

#include <assert.h>
//字符串拷贝
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
void my_strcopy(char* dest, const char* src)	//const修饰 *src,使其不可被改变。
{
	assert(dest != NULL);	//断言,不满足条件会报错并注明报错位置。
	assert(src != NULL);	//断言,不满足条件会报错并注明报错位置。
	while (*dest++ = *src++)	//循环赋值。 若写反 dest 和src,会直接报错。
	{
		;
	}

}

int main()
{
	char arr1[10] = "XXXXX";
	char arr2[] = "hello";

	my_strcopy(arr1, arr2);
	printf("%s\n", arr1);	//打印结果 hello 。

	return 0;
}

将返回值改成char*,可以直接打印返回值,演示如下:

//字符串拷贝,返回目标空间的起始地址。
//将一个字符串中的字符复制给另一个字符串,\0也复制进去
char* my_strcopy(char* dest, const char* src)	//const修饰 *src,使其不可被改变。
{
	assert(dest != NULL);	//断言,不满足条件会报错并注明报错位置。
	assert(src != NULL);	//断言,不满足条件会报错并注明报错位置。
	char* ret = dest;
	while (*dest++ = *src++)	//循环赋值。 若写反 dest 和src,会直接报错。
	{
		;
	}
	return ret;
}

int main()
{
	char arr1[10] = "XXXXX";
	char arr2[] = "hello";

	printf("%s\n", my_strcopy(arr1, arr2));	
	//打印结果 hello 。一行代码打印,使用时更方便。

	return 0;
}

    

    5.2、const的作用

        const修饰指针变量时

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

指针指向内容不能改变,演示如下:

int main()
{
  //const 在*左边,指针指向的内容不能被改变。
	int a = 0;
	const int* pa = &a;	//const修饰 *pa,使其指向的内容不可被改变
	*pa = 10;	//报错。左值指定 const 对象,意思不能被改变。

	return 0;
}

指针变量本身可变,演示如下:

int main()
{
  //const 在*左边,指针指向的内容不能被改变。
	int a = 0;
	int b = 0;
	const int* pa = &a;	//const修饰 *pa,使其指向的内容不可被改变
	//进入调试,看到这里 pa 是 0x0113fdb8 。
	pa = &b;	
	//进入调试,看到这里 pa 是 0x0113fdac 。
  //即*pa指向的内容不能被改变,但pa指针变量本身可以被改变。

	return 0;
}

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

指针变量本身不能改变,演示如下:

int main()
{
	//const 在*右边,指针变量本身不能被改变。
	int a = 0;
	int b = 0;
	int* const pa = &a;	//const修饰pa,使其不可被改变
	pa = &b;	//报错 382 行 左值指定 const 对象。意思pa不能被改变。

	return 0;
}

指针指向的内容可变,演示如下:

int main()
{
	//const 在*右边,指针变量本身不能被改变。
	int a = 0;
	int b = 0;
	int* const pa = &a;	//const修饰pa,使其不可被改变
	*pa = 1;		// a 的值被改为1。
	//即const 在*右边,指针指向的内容是可以被改变的。
	return 0;
}

*左右均由const,则指针和指针指向内容均不可变,演示如下:

int main()
{
	int a = 0;
	int b = 0;
	const int* const pa = &a;	
	//const修饰 *pa 和 pa,使其指向的内容不可被改变,其本身也不可被改变。
	*pa = 1;	// 报错。左值指定 const 对象,意思*pa不能被改变。
	pa = &b;	//报错。左值指定 const 对象,意思pa不能被改变。

	return 0;
}

6、编程常见的错误

        6.1、编译型错误

        直接看错误提示信息(双击),解决问题。比如未写分号,直接双击就能定位过去,很容易处理。

        6.2、链接型错误

        看错误提示信息,主要在代码中找到错误信息中的标识符,然后手动定位问题。一般是标识符名不存在或者拼写错误。

        6.3、运行时错误

        借助调试,逐步定位问题。最麻烦。

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值