实用调试技巧

1. 什么是bug?

软件的BUG,狭义概念是指软件程序的漏洞或缺陷,广义概念除此之外还包括测试工程师或用户所发现和提出的软件可改进的细节、或与需求文档存在差异的功能实现等。

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

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

1)调试是什么?

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

2)调试的基本步骤

1.发现程序错误的存在

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

3.确定错误产生的原因

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

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

3)Debug和Release的介绍

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

 release不能一步一步调试。

3. Windows环境调试介绍

1)调试环境的准备

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

2)学会快捷键 

常用快捷键:

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

F9:创建断点和取消断点

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

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

F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。 F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最常用的)。

CTRL + F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。

F5和F9键的使用:

断点的意思是,程序只要有机会经过这个断点,就会在断点这个位置停下来。

点击F5键:

 

F10和F11的使用:

 按F10进行调试:

点击F11进入函数内部:

CTRL + F5的使用:

 程序直接运行起来而不调试。

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

(1)查看临时变量的值

在调试开始之后,用于观察变量的值。(一定是要开始调试之后)

 监视窗口是用得最多的。

(2)查看内存信息

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

 表示的是内存存放的数据,以十六制的形式显示。

(3)查看调用堆栈、

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

(4)查看汇编信息

(5)查看寄存器信息

4.一些调试的实例

实例1:

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

#include<stdio.h>
int main()
{
	int n = 0;
	scanf("%d", &n);
	int i = 0;
	int ret = 1;
	int j = 0;
	int sum = 0;
	for (j = 1; j <= n; j++)
	{
		//求和
		ret = 1;
		for (i = 1; i <= j; i++)
		{
			ret *= i;//求n的阶层
		}
		sum += ret;
	}
	printf("sum=%d\n", sum);
	return 0;
}

 

实例2:

问:这段代码会执行结果是什么?

问:什么原因导致以上代码陷入死循环呢?

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

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

2)常见的coding技巧: 1. 使用assert  2. 尽量使用const  3. 养成良好的编码风格  4. 添加必要的注释 5. 避免编码的陷阱。

示范:

模拟实现库函数:strcpy

strcpy的使用:

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[20] = { 0 };
	char arr2[] = "hello world";
	//把数组arr2的内容拷贝到arr1中
	strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

 

问:数组arr2里面的内容是字符串,那字符串后面的 '\0' 是否也拷贝到数组arr2中呢?

以上可知:strcpy在拷贝字符串的时候,会把源字符串中的 '\0' 也拷贝过去。 

用my_strcpy模拟实现库函数:strcpy

#include<stdio.h>
#include<string.h>
void my_strcpy(char* dest, char* src)
{
	while (*src != '\0')//把\0放到的dest所指向空间,程序就停下来
	{
		*dest = *src;
		dest++;
		*src++;
	}//以上循环过后,\0还未拷过去
	*dest = *src;
}
int main()
{
	char arr1[20] = "XXXXXXXXXXXXXX";
	char arr2[] = "hello world";
	//把数组arr2的内容拷贝到arr1中
	my_strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

以上代码虽然完成了任务,但是效果不好。

改进之后:

#include<stdio.h>
#include<string.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
	char* ret = dest;
	//断言
	assert(src != NULL);
	assert(dest != NULL);
	while (*dest++ = *src++)
	{
		;
	}
	//return dest; //err 
	return ret; //OK
}
int main()
{
	char arr1[20] = "XXXXXXXXXXXXXX";
	char arr2[] = "hello world";
	char* p = NULL;
	printf("%s\n", my_strcpy(arr1, arr2));
	return 0;
}

#include<stdio.h>
#include<string.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
    char* ret = dest;
	//断言
	assert(src != NULL);
	assert(dest != NULL);
	while (*dest++ = *src++)
	{
		;
	}
    //return dest; //err 
    return ret; //OK
}
int main()
{
	char arr1[20] = "XXXXXXXXXXXXXX";
	char arr2[] = "hello world";
	char* p = NULL;
	my_strcpy(arr1, arr2);
    printf("%s\n", arr1);
	return 0;
}
//为什么返回char*呢?
//是为了实现链式访问。
//返回char*时,这个函数的返回值就可以作为其他函数的参数
//strcpy函数返回的是目标空间的起始地址——即返回的是dest
//因为dest++了,所以直接return dest是不正确的
//所以刚开始的时候,可以用char* ret = dest
//(即先将目标空间的起始地址保存起来,return ret就可以了)
#include<stdio.h>
#include<string.h>
void my_strcpy(char* dest, char* src)
{
	while (*src != '\0')
	{
		*dest++ = *src++;
	}
	*dest = *src;
}
int main()
{
	char arr1[20] = "XXXXXXXXXXXXXX";
	char arr2[] = "hello world";
	//把数组arr2的内容拷贝到arr1中
	my_strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}
#include<stdio.h>
#include<string.h>
void my_strcpy(char* dest, char* src)
{
	while (*dest++ = *src++)//当*src指向\0,并把\0拷贝过去时;因为\0ASCII吗值为0
		                    //所以表达式为假;跳出循环
	{
		;
	}
}
int main()
{
	char arr1[20] = "XXXXXXXXXXXXXX";
	char arr2[] = "hello world";
	//把数组arr2的内容拷贝到arr1中
	my_strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

使用assert:

#include<stdio.h>
#include<string.h>
#include<assert.h>
void my_strcpy(char* dest, char* src)
{
	//断言
	assert(src != NULL);//如果这个表达式为假的时候,assert报错,引用头文件#include<assert.h>
	while (*dest++ = *src++)
	{
		;
	}
}
int main()
{
	char arr1[20] = "XXXXXXXXXXXXXX";
	char arr2[] = "hello world";
	char* p = NULL;
	my_strcpy(arr1, p);//不小心传了个空指针
	printf("%s\n", arr1);
	return 0;
}

#include<stdio.h>
#include<string.h>
#include<assert.h>
void my_strcpy(char* dest, char* src)
{
	//断言
	assert(src != NULL);
	assert(dest != NULL);
	while (*dest++ = *src++)
	{
		;
	}
}
int main()
{
	char arr1[20] = "XXXXXXXXXXXXXX";
	char arr2[] = "hello world";
	char* p = NULL;
	my_strcpy(arr1, arr2);
	return 0;
}

尽量使用const:

万一有人将代码拷反了,将*dest++ = *src++写成* src++= *dest++,这时候可以用const,避免程序运行错误。

 报的不是运行的错误,而是编译的错误,直接将错误扼杀在编译中。

const详解:

虽然num不能直接被改,但是可以通过其他途径绕过去改变它的值 

问:如果const也锁定int *p呢?答:这样便不能通过*p改变num的值了。

1.当const 放在*的左边:

问:*p不能改变num的值,那p是否重新可以存放其放其他变量的地址呢?

答:可以

结论:当const 放在*的左边(即const int* p = &num),p所指向的对象不能通过*p来改变了,但是p本身的值是可以改变的。

2.const 放在*的右边:

结论: 当const 放在*的右边(即int* const p = &num),p所指向的对象可以通过*p来改变了,但是p本身的值是不可以改变的。

 

const int* const p = &num,p所指向的对象既不可以通过*p来改变了,p本身的值也不可以改变的。

const修饰指针变量的时候:

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

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


版权声明:本文为m0_68071216原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。