【C语言初阶】❤️ 学会调试技巧 希望你的代码永远不会出现bug(拒绝bug + 一定要收藏哦)❤️

温馨提示

大家好我是Cbiltps,在我的博客中如果有难以理解的句意难以用文字表达的重点,我会有配图。所以我的博客配图非常重要!!!

如果你对我感兴趣请看我的第一篇博客

开篇介绍

今天写的是【C语言初阶】的最后一篇内容:实用调试技巧,马上就会开始写【C语言进阶】的相关内容了!

我的博客已经写到了这里,到这个阶段很有可能遇到一个问题:写代码!

写代码的过程中好像似懂非懂的掌握了很多语法,虽然知道怎样写,但是写出来之后可能存在很多bug,它能编译、能运行起来,但是运行起来的结果不是我想要的、运行起来的程序可能崩溃了等等一系列原因。

那怎样才能解决问题呢?
那你就来我的博客学习,我教你用调试的技巧解决问题!

当然调试的能力是需要练习的,一定要积极的去实践,才能达到好的效果!!!

本章重点

  • 什么是bug?
  • 调试是什么?有多重要?
  • debug和release的介绍
  • windows环境调试介绍
  • 一些调试的实例
  • 如何写出好(易于调试)的代码
  • 编程常见的错误

正文开始


1. 什么是bug?


1945年,一只小飞蛾钻进了计算机的电路里,导致系统无法正常运行。一位名叫格蕾丝.赫柏的上尉将飞蛾拍死在工作日志上并写道:就是这个bug(虫子),害的我们今天的工作无法完成。至此来开了程序员与 bug75年的爱恨情仇。

讲到这里给大家介绍一个人:

扫描二维码关注公众号,回复: 15146341 查看本文章

一个标准的大美人儿

格蕾丝·赫柏(Grace Murray Hopper),1906年12月9日出生于美国纽约,计算机软件工程第一夫人,同时也是美国海军将军。

这位奶奶绝壁是个狠人,她的人生我只能用牛逼两个字形容!!!就是她,就是她!
在这里插入图片描述
然后这个是她那天的工作日志:
在这里插入图片描述


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


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

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

2.1 调试是什么?

你是这样写代码的吗?
在这里插入图片描述
你是这样调试的吗?
在这里插入图片描述
如果你有上述的情况,那你就是在迷信式调试!
对此,我表示拒绝!
我们要科学的调试!

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

2.2 调试的基本步骤

  • 发现承认程序错误的存在
  • 隔离消除等方式对错误进行定位
  • 确定错误产生的原因
  • 提出纠正错误的解决办法
  • 对程序错误予以改正重新测试

2.3 Debug和Release的介绍

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

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

这里有一个问题:

假如你是一个测试人员,你测试的是Debug版本还是Release版本呢?
作为一个测试人员要站在客户的角度所以,测试人员测试的是Release版本。


3. Windows环境调试介绍


3.1 调试环境的准备

在环境中选择debug选项,才能使代码正常调试。
在这里插入图片描述

3.2 学会快捷键

在这里插入图片描述
最常使用的几个快捷键

  • F5启动调试,经常用来直接跳到下一个断点处
    F5其实不能准确的用来调试,如果中间没有拦住它的话就结束了,所以F5要和F9配合使用。

  • F9创建断点取消断点
    断点可以在程序的任意位置设置断点。
    这样就可以使得程序在想要的位置随意停止执行继而一步步执行下去
    在这里插入图片描述

3.2.1 条件断点

假如你要设置断点一个循环的第n次循环处,这样可以吗?
答案肯定是可以的。

for (i = 0; i < 10; i++)
{
    
    
	printf("%d\n", arr[i]);
}

假如断点要设置到上段代码的 i = 3 处(按下图操作):
在这里插入图片描述

3.2.2 多个断点

如果你在程序中设置了多个断点的时候,按F5会发生什么?
在这里插入图片描述
如果你要进入下一个断电处,就先可以把上一个断点取消然后按F5进入你想进入的断点!

  • F10逐过程

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

  • F11逐语句
    就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是最长用的)。
    在这里插入图片描述

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

VS2019还有很多的快捷键,我在CSDN上找到了一篇快捷键博客大家可以去看一下!

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

调试模式下,点击调试进入窗口页面,会有下面的选项,下面的选项都可以点开!
在这里插入图片描述

3.3.1 查看临时变量的值

在调试开始之后,用于观察变量的值。
在这里插入图片描述

3.3.2 查看内存信息

在调试开始之后,用于观察内存信息。
在这里插入图片描述

3.3.3 查看调用堆栈

通过调用堆栈,可以清晰的反应函数的调用关系以及当前调用所处的位置。
在这里插入图片描述

3.3.4 查看汇编信息

什么是反汇编?

反汇编(Disassembly):把目标代码转为汇编代码的过程,也可以说是把机器语言转换为汇编语言代码、低级转高级的意思,常用于软件破解(例如找到它是如何注册的,从而解出它的注册码或者编写注册机)、外挂技术、病毒分析、逆向工程、软件汉化等领域。

学习和理解反汇编语言对软件调试、漏洞分析、OS的内核原理及理解高级语言代码都有相当大的帮助,在此过程中我们可以领悟到软件作者的编程思想。

总之一句话:软件一切神秘的运行机制全在反汇编代码里面。

进入反汇编的方式有两种方式:

  • 这是一种进入反汇编的方式:
    在这里插入图片描述
  • 另一种方法:调试模式下,右键点击转到反汇编即可。
    在这里插入图片描述

3.3.5 查看寄存器信息

可以查看当前运行环境的寄存器的使用信息。
在这里插入图片描述


4. 一些调试的实例


4.1 调试实例一

实现代码:求1!+2!+3! …+ n!(不考虑溢出)

#include <stdio.h>

int main()
{
    
    
	int n = 0;
	int i = 0;
	int ret = 1;
	int sum = 0;

	for (n = 1; n <= 3; n++)
	{
    
    
		for (i = 1; i <= n; i++)
		{
    
    
			ret *= i;
		}
		sum += ret;
	}
	printf("%d\n", sum);//打印后输出的竟然是15?

	return 0;
}

这时候我们期待输出9,但实际输出的是15?
直接进入调试并打开监视窗口:
在这里插入图片描述
最后修改bug:

for (n = 1; n <= 3; n++)
	{
    
    
		ret = 1;//在这里把 ret 清为1就可以了
		for (i = 1; i <= n; i++)
		{
    
    
			ret *= i;
		}
		sum += ret;
	}

调试的时候我们要做到几点:

  1. 首先推测问题出现的原因(初步确定问题可能的原因最好)。
  2. 实际上手调试很有必要。
  3. 调试的时候我们心里有数自己写的代码你要清楚,程序执行到哪里应该发生什么事情,如果没有发生想要的事情,那就是产生了bug!)。

4.2 调试实例二(2016Nice笔试题)

运行下面的代码看看会发生什么?

#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;
}

当我们运行发现是无限循环的时候,开始调试:
在这里插入图片描述
在此期间我们发现了一个问题:i的值和arr[12]的值都是相同的,地址也是相同的,为什么呢?
在这里插入图片描述
所以只要把i调到范围内就可以了:

for (i = 0; i <= 10; i++)//改成这样就可以了

在这里我们回顾前面的知识点:Debug和Release 我们知道Release版本可以做到优化,那我们改成Release版本看看会发生什么?
在这里插入图片描述
这是如何做到的?为了方便观察,我们把代码改成这样:


#include <stdio.h>

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[0]);
	printf("&p\n", &arr[9]);
	
	return 0;
}

在这里插入图片描述

这个案例出自一本书《C陷阱和缺陷》,这本书非常的经典大家下去可以看一下!

这个案例也曾经出现在2016Nice公司校招的笔试题中!

当你遇见这样的题的时候(注意三点):

  1. 局部变量放在栈区
  2. 栈区的使用习惯(高地址->低地址),可以画图解释
  3. 随着下标的越界就可能导致死循环

5. 编程常见的错误


  • 编译型错误
  • 链接型错误
  • 运行时错误
    在这里插入图片描述

5.1 编译型错误

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

#include <stdio.h>

int main()
{
    
    
	printf("haha\n");
	return 0 //注意这里没有加;
	//这就是编译型错误:语法错误
}

在这里插入图片描述

5.2 链接型错误

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

#include <stdio.h>

int main()
{
    
    
	int ret = Add(2, 3);//但是我并没有定义函数或者函数的名字写错也会链接错误
	printf("%d\n", ret);
	
	return 0;
}

在这里插入图片描述

5.3 运行时错误

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

上面的实例就是运行是错误,这里就不多举例了。


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


6.1 优秀代码中的coding技巧

  • 代码运行正常
  • bug很少
  • 效率高
  • 可读性高
  • 可维护性高
  • 注释清晰
  • 文档齐全

常见的coding技巧:

  • 使用assert(具体在下面的代码示范中会讲到)
assert(src != NULL);//这是一个宏,这个宏叫断言 —— 在Release版本中会被优化掉
assert(dest != NULL);

在这里插入图片描述

  • 尽量使用const(具体在下面的代码示范中会讲到)

在这里主要讲一下:const修饰指针

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

int n = 100;
const int*  p = &num;// int const* p = &num;//在 * 的左边就可以
*p = 20;//err
p = &n;//ok

2.const放在*的右边:修饰的是指针变量本身(p)
指针变量本身(p)不能修改了,但是指针指向的内容(*p)可以修改

int n = 100;
int* const p = &num;
*p = 20;//ok
p = &n;//err
  • 养成良好的编码风格
  • 添加必要的注释
  • 避免编码的陷阱

6.2 经典案例示范

在此之前,我们都知道C语言库函数有strcpy函数:

//它是这样用的
#include <stdiuo.h>
#include <string.h>

int main()
{
    
    
	//strcpy - string copy - 字符创拷贝
	char arr1[] = "abcdef";//arr1里面有7个字符,最后还有个\0
	char arr2[10] = {
    
     0 };
	strcpy(arr2, arr1);//arr1的字符全部拷贝到arr2
	printf("%s\n", arr2);

	return 0;
}

然后我们自定义一个函数来模拟实现库函数strcpy

//dest 是指向目标空间的
//src  是指向源字符串的

//1 这段代码不太好,只是完成了功能而已
void my_strcpy(char* dest, char* src)
{
    
    
	while (*src != '\0')
	{
    
    
		*dest = *src;
		dest++;
		src++;
	}
	*dest = *src;//传'\0'
}

//2 简洁了一点
void my_strcpy(char* dest, char* src)
{
    
    
	while (*src != '\0')
	{
    
    
		*dest++ = *src++;
	}
	*dest = *src;
}

//3 这样写更简洁
void my_strcpy(char* dest, char* src)
{
    
    
	//1. 拷贝字符
	//2. 遇到'\0'循环停止,'\0'本质就是0
	while (*dest++ = *src++)
	{
    
    
		;
	}
}

//虽然上面的代码越来越简洁了,但是还是不好!

因为我们用的是指针,所以有一个非常危险的点:我们遇见指针就直接用了,万一是空指针怎么办?

如果是空指针,会对空指针解引用,这个代码会崩溃(挂掉)!

我在初阶指针博客讲到:指针在使用之前最好对指针做一个判断!

那我们就这样设计函数:

//4 但是这种写法每次进入循环的时候都会判断,就会很麻烦
void my_strcpy(char* dest, char* src)
{
    
    
	if (src == NULL || dest == NULL)
	{
    
    
		return;
	}
	
	while (*dest++ = *src++)
	{
    
    
		;
	}

//5 但是这样还是有问题
#include <assert.h>

void my_strcpy(char* dest, char* src)
{
    
    
    assert(src != NULL);//这是一个宏,这个宏叫断言 —— 在Release版本中会被优化掉
	assert(dest != NULL);
	
	while (*dest++ = *src++)//如果这个条件不小心写反怎么办?
	{
    
    
		;
	}

//6 这个版本如果看不懂,请看下面的图解
void my_strcpy(char* dest, const char* src)//const让*src不被修改
//const放在这里,对于代码的健壮性(或者鲁棒性)增加了。
{
    
    
	assert(src != NULL);
	assert(dest != NULL);

	while (*dest++ = *src++)
	{
    
    
		;
	}
}

在这里插入图片描述

//7 经过了7次的优化,这个是最终版!
#include <stdio.h>
#include <assert.h>

char* my_strcpy(char* dest, const char* src)
{
    
    
	assert(src != NULL);
	assert(dest != NULL);

	char* temporary = dest;

	while (*dest++ = *src++)
	{
    
    
		;
	}
	return temporary;
}

int main()
{
    
    
	char arr1[] = "abcdef";
	char arr2[10]= "xxxxxxxxx";

	char* ret = my_strcpy(arr2, arr1);
	printf("%s\n", ret);

	return 0;
}

在写这个函数的时候,要注意几个点,避免跌入陷阱:

  1. 源字符串中一定要有\0
char arr1[] = {
    
     'a', 'b', 'c' };//这里没有'\0',在拷贝的时候会越界直到找到'\0'
char arr2[10] = "xxxxxxxx";
  1. 目标空间必须足够大
char arr1[] = "abcdef";//7个字符无法拷贝到三个字符的数组中
char arr2[3] = {
    
     0 };
  1. 目标空间必须可修改
char arr1[] = "abcdef";
const char* arr2 = "xxxxxxxxxxx";//目标空间没法修改,无法拷贝
//注意:这里的指针指向的是常量字符串,常量字符串是放在常量区的,不能修改。
//而且,这里最好用const来修饰,这样严谨的写代码安全性会提高。

//另外讲一个知识点:怎么把常量字符串放到了指针里?
//就是把常量字符串首元素地址存放在 arr2 中。

这个案例出自于一本书:《高质量C/C++编程》
书中最后章节试卷中有关strcpy模拟实现的题目。


7. 最后叮嘱一下


以后遇到代码跑出来是错误的:

  1. 三思——好好想一想问题在哪里
  2. 调试——想不出来问题在哪里,进行调试
  3. 请教——调试未解决,请教别人

告诫大家的话:

  • 一定要熟练掌握调试技巧。
    多多动手,尝试调试,才能有进步。
  • 初学者可能80%的时间在写代码,20%的时间在调试。
    但是一个程序员可能20%的时间在写程序,但是80%的时间在调试。
  • 我们所讲的都是一些简单的调试。
    以后可能会出现很复杂调试场景:多线程程序的调试等。
  • 多多使用快捷键,提升效率。

全文结束(感谢语)

【C语言初阶】全片结束,请大家关注我的后续作品【C语言进阶】的相关内容!

经过时间的磨练,如果你已经看我的博客学习到了这里,我相信你对C语言有了基础!

在这里感谢我的老师!
感谢我的每一位粉丝!
感谢每一个为我三连的人!
非常感谢,你们的支持就是我源源不断的动力!

猜你喜欢

转载自blog.csdn.net/Cbiltps/article/details/120392640