C语言--调试技巧

目录

1什么是bug?

2.调试

2.1调试是什么?

2.2 调试的基本步骤

 2.3 Debug和Release

release版本调试:

debug版本调试:

3.如何调试

3.1开始调试

3.2特殊情况(按F11的次数庞大):

方法1

方法2 

3.3自动窗口和局部变量:

自动窗口过程:

4.关于内存

5.调用堆栈

5.1栈的动态变化:

 6.小技巧(监视观察多个值):

疑惑: 

7一些调试的实例

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

问题出现:

实例二 死循环与数组越界该值 

研究程序死循环的原因。 

8.库函数:strcpy() -- 字符串拷贝

模拟实现库函数:

方式一

方式二

9.断言

10.const的作用

const在*左边

const在*右边

11.编程常见的错误


1什么是bug?

bug:本意是臭虫、缺陷、损坏、窃听器、小虫等意思。

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

多用来形容电脑程序或者软件出现问题,内部隐藏着不容易被发现的缺陷,会引起软件无法正常使用。

2.调试

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

一名优秀的程序员是一名出色的侦探。 每一次调试都是尝试破案的过程。

2.1调试是什么?

调试(英语:Debugging / Debug),又称除错,是发现和减少计算机程序或电子仪器设备中程序 错误(bug)的一个过程。(多练才能掌握技巧)

2.2 调试的基本步骤

 2.3 Debug和Release

程序员的版本:

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

测试人员的版本:

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

release版本调试:

按下F10开始执行调试 

打开监视窗口,输入变量名字

 先按F10走一步,接着按下F11

 

会发现进入以下程序,无法观察值的变化 

但如果改成debug,结果就不一样了

debug版本调试:

可以看到调试箭头进入函数内部,以及进入了循环体内,可以观察值的变化

3.如何调试

方法:

F5 - 开始调试 ,运行结果直接出现
ctrl + F5 开始执行(不调试)
F9 设置断点/取消断点


F10  逐过程 ,仅仅是一条语句一条语句的执行,会跨越函数执行

F11 逐语句,会进入函数内部

F5和F9是配合使用的,若按下了F9设置断点,再按下F5,就会直接跳到第一个断点处,这两个功能键一起使用,才能发挥用处

特殊按键:

Fn 是辅助功能键(可以关闭)

Fn键是组合键,一般位于键盘的左下角,它是需要配合其它按键一起来使用的,使用fn配合其它按键可以实现调节笔记本屏幕亮度、声音大小、关闭屏幕显示、禁用触控板、开启/关闭无线网络等功能。

3.1开始调试

按下F10,调试开始,若遇到循环条件数值大的, 或者代码量大的程序,将会异常麻烦,不可能都按F10或者F11让程序一步一步走,

 若此时认为前面的程序语句已经没有问题了,这时可以直接在你认为有毛病的或者你想停下的代码程序下按下F9,接着直接按下F5 

结果: 

此时可以发现整个程序就直接从断点处开始,前面的语句已经全部执行完了 

3.2特殊情况(按F11的次数庞大):

若认为程序在循环语句下第500次出现了问题,像这种程序一步一步的按下去是很麻烦的,

方法1

可以对着断点按鼠标右键,打开条件, 断点符号会发生变化(x输入错误,应该是i)

接着按下F5, 可以发现直接变成了500,但是此语句还未执行(前面的条件都执行了)

方法2 

可在想检查的循环次数中加入条件语句,下面是程序

#include<stdio.h>
void test()
{
	printf("hehe\n");
	printf("haha\n");
	printf("enen\n");
}
int main()
{
	int i = 0;
	char ch[] = "abcdef";
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	test();
	for (i = 0; i < 100; i++)
	{
		if (i == 50)
		{
			printf("aaa\n");
		}
		printf("%d ", i);
	//	printf("%d ", i);
	}
	/*for (i = 0; i < 10; i++)
	{
		printf("wakawaka\n");
	}*/
	return 0;
}

加入的条件语句:

if (i == 50)
        {
            printf("aaa\n");
        }

3.3自动窗口和局部变量:

 自动窗口:自动添加一些信息和捕获一些信息让你看到,程序执行的过程中上下文的变量自动加到这个窗口里面,优点是不用手动添加变量,自动就会添加,缺点是打印过程中有些变量会自动消失(自动删除)

自动窗口过程:

1

 2

 3

 局部变量和自动窗口很类似,只是不监视全局变量(两者都是自动生成不用手动添加)

 监视窗口可以监视变量的值和地址,也可以同时打开多个窗口

4.关于内存

 每一个变量都有它对应的地址,这里是一行显示1列

这里是一行显示4列 

5.调用堆栈

void test2()
{
	printf("hehe\n");
}
void test1()
{
	test2();
}
void test()
{
	test1();
}
int main()
{
	test();
	return 0;
}

原理:

5.1栈的动态变化:

起初:

 过程: 

 最终结果:

 6.小技巧(监视观察多个值):

void test(int arr[])
{


}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	test(arr);
	return 0;
}

疑惑: 

因为arr指针中存放的是第一个元素的地址 所以只能看到一个元素

解决方法: 在arr后面加入,和数字(必须小于数组长度)

总结:

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

一定要熟练掌握调试技巧。

初学者可能80%的时间在写代码,20%的时间在调试。

但是一个程序员可能20%的时间在写 程序,但是80%的时间在调试。

我们所讲的都是一些简单的调试。 以后可能会出现很复杂调试场景:多线程程序的调试等。

多多使用快捷键,提升效率。

7一些调试的实例

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

错误代码:

//实现代码:求 1!+ 2!+3!+...+10!不考虑溢出
int main()
{
	int n = 0;
	int ret = 1;
	int i = 0;
	int sum = 0;
	for (n = 1; n <= 3; n++)
	{
		
		for (i = 1; i <= n; i++)
		{
			ret *= i;
		}
      sum = sum + ret;
	}
	printf("%d\n", sum);
	return 0;
}

前提说明: 

首先按下F10,开始调试,打开监视窗口输入以下的变量

问题出现:

解题顺序:1

可以看出当n=3时,sum已经求完了1!+2!,所以前两个阶乘的之和的值是为3的,接着当代码走完当n=3,i=1时此语句中的条件语句时发现此时ret的值为2,根据前面知识可知i的值是为了求某次阶乘的具体值

所以i=1应该为ret=1!=1*1,答案为啥是2呢,接着往下看

2

i=2时,ret=4,此时应该为ret=2!=1*2=2,答案也不相符

3

到i等于3时竟发现ret=12,但是ret=3!=1*2*3=6,怎么可以等于12呢,推测应该是第一步的时候就错了

4

可以看到,当n=2时,i=2时,此时ret=2,所以导致了n=3,i=1时,ret=1*2=2,说明了此时ret还是保留了那个2的值,没有经历过重置

所以正确的代码是:

//实现代码:求 1!+ 2!+3!+...+10!不考虑溢出
int main()
{
	int n = 0;
	int ret = 1;
	int i = 0;
	int sum = 0;
	for (n = 1; n <= 3; n++)
	{
		ret=1;
		for (i = 1; i <= n; i++)
		{
			ret *= i;
		}
      sum = sum + ret;
	}
	printf("%d\n", sum);
	return 0;
}

ret要在每次求完一个阶乘的时候重置为1,才能正确算出值

实例二 死循环与数组越界该值 

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

代码运行:

研究程序死循环的原因。 

图解:

原理:

1.i和arr是局部变量,局部变量是放在栈区上的

2.栈区内存的使用习惯是:先使用高地址处的空间,再使用低地址处的空间

解释一下第2条:这条原理的作用体现在栈上,代码先定义的i,那么i就会在高地址处,后定义的是数组arr,数组在低地址处,所以上图的arr是位于i的下方处的

3.数组随着下标的增长, 地址是由低到高变化的

关于arr和i关系这是我的个人的理解:

假设i的地址是0x11223344,arr这个数组经过循环越界到了i的那块空间里面去,直到数组arr的下标为i时,即为arr[i]时,arr[i]和i是属于同一块空间,所以对于arr[12]=0,进行赋值的时候,相当于将i的值也改成了0

类比为:对于同一个房间,灯是开着的,我进去把灯关了,那你进去的时候,房间就是黑的

8.库函数:strcpy() -- 字符串拷贝

#include<string.h>
int main()
{
	char arr1[] = "hello world";
	char arr2[20] = { 0 };
	strcpy(arr2, arr1);
	printf("%s\n", arr2);

}

代码运行: 

模拟实现库函数:

方式一

#include<string.h>
void my_stcpy(char* dest, char* src)
{
	while (*src != '\0')
	{
		*dest = *src++;
	}
	*dest = *src;//\0的拷贝
}


int main()
{
	char arr1[] = "hello world";
	char arr2[20] = { 0 };
	strcpy(arr2, arr1);
	printf("%s\n", arr2);

}

原理:

这种写法在while循环内不会拷贝'\0',所以要单独写一条   *dest = *src;

方式二

#include<stdio.h>
void my_stcpy(char* dest, char* src)
{
	while (*dest++ = *src++)
	{
		;
	}
}
int main()
{
	char arr1[] = "hello world";
	char arr2[20] = { 0 };
	my_strcpy(arr2, NULL);//若传参传错了就会抛出问题
	printf("%s\n", arr2);

}

遇到'\0',其ascll码值是0,0为假,跳出循环,这样写即把字符串拷贝过去,也把'\0'拷贝过去,但是到了'\0'的位置,dest和src还是会++

9.断言

作用:发现问题,就会把问题抛出来

假设传参传错了,错误参数:NULL 

my_strcpy(arr2, NULL);

使用断言

#include<stdio.h>
#include<string.h>
#include<assert.h>
void my_strcpy(char* dest, char* src)
{
	//断言
	assert(dest&&src != NULL);//条件为真的话就不会报错
	while (*dest++ = *src++)
	{
		;
	}
}
int main()
{
	char arr1[] = "hello world";
	char arr2[20] = { 0 };
	my_strcpy(arr2, NULL);//若传参传错了就会抛出问题
	printf("%s\n", arr2);
}

代码运行:

假设不用断言,就不知道错在哪里

用以下这个代码

#include<stdio.h>
void my_strcpy(char* dest, char* src)
{
	if (dest == NULL || src == NULL)//遇到问题不解决问题,逃避问题
	{
		return;
	}
	while (*dest++ = *src++)
	{
		;
	}
}
#include<string.h>
int main()
{
	char arr1[] = "hello world";
	char arr2[20] = { 0 };
	my_strcpy(arr2, NULL);
	printf("%s\n", arr2);
}

执行: 

10.const的作用

const在*左边


int main()
{
	int n = 10;
	const int m = 0;//m=20;//err,错误
	
	//const修饰指针
	//1.const 放在*的左边,*p不能改了,也就是p指向的内容,不能通过p来改变了。但是p是可以改变的,p可以指向其他的变量
	const int* p = &m;
	//*p = 20;err
	p = &n;//ok
   return 0;
}

1.const 放在*的左边,*p不能改了,也就是p指向的内容,不能通过p来改变了。但是p是可以改变的,p可以指向其他的变量

const在*右边

#include<stdio.h>
int main(){
 int n = 10;
 const int m = 0;//m=20;err,错误

//2.const 放在*的右边,限制的是p,p不能改变,但是p指向的内容*p,是可以通过p来改变
    int* const p = &m;
	*p = 20;
	//p = &n;
	printf("%d\n", m);
	return 0;
}

2.const 放在*的右边,限制的是p,p不能改变,但是p指向的内容*p,是可以通过p来改变

图解:

3.这张图还包括了const两边都有*,那么以上两种赋值方式都是错误的

结论:

const修饰指针变量的时候:

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

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

11.编程常见的错误

编译型错误

直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。

链接型错误

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

运行时错误

借助调试,逐步定位问题。最难搞。

最后:

做一个有心人,积累排错经验。

猜你喜欢

转载自blog.csdn.net/weixin_65186652/article/details/129184528