IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> c语言入门---调试技巧 -> 正文阅读

[C++知识库]c语言入门---调试技巧

什么是bug?

程序错误,即英文的Bug,也称为缺陷、臭虫,是指在软件运行中因为程序本身有错误而造成的功能不正常、死机、数据丢失、非正常中断等现象。 早期的计算机由于体积非常庞大,有些小虫子可能会钻入机器内部,造成计算机工作失灵。1947 年 9 月 9 日,赫柏及其团队发现了第一个 Bug。当天下午 3 点 45 分,赫柏在哈佛的 Mark II 电脑的日志簿上记录下了“第一个电脑故障”。问题的根源是一只飞蛾卡在了电脑的继电器触点之间,赫柏及时地把这只飞蛾粘在了 Mark II 的日志上,并用双关语写道:“第一次发现了真正的 Bug。”( “First actual case of bug being found.” )这个 Bug 其实是被其他人发现的,但是赫柏在日志上做了记录。从此以后,人们将计算机错误称为Bug,与之相对应,人们将发现Bug并加以纠正的过程叫做“Debug”,意即“捉虫子”或“杀虫子”。那么这个图片就是当年的bug:请添加图片描述

调试是什么?调试的基本步骤是什么?

调试是什么?

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

调试的基本步骤是什么?

那么我们的调试的过程分为这么几个部分,首先是我们程序员自己发现错误,我们在运行的代码的过程中编译器会自动帮助我们识别一些错误,那么这时我们程序员自己就会寻找这些错误出现的原因,来一步一步的修改我们的代码,那么这就是我们调试的第一步,那么我们程序员也不是万能的,有些错误可能藏的比较隐蔽他没发现,那么这时就到了我们的测试人员上场了,他们就会用专门的软件,根据公司对这个程序的要求来进行专业的测试来发现这个代码中的bug,那么发现错误之后就会返回给我们的程序让其对其进行修改,这样发现错误修改错误,一直到测试部门通过就发行出来给我们的用户使用,但是我们用户在使用的时候也会发现出来一些bug,比如说我们平时打的一些游戏是不是bug满天飞啊,比如说我玩的战地5那简直就是神仙打击,所以我们用户也会发现一些新的bug,那么这些bug发现出来之后,我们就会返还给我们的程序员,程序员就会根据这个bug来找到错误的原因,并且找到解决这个bug的方法,然后再进行测试再发行新的版本,然后再找到bug这样反反复复,比如说我们的王者荣耀出了这么多年了,更新了那么多次,但是依然会出现新的bug这是同样的道理,那么我们这里消除错误的方法一般都是以隔离的形式来对错误进行定位,因为我们的错误的出现一般都是模块的形式,不可能说整个写的代码都有问题,可能一个问题就出现在一个函数里面等等,比如说我们后面学的数据结构,我们就是采用函数的形式对其进行分装,那么我们以后发现错误的时候就可以对这些函数进行单独的测试来一个一个的排查我们的问题,那么这个就是我们的调试的过程。

Debug和release的区别

我们的同一段代码在运行的时候是可以产生两种不同的条件的,一个是Debug版本另外一个就是release版本,比如说我们这个代码:

#include<stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

那么我们这里首先在Debug的版本下运行一下:请添加图片描述
我们可以看到在对应的路径下面出现了这么一个Debug文件:
请添加图片描述
我们点进Debug文件夹就可以看到下面的这个场景:
请添加图片描述
那么我们这里就有一个应用程序,那么这个就是我们Debug条件下所创建的大小为39KB,那么这里我们可以将我们的Debug改成我们的release版本我们再来看看会发生什么样的情况:请添加图片描述
我们发现这里多了一个Release的文件夹,那么我们这里的点进去看也有一个可执行的程序:
请添加图片描述
但是在这个可执行程序跟我们上面的那个就有那么点点的不同,因为我们这里的大小要比我们上面的小很多,上面那个是39KB而我们这个只有9KB,所以单从这一点我们就可以看到我们这里的两个版本不仅仅只有名字上的不同,也会有一些细节方面上的不同,那么我们这里就来简单的介绍:
Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户最好地使用。
那么看了上面的介绍想必大家应该能够明白我们上面的两个可执行程序为什么大小上会有区别了,那么这里有小伙伴们就要说了,那么我们在平时写代码的时候是不是都使用release版本啊,因为这个执行速度更快啊,那么我们这里答案是不是的哈,虽然我们这里运行的速度快,并且内存小但是我们这个release版本他是无法进行调试的,而调试这个功能对我们程序员来说是非常重要的,比如说我们刚刚写的那个代码:
我们先用Debug版本调试一下:请添加图片描述
我们发现这个代码它可以一步一步的来进行调试,比如说我们这里的循环它循环的是十次,那么我们这里每次循环的过程我们都可以一一的观测出来,但是我们这里release版本就不会它会一下子跳过这整个循环,就像这样:请添加图片描述
所以这就是我们这里的一个主要的区别,所以说Debug版本是我们程序员用的版本,而我们的用户用的就是我们的release版本。

windows的调试介绍

调试的准备

那么这里的调试的准备就很简单我们只用将我们的版本改成Debug版本就行。

调试的操作

1.F5

启动调试,经常用来跳转到我们的下一个断点处。那么这句话是什么意思呢?我们可以来看一下我们上面的代码我们直接按一下F5看看会发生什么:请添加图片描述
我们发现我们这里直接打印出结果然后就直接结束了调试,那么这里出现这种的情况是因为我们没有设置断点,如果我们设置断点的话我们这里按个F5他就会自动跳到我们的断点出,等待着下一步的调试,那么我们这里是如何来设置我们的断点的呢?那么这里我们就只需要在你想要断点的那一行处按一下F9就可以了在这里设置一个断点,那么这时我们再按一下F5我们看看会有啥情况:请添加图片描述
我们发现这里的箭头直接指向了我们这里的断点处,那么这里我们就可以执行其他的操作,那么这里我们再按一下F5的话,我们这里就会直接跳到下一个断点处,那么我们这里的下面没有断点了,那么是不是就应该直接程序编译结束了呢?答案是并不是,虽然我们这里看的我们这里没有断点,但是我们这里是存在逻辑上的断点的,所以我们再按一次F5也不会直接退出调试的,我们可以看看这个代码:

#include<stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", i);
	}
	return 0;
}

在这里插入图片描述
我们按了一次F5我们的箭头理所当然的跑到我们这里的第一个断点处,此时我们可以通过监视来看到此时的i的值还是0,请添加图片描述
但是我们这里再按一下F5的话,我们这里会发现这里的箭头依然是在这个位置没有发生更改:
请添加图片描述
但是我们这里通过监视就可以看到我们这里的i却变成了1,那么这里就更可以说明了一件事就是我们这里的F5跳跃的是逻辑上的断点,并不是我们看到的断点,那么这里的逻辑上的断点就是这样的:请添加图片描述
我们得将这个循环拆开,往里面添加断点,这里的断点就是我们所说的逻辑上的断点,那么这里固然就存在了一个问题就是,要是我们在调试的时候知道这个循环中的前几次循环是对的,但是我要是设置断点的话还是得经历这些本就是对的循环经历,那么这里是不是就有点麻烦啊,所以在我们c语言当中我们是可以给这个断点添加条件的,右击断点就可以然后就会出现这样的窗口请添加图片描述
那么这里我们就可以给这个断点来设置条件,那么我们就设置当i=4的时候这个断点才生效,那么我们这里按一下F5来看看会变成什么样子:
请添加图片描述
我们这里直接就打印出来了0 1 2 3 ,并且我们这边的调试显示的也是i=4请添加图片描述
那么我们这里可以给我们的断点加上条件的话,那么我们在调试的时候就会方便很多。

2.F9

那么根据我们上面的介绍就可以知道我们这里的F9是用来设置断点的。

3.F10

那么相对于我们的F5我们的F10就没有那么的快我们的F10他是逐个过程的调试,一个过程可以是一次函数的调用也可以是一条语句

4.F11

逐语句,就是每次都执行一条语句,但是这个快捷键能够使得我们的执行过程进入函数的内部。比如说我们下面的代码:

#include<stdio.h>
int ADD(int x, int y)
{
	int z = x + y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	int c = ADD(a, b);
	return 0;
}

那么我们这里可以对他进行调试首先我们用F10来进行调试:
请添加图片描述
但是我们这里的调试的情况是直接跳过了我们这里的这个函数的调用,所以如果我们想要进入这个函数的内部的话,我们就得在此处按F11来进入我们函数的内部,那么我们这里可以看一下:
请添加图片描述
我们要是按F11的话我们这里就可以直接进入我们的函数内部。

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

那么这里我们就来讲讲如何来查看我们程序的信息,那么这里我们下面的操作都是得在你开始调试之后才能够看到,这里大家注意一下。

自动窗口

请添加图片描述
那么我们自动窗口的打开的路径就是在调试开始后点击上方的调试的按钮然后鼠标划到窗口,然后你就可以看到有自动窗口这个选项,然后点击进去我们就出现了这样的情况:请添加图片描述
那么我们这里的自动窗口就会自动根据程序调试的进度来判断你可能会观察的值,那么这里就出现了一种情况就是我们要是想观察其他的值呢?比如说我们这里在函数调用之前我们可以看到这里甚至连b的值都没有显示出来,然后我们再进入函数看看:请添加图片描述
我们发现这里的值就只剩我们这里函数内部的值了,之前的值我们都观察不到了,那么这里就是我们的一个缺点,编译器太自作主张了自动改变我们要观察变量,所以这个窗口我们用的并不多。

局部变量

那么我们这里还有一个选着就是局部变量,那么这个窗口的特点就是直接加载我们这个程序中的的全部的变量请添加图片描述
大家可以看到我们这里的程序虽然只运行到了开头,但是我们这里调试部分已经把我们这里当前代码块中的所有变量全部都显示了出来,然后我们这里可以再通过按F11进入这个函数中来看看这个我们这里的监视会发生什么样的变化:
请添加图片描述
我们可以发现我们这里刚进入函数的内部,我们这里的监视的内容就发生了变化,变成了当前函数内部的全部变量的值,所以通过上面的操作我们可以看到我们这里局部变量的作用就是加载当前的全部变量。

监视

那么我们这里的监视相对于上面的两个功能则更加的实用一些,因为我们这里的监视是你想要观察谁就可以观察谁,我们上面的那两个功能里面所观察的值都是编译器自己给你的,你无法做出修改但是我们这里的监视你就可以在你观察的值里面进行修改,比如说上面的a的值,我们确实可以通过上面的那两个功能来看它的值是多少,但是我要是想观察这个a的值加上这个b的值呢?我要是想观察一个表达式的结果呢?我要是想判断一个关系表达式的结果是真还是假呢?那么这些功能我们的监视都可以实现,比如说下面的代码:

#include<stdio.h>
int ADD(int x, int y)
{
	int z = x + y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	if (a < b)
	{
		int c = ADD(a, b);
	}
	return 0;
}

那么我们这里的监视就可以这样做请添加图片描述
我们这里就可以任意的对我们想要观察的值附加一些条件上去,这样我们观察的值也就会更加的丰富一点,然后我们这里在进入函数内部之后这些值也不会消失,这样就可以更加的方便我们的观察:
请添加图片描述

内存

那么我们程序在跑起来的时候肯定会在内存中存取一些有用值上去,那么我们这里就可以通过内存这个选项来观察我们这里内存中的值,那么我们这里观察的样子就是这样的:
请添加图片描述
那么我们这里内存中观察的值就是这样的,我们这里讲上面的列改成了4,这样的话我们这里每一行观察到的值就是一个字节,那么我们这里就在上面输入你想要观察的地址,然后我们这里就会自动的跳转到你输入的地址处,如果你不知道该变量的地址,你可以这么输入:请添加图片描述
然后再敲击回车这样的话他就会自动的找到这个变量a所对应的地址
请添加图片描述
那么我们内存中存储的实际上是二进制,但是我们这里编译器为了方便大家观察,就把这个二进制转化成十六进制,那么我们这里还可以看到我们这个数据的后面还有一些奇奇怪怪的符号,那么这个符号的就是编译器根据我们这里内存中的数据自行推导出来的一些可能表示的符号,所以这个用处不大大家不必关心。

调用堆栈

我们在写函数的时候总是会出现这样的情况就是就是一个函数的内部调用另外的一个函数,结果这个函数的内部又调用另外的一个函数,那么很多时候这个逻辑是非常的复杂的不利于我们的观察,那么这时就有了我们调用堆栈这个功能,那么这个功能就可以在调试开始的时候看到我们这里函数调用的逻辑:

#include<stdio.h>
void test3()
{
	;
}
void test2()
{
	test3();
}
void test1()
{
	test2;
}
int main()
{
	test1();
	return 0;
}

那么我们这里就可以看一下我们这里的函数堆栈中的逻辑:
请添加图片描述
那么我们这里就可以看到我们这里函数调用的逻辑,我们这里调用的顺序就是从下往上,那么我们这里就是先调用我们的main函数在调用我们的test1函数,再调用我们的test2函数再调用我们的test3函数,那么根据我们之前的学习我们就知道一件事情就是我们这里的main函数也是通过外部调用得到的,那么我们这里可以右击鼠标把显示外部代码这个选项勾上就可以显示我们这里完整的函数调用的过程:
请添加图片描述

寄存器

那么根据我们之前的学习我们知道在程序运行的时候会有这么一个东西就是寄存器,他会存放一些十分特殊的值,那么我们怎么来观察各个寄存器中的值呢?那么这里我们就可以在那窗口里面找到这些寄存器这个选项:
请添加图片描述
那么点开之后我们就会出现这样的一个窗口,那么这里就显示的是我们所有寄存器中存储的值,我们程序在跑的时候我们的寄存器就会存入不同的值,那么我们就可以通过这个窗口来观察我们的不同地方寄存器中的不同的值。当然如果你要是知道我们寄存器的名字的话也是可以通过监视来观察我们这里寄存器的值的。

反汇编

如果有时候你想要观察一个代码的底层逻辑的话我们这里就可以通过反汇编来讲这些语言改成汇编语言来观察他们的底层逻辑,那么如何来反汇编呢?那么这里我们就可以直接在代码处点击鼠标的右键,然后就会出现一堆的选项,然后我们就可以在其中找到我们其中的一个反汇编这个选项,点击这个选项你就会看到这样的景象:
请添加图片描述
那么这里就是我们的汇编代码。

实例一

我们可以来看一下下面的这个代码

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

那么我们这个代码的作用就是计算我们这里的1!+2!+3!…+n!,但是我们这里在运行的时候就会发现我们这里计算是有错误的,我们这里输入了一个3,但是我们这里运行输出的结构却是15,我们可以看一下:
请添加图片描述
那么这里是什么原因呢?我们这里发现了错误那么我们这里就可以尝试一下通过调试来看看这个问题出现的原因:请添加图片描述
那么我们这里首先输入了一个3,此时我们的各个数据都没有问题,那么我们就进入循环请添加图片描述
、进入循环之后我们就将我们的j初始化为了0,然后我们就进入了内部的循环,那么这个内部循环的作用就是计算我们的阶乘,那么我们这里的i一开始是1,所以我们这里的内部循环就只会进入一次,然后就跳出了循环,那么这个时候我们就会将我们的值加给我们的sum请添加图片描述
那么此时我们的sum的值就是1,然后我们第一次循环就结束了,我们的i就变成了2,小于我们这里的n,然后我们就再进入这里的内部循环,因为我们这里的i等于2,所以我们这里的内部循环就会循环两次,请添加图片描述
我们可以看到我们这里的ret是等于1的,并且我们的j也等于1 ,然后我们这里循环了两次我们的结果就成了这样:请添加图片描述
我们的ret的值等于2,那么这里也是对的因为我们的2的阶乘确实是等于2 的,那么我们再将这个值加给我们的sum,那么此时我们的sum的值就应该等于3请添加图片描述
那么我们这里可以看到我们这里的值确实是等于3的,然后我们的i就等于了3就会再循环一遍,那么这里就会再次进入我们内部的循环请添加图片描述
那么这里我们就来计算的3的阶乘,那么这里我们就将j的值初始化为1,开始计算我们3的阶乘,我们数学中的阶乘的计算是从1开始一直往后面乘的,但是我们仔细观察的话就会发现我们这里的ret是2啊,那么是2的话我们这里的阶乘就变成了从2,开始往后面乘了,那么这样的话我们这里就必然会出现错误啊,所以我们将这个内部循环算完之后就可以发现我们这里的ret的值就变成了12请添加图片描述
所以这就是我们这里问题存在的关键,我们在第三次循环的时候我们一开始ret的值就变成了2,并不是1所以这就是我们的问题所在,那么为了解决这个问题我们就可以直接在每次进入内部循环之前加上一段代码,将我们的ret的值初始化为1,就可以解决我们这里的问题:

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

实例二

#include<stdio.h>
int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i <= 12; i++)
	{
		arr[i] = 1![请添加图片描述](https://img-blog.csdnimg.cn/e4f91e3ee7ae4c8488605bf16ecc6434.png)
;
		printf("hehe\n");
	}
	return 0;
}

那么我们再来看看这个代码,我们这里创建了一个10个元素的数组,然后我们就进入了一个循环这个循环会循环13次,每次都将arr[ i ]的值赋值为0,并且打印一个hehe那么这里我们就可以将这个代码运行一下:
请添加图片描述
那么我们这里就出现了一个问题,我们这里出现了死循环,那么这是为什么呢?我们可以看看代码发现我们这里的arr数组在进行赋值的时候会出现越界访问的情况,那么我们这里的问题应该就出现在我们这里的调试部分,那么我们这里就可以通过调试的方法来看一下到底是为什么:
请添加图片描述
那么这里是我们一开始各个变量对应的值,我们知道循环10次是不会出现什么问题的,那么我们这里直接循环11请添加图片描述
并且也打印了11个hehe出来请添加图片描述
那么我们这里就再来往下走循环第12次
请添加图片描述
我们发现我们这里i的值变成了11,我们的arr[11]的值也变成了1,那么我们这里还是没有发现什么异常,那么我们再走一步我们这里就会将arr[12]的值变成1并且打印一个hehe,那么再走一步
请添加图片描述
我们发现我们上面确实打印出来了一个hehe但是我们这里i的值也发生了变化请添加图片描述
我们i的值确实变成了12,但是我们再执行完arr[12]=1之后,我们这里的i的值也发生了变化,请添加图片描述
我们这里i的值也变成了1,那也就是我们arr[12]对应的位置的值就是i ,那么这里的i变成1的话我们就又会进入这个循环,然后我们就又会对原来初始化过的位置的值再进行一次初始化,那么初始化到最后又会把i赋值成1,那么这里就是我们问题出现的关键,因为我们知道在栈区中内存的使用是从高地址往低地址使用的,而我们数组中随着数组下标的增长,我们对应的空间的地址的是在逐渐变高的,所以我们这里越界访问的话就会出现把我们i的值也改变的情况,所以这就是我们问题出现的关键所在。

编程常见类型的错误

编译型的错误(语法错误)

那么这个错误就非常的简单,就是你的语法出现了错误,比如说你把int a写成了itn a这就是一个语法的错误,还比如说你写的时候分号没有加啊,或者说写了一个中文的分号上去,那么这些都是错误所在,那么我们这样的错误就叫编译型的错误。这种错误编译器都会给你报出了告诉你在哪个位置出了错误。-

链接型的错误

那么这个错误出现的原因很可能就在于你使用了一个函数,但是这个函数你是没有实现的,所以就会报出这样的错误来,比如说下面的代码:

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = add(a, b);
	return 0;
}

我们这里使用这个add函数但是我们并没有实现这个函数,那么我们在运行这个代码的时候就会报出这样的错误请添加图片描述
这里的LNK就是链接型错误的标志,大家以后写代码的时候可以注意一下。

运行时的错误

那么上面两种错误都是比较简单的,但是我们这个错误就比较的隐蔽他不会报错,得我们自己一步一步的调试来找到错误的所在,那么这个就只能靠大家慢慢的积累了。
点击此处获取代码

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 18:45:50  更:2022-08-19 18:46:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/11 3:44:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码