C语言拯救者 番外篇 (Windows实用调试技巧)

注:linux开发环境调试工具是gdb,后期博客会介绍。

目录

程序员要掌握的重要技术,便是要学会调试

1.1 调试是什么?有多重要?

1.2 调试的基本步骤

2.1 Debug和Release

2.2 下面程序在Debug和Release版本的区别

2.3 快捷键的使用方法

2.4例题:我想知道153是否为自幂数,你是否在傻乎乎的一直按着F10 ,那当i=100000要按多久?

 3.1 通过调试找出代码问题

实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。(下面代码有错误)

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

3.3 模拟实现库函数:strcpy(字符串拷贝)

my_strcpy函数设计返回值类型char*是为了实现函数的链式访问

4.1 Const练习解释

5.1 编程常见的错误

5.2 编译性错误就是语法错误

5.3 链接型错误 

5.4运行时错误 - 借助调试解决的错误 (求阶乘例题)


程序员要掌握的重要技术,便是要学会调试

有的女人就像Windows 虽然很优秀,但是安全隐患太大。 
有的女人就像UNIX 她条件很好,然而不是谁都能玩的起。 
有的女人就像C# 长的很漂亮,但是家务活不行。 
有的女人就像C++,她会默默的为你做很多的事情。 
有的女人就像JAVA,只需一点付出她就会为你到处服务。 
有的女人就像JAVA script,虽然对她处处小心但最终还是没有结果。 
有的女人就像汇编 虽然很麻烦,但是有的时候还得求它。 
有的女人就像 SQL,她会为你的发展带来莫大的帮助。 
爱情就是死循环,一旦执行就陷进去了。 
爱上一个人,就是内存泄露,你永远释放不了。 
真正爱上一个人的时候,那就是常量限定,永远不会改变。 
女朋友就是私有变量,只有我这个类才能调用。 
情人就是指针,用的时候一定要注意,要不然就带来巨大的灾难。


1.1 调试是什么?有多重要?

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

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


1.2 调试的基本步骤

发现程序错误的存在

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

确定错误产生的原因

提出纠正错误的解决办法

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


2.1 Debug和Release

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

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

2.2 下面程序在Debug和Release版本的区别

#include <stdio.h>
int main()
{
    int i = 0;
    int arr[10] = {0};
    for(i=0; i<=12; i++)
   {
        arr[i] = 0;
        printf("hehe\n");
   }
    return 0;
}

在debug版本中,程序死循环;在release版本中,程序可以执行

原因在于:变量在内存中开辟的顺序发生了变化,影响到了程序执行的结果。

代码运行的结果:大概率是死循环

首先arr只有10个元素,但是i循环到12次,arr[10]的时候已经越界访问了.首先i和arr是局部变量,先创建i,再创建arr,又因为局部变量都是放在栈区上的,那栈区的使用习惯是先使用高地址,再使用低地址

i如果先初始化就放在上面,arr就是数组,但是数组的下标的增长地址是由低到高来增长的,数组如果越界了,也许有一次越界到了i所在的地址,把i的地址和arr数组的地址改为了0,for循环就重新开始计算了,为什么说结果大概率是死循环呢,因为你不知道arr数组和i的地址中间差了几个字节,但是如果arr数组越界访问到i的话就会死循环

把arr和i放反也是因为栈区的使用习惯,i如果后初始化的话,i就是低地址空间,这样arr越界访问就永远不会访问到i的地址,所以他会打印完后程序崩溃

中间空了多少是编译器自己的写法问题,比如你去其他平台也许空的都不一样,那为什么会死循环不会报错呢?因为程序一直在跑,for循环没有停下来,虽然arr越界访问了,但是程序必须得先执行完才能够报错,得完成一件事情才行


	int main()
	{
		int i = 0;
		int arr[10] = {1,2,3,4,5,6,7,8,9,10};
		printf("%p\n", &i);
		printf("%p\n", &arr[9]);
		}
 

可以佐证,栈区空间使用习惯先使用高地址,再使用低地址

 


2.3 快捷键的使用方法

1.F5 开始调试

使用方法:直接按F5程序直接结束了,不能单独使用,需要配合F9(断点)使用

(多次按F5,会在原有逻辑断点处停下,例如循环从一次变为两次,停在下一次循环断点处),有人会发现按F5没有反应,这时候需要加上Fn+F5  or Fn+F10

2.Ctrl +F5 不调试,直接执行代码

3.F9  设置/取消 断点

使用方法:代码执行,按下F5,只有遇到断点F9处才停下

4.Ctrl + F 查找关键字

5.Ctrl +K+C 添加注释(配合全选使用)  Ctrl + K+U取消注释

6.C语言中复制不需要选中复制,直接在需要复制的那一行按Ctrl+V,即可复制

此外,还有更多的快捷键,博客添加到下面

VS中常用的快捷键_MrLisky的博客-CSDN博客_vs快捷键

有同学发现,我打开调试->窗口,并没有发现监视是为什么?

这是因为,监视里的很多窗口是需要调试开始的时候才会显示出来,监视就是用来配合代码查错,要多练习


2.4例题:我想知道153是否为自幂数,你是否在傻乎乎的一直按着F10 ,那当i=100000要按多久?


int main()
{
	int i = 0;
	for (i = 0; i <= 100000; i++)
	{
		int tmp = i;
		int n = 1;
		//第一步判断是几位数
		while (tmp / 10)
		{
			n++;
			tmp = tmp / 10;
		}
		//计算每一位次方和
		tmp = i;
		int sum = 0;
		while (tmp)
		{
			sum += pow(tmp % 10, n);
			tmp = tmp / 10;
		}
		//3.判断
		if (i == sum)
		{
			printf("%d ", i);
		}
	}
	return 0;
}

我们可以使用F5配合F9,在F9断点处右键->条件(条件断点),输入i==153(条件设置),这时候按F5,你会发现i跳过了152位,直接来到153


2.5  F10(逐过程,遇到函数不会进入函数内部,直接执行完整函数内容)

  F11(逐语句,遇到函数会进到函数,会执行代码的每个细节)

int Add(int a, int b)
{
	return a + b;
}

int main()
{
	int a = 1;
	int b = 2;
	int ret = Add(a,b);
	printf("%d", ret);
	return 0;
}

F10直到在函数Add处,按F11进入函数内部,配合监视窗口使用,此外监视还有自动监视(不推荐),自动监视帮你把所有的变量都放了出来,想要观察指定的数便容易出错,我们只需要在监视里面观察我们想要的数字即可

内存监视


调用堆栈,当函数调用逻辑复杂,可以查看堆栈的调用逻辑

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

 3.1 通过调试找出代码问题

实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。(下面代码有错误)

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

 for(int j=1; j<=i; j++)//n的阶乘
 {
 ret *= j;
 }
 sum += ret;
 }
 printf("%d\n", sum);
 return 0;
}


//1!=1
//2!=2
//3!=6
//9
我们想求3!,输入3却发现结果是15,肯定是代码出了问题,于是我们按F10开始调试,再监视中输入我们想观察的变量

第一次执行,

代码没有问题,1!就是1

 

第二次执行,2!就是2,但是我们继续向下执行,却发现ret值是2,要知道ret的作用是1*2*...*n的作用,ret值变了,结果自然也就错了

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

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

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

常见的coding技巧:

1. 使用assert

2. 尽量使用const

3. 养成良好的编码风格

4. 添加必要的注释

5. 避免编码的陷阱。

3.3 模拟实现库函数:strcpy(字符串拷贝)

把原指针指向的内容拷贝到目的空间指针指向的空间处,同时\0也要拷贝过去

 

满分10分,这个代码给5分,不及格 ,虽然他也能完成我们想要实现的字符串拷贝

void my_strcpy(char* dest, char* src)//将src空间处的字符拷贝到目标空间dest
{
	while (*src != '\0')
	{
		*dest = *src;
		dest++;
		src++;
	}
	*dest = *src;//再把\0拿下来
}

int main()
{
	char arr1[] = "hello bit";
	char arr2[20] = "xxxxxxxxxxx";
	my_strcpy(arr2,arr1);

	return 0;
}

如何改进?第一,字符串拷贝进行了两次,我们能否优化

可以改为后置++

void my_strcpy(char* dest, char* src)
{
	while (*src != '\0')
	{
		*dest++ = *src++;
	}
	*dest = *src;
}
assert是用来断言的,当我们传过来的两个指针为NULL时,没有断言便进行解应用时,我们便非法访问内存了,代码有风险
上面的代码还是不够简化,我们能否将dest = src 两句合在一起,
#include <assert.h>

void my_strcpy(char* dest, char* src)
{
	//assert(dest != NULL);//断言
	//assert(src != NULL);//断言

	assert(dest && src);//优化断言,一个为假(NULL)便报错

	while (*dest++ = *src++)//先执行src,src指向hello bit,由于++是后置,先使用再++
                            //h被*dest内容复制,表达式结果是h的ASCII码值
                            // \0的ASCII码值为0,先执行完\0 = x,表达式不成立结束循环
	{
		;
	}
}

然后我们看官方版本的strcpy介绍

char *strcpy( char *strDestination, const char *strSource );

my_strcpy函数设计返回值类型char*是为了实现函数的链式访问

还需要再加上const,让src目标空间处的变量不可被修改(变成常变量),以免代码出错

满分的函数设计!

char* my_strcpy(char* dest, const char* src)
{
	assert(dest && src);//断言
	char* ret = dest;
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

int main()
{
	char arr1[] = "hello bit";
	char arr2[20] = "xxxxxxxxxxx";
	char* p = NULL;

	printf("%s\n", my_strcpy(arr2, arr1));

	return 0;
}

4.1 Const练习解释

const是为了让我们代码更加健壮性,但是我们要怎么去使用const来精确的限制我们想要的代码呢?

int main()
{
	int num = 10;
	num = 20;//第一种方法改num变量值
	int* p = &num;
	*p = 100;//指针方法改变num变量值

	return 0;
}

加const又会怎么样?

int main()
{
    const int num =0;//常变量,不可被修改
    num = 20;  //编译器不通过
    int * p =&num;
    *p = 20;//成功改掉,编译器通过,证明我们可以使用指针来去改被修饰的常变量
    printf("%d\n", num);//20 这种操作破坏了const,本意不改却被投机取巧改掉了
	return 0;
}

把门锁了,不让你从门进,你却砸烂了窗户跳了进去,虽然进去了但是不合规矩

int main()
{
    const int * p =&num;
    *p = 20;//X 窗户也给你封死
    int * const p =&num;
    p = 20 ; //X  门封死
    const int * const p =&num;//X 窗户, 门封死也给你封死
	return 0;
}

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


 

5.1 编程常见的错误

5.2 编译性错误就是语法错误

int main()
{
   return 0     //漏掉了;  在编译过程中报错,没有运行起来
}

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

5.3 链接型错误 

//没有引头文件
//int Add(int x, int y)//或者没有定义Add函数
//{
	//return x + y;
//}

int main()
{
	int a = 10;
	int b = 20;
	
	int c = add(a, b);//函数名写错了
	
	printf("%d\n", c);

	return 0;
}

 

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

5.4运行时错误 - 借助调试解决的错误 (求阶乘例题)


C语言初阶知识点完结!

猜你喜欢

转载自blog.csdn.net/weixin_63543274/article/details/123756664
今日推荐