1. 概述
调试还是非常重要的,这也是编程的一部分,不仅仅是编程,也是学习的一部分。因为如果你知道如何调试你的代码,你会明白这个程序是如何工作的,计算机如何实际运行你的代码。所以接下来会讲讲调试。和前面的篇章一样,我们将继续使用Visual Studio来讲解调试,这些调试概念也几乎适用于其他的IDE。大多数IDE都会支持我这篇文章中将要展示的内容。但基本上我们会讲到两个重要的特征,我们可能会用断点,来将东西分成2个部分。因为断点是调试和在内存中查找数据的重要部分。断点和读取内存,这是调试的两大部分。当然,你会同时使用它们。换句话说,你要设置断点就是为了读取内存。那么调试的意义是什么呢?debug这个单词的意思是de bug,对吧,就是为了将代码中的错误清除。要想从我们的代码中删除一个bug,我们就必须要诊断出我们的代码错在哪儿了。这部分实际上是很棘手的,即使你在这门语言上很有经验。最终你要记住,电脑永远是对的,在99%的情况,不可能出现,你做了正确的事情,而电脑却不工作了。通常是你编写的代码出了错误,而不是计算机的问题。意识到这点很重要,对程序员来说很重要。你很快就会知道,计算机总是对的。所以调试这一切都是为了找出你的错误。我到底做了什么,才会犯错。好了,接下来来看看案例
2. 案例
1. 准备项目
准备一个简单的项目,项目中有三个文件,分别是Main.cpp、Log.cpp和Log.h,具体内容如下
2. 断点
接下来,我们首先要做的是,设置一个断点,然后逐步执行我们的程序。
那么什么是断点呢?
断点是程序中调试器将中断的点,这里中断的意思是暂停。我们可以在我们的程序中任何代码行上设置断点。当程序执行到这个设置了断点的代码行时,程序将暂停。在我们这个例子的整个项目中,他会挂起执行线程,以便让我们来看看这个程序的状态,这里的状态指的是内存。我们可以暂停我们的程序,看看在它的内存中发生了什么。记住,一个运行中的程序所需的内存是相当大的,包括你设置的每个变量,包括要调用的函数,包括所有。当你将程序中断后,内存数据实际上还在,这使得我们能够查看内存,以便诊断程序出现的问题。通过查看内存,你可以看到每一个变量的值,可以判断这个变量是不是应该设置为这个值,可以看到一些显然的错误。你还可以单步逐行运行你的代码,如,我们可以设置一个断点到第5行,然后点击一个按钮,程序将只前进一行到第6行。你也可以使用步入(step into)到函数内,看看函数会运行到哪里。你可以用断点做很多事情,这很很神奇,而且非常常用。如果你在编程时不用断点调试,那我就不知道你在干什么了。
回到VS
我们可以通过键盘F9
在你光标停留行设置断点,在按一次,删除断点
或者你可以点击这个侧边栏上的任意地方,点击一次就可以打上断点,再次点击就可以移除断点。
显然,如果你在第三行打上断点
因为第三行什么也没有,所以这个断点不起作用。因此,请确保你是在将会被执行的代码行上打上的断点。比如在第6行就可以打上断点,因为它是我们程序执行的第一行代码。
接下来要做的就是运行代码,通过F5
或是点击工具栏的本地Windows调试器
。
这里有一点要注意,调试断点,要确保你现在处于debug模式
因为如果你处于release模式,编译器实际上会改变你的代码,断点可能永远不会被击中,因为你的程序被重新安排了。以后会更深入的讨论什么是release模式。现在最重要的是,确保运行程序时是出于debug模式。
3. 本地Windows调试器
如果我们点击点击工具栏的本地Windows调试器
按钮或按下F5,这确保我们在运行时附加了调试器。
你可以看到,我们VS的界面变成了如下的布局。
4. 程序暂停位置指示图标
在断点上带有一个大的黄色箭头,指示了在我们的程序中,当前指令所在的位置。
5. 调试相关功能按钮
我们来看下工具栏
可以看到之前的本地Windows调试器
变成了
继续
,点击它会继续正常执行我们的程序,直到遇到下一个断点。
然后后面还有一堆按钮。
F11
或点击这个按钮逐语句
让我们进入(step into)函数。
F10
或点击这个按钮
逐过程
跨过当前行。
Shift F11
或点击这个按钮
跳出
跳出函数。
这三个按钮会控制什么?
step into(逐语句)的意思是进入到当前指示行代码的函数里面,如果这行有函数,案例中这行有函数也就是Log函数。
我们如果点击或者
F11
,我们将步入进log函数,然后就可以看到log函数到底做了什么。
setp over就是从当前函数指示行跳到下一行代码。
step out的意思是跳出当前函数,回到调用这个函数的位置,在这个例子中,因为是回到调用main函数的位置,也就是C++标准库函数的位置。
让我们按下F11
步入log函数中,也可以点击工具的按钮
可以看到,在stack栈帧的最开始,我们没有开始执行任何代码。这里,我们只是设置函数栈帧结构
。
我们可以将鼠标悬停在参数message变量上
可以看到,这个message被设置为了"Hello World",这就是调试的第二部分。对,我们现在在读内存
。
如果我们继续按下F10
或点击工具栏,它跳到
std::cout << message << std::endl;
如下
实际上,黄色箭头在这里,意味着
这一行的代码实际上还没有执行。是的,箭头在这便是将要执行这句代码了
。
当我按下按钮F10
跨过或者Shift F11
跳出,亦或者按下F5或点击继续执行我们的程序。我们只要按下其中一个,就会执行
std::cout << message << std::endl;
这行代码,甚至更多代码会被继续执行。
黄色箭头表示它在这一行代码这里,但是还没有真正执行这行代码。
如果我现在打开我们的程序
从控制台来看,并没有打印输出Hello World
,所以这行代码还没有执行std::cout << message << std::endl;
。
但是我们按下F10或点击,然后回来检查。
可以看到,我们的控制台打印了Hello World
,因为我们已经执行std::cout << message << std::endl;
这行代码。
通过设置断点,在我们的程序中,我们可以逐行运行整个程序,这是很有用的,当你想弄清楚你做错了什么时。
回到代码,如果我们继续F10或点击
你会看到,最终会回到我们的main函数,因为Log函数执行完了。
继续按F10或点击
这将再次带我们进入到main函数内的下一行代码。如果我继续按这些代码,将继续进行下去。
如果我按F5,将继续正常运行我们的程序,按下F5或点击
在控制台按下回车键关闭我们的程序。
以上,基本上就是调试的全部,我的意思是已经展示了几乎所有的东西。下面会给出更多的例子。
我们再在main函数中做一些变量,定义一个整数变量a并赋值8,然后给变量一个自增a++,它会增加1,然后创建一个const char* 指针string为"Hello",后面在写一个简单的for循环,遍历这个字符串并在每一行中打印没有字符。如下
这次没有打任何断点,我们直接F5运行程序
可以看到我们得到的东西。
现在,我们单行执行看看。首先我们在int a = 8;
这行按下F9设置断点。
按下F5或点击
现在,我们来将鼠标悬停在变量a
上面。
看到a的值是负的8亿多,为什么是负的8亿多。还记得吗,那个黄色箭头指示的那行并不是已经执行了,而是将要执行。我们现在还没有执行到这第6行
int a = 8;
。这行代码是创建并设置了a变量的值。
调试器当前显示的是
a将要被设置的内存位置的数字被显示出来了,因为我们没有把这个变量设置成任何东西。它只是未初始化的内存,这意味着这个值只是给我们展示了内存中实际包含的内容。这将是一个极好的时间。
6. 调试辅助窗口介绍
可以看到最底部的窗口,这里有几个比较重要的窗口:自动窗口,局部变量和监视。
自动窗口
和局部变量
向咱们展示可能对我们很重要的局部变量或变量。
监视
让我们可以观察变量。
比如,我们在监视
名称下输入函数中的变量a,然后回车。
你可以看到显示的值。如果我们还想看字符串,我们也可以将main函数中的string变量输入进去,如下
然后可以告诉我们字符串是什么。当然,这里还没有初始化内存,因此目前是完全无用的。但是随着我们一步步向下执行我们的程序,这些值将更新显示内存中的值。
1. 查看内存视图
说到内存,有一个内存视图,可以查看我们程序的内存。
我们到菜单栏中找到调试。调试->窗口->内存->内存1(1)。
点击它
我们将会看到这个奇怪的面板在这里。这个就是内存视图,将展示我们程序的所有内存,所以在最左边我们看到内存地址。
在中间,我们看到实际的数据,以十六进制格式表示的实际值。
我们看到ASCII码对这些数字的解释在最右侧
1. 获取变量内存地址
如果你想在内存视图中找到main函数中a变量存储在程序的内存中的位置,那么你需要知道a变量的内存地址。要做到这一点,我们只需要在a变量前面加上&
,就像这样&a
。
我们可以在内存视图的地址栏那里输入&a
然后按下回车键,会得到变量a的内存地址0x008FFA80
(每次运行都不一样),内存视图便会定位到变量a的内存地址。
在这个例子中,a变量在内存中的内容是一大堆c
2. 计算十六进制数据
这个cc数字实际上是十六进制数。如果你想知道它是多少,你可以使用计算器。
按下win键,输入
计算器
回车。
切换为程序员视图。
我们点击HEX那行的十六进制
输入CC
可以看到十进制是204。
为什么变量a的内存内容是一堆cc,为什么是这些数字。这些内存不应该是随机的吗。一堆cc,看上去就是很明确的。
这就是调试模式debug,调试模式dubug会减慢我们的程序。这是编译器会让我们的程序做某些事情,一些额外的东西会让我们的调试更加轻松。
例如,这个a变量的内存是一堆cc,意味着它是未初始化的栈内存。这实际发生的是,编译器知道我们准备做一个变量,但我们还没有初始化它,所以编译器要做的就是用cc把它填满。这样,如果我们在调试代码,一旦出了问题,我们就可以去看看内存,若看到这个变量被设置为cc,我们就可以知道,我们还没有初始化过这个变量,这样就可能会知道为什么这样做会出错等等判断。像这样的额外的东西,比如在我们初始化内存之前设置它为cc,显然,我们的程序正在做一些额外的事情,这会减慢速度,我们不想在release模式中做这样的事情。当我们release我们的程序,发布我们的应用时,我们不需要这些额外的东西。但是在调试时,这是非常有用的。
2. 监视窗口(watch窗口)
下面再来看看监视窗口(watch窗口)
可以选择变量a那行,鼠标右键点击十六进制显示(H)
现在你可以看到a的十六进制值是0xcccccccc
,是一大堆cc
这当然意味着变量a当前正在栈内存初始化
。
让我们将变量a在监视窗口中回到非十六进制,也就是十进制,取消十六进制显示(H)
选中。
7. 继续调试
回到代码,现在黄色箭头指向
int a = 8;
这行,表示这行还没有执行,将要执行
。
我们按下F10或点击,这会发生很多事情。
首先黄色箭头指向了
a++;
这行,代表这行代码还未执行,将要执行。
再来看看监视窗口,我们变量a的值变成了8
8的值显示为红色,表示它自上一个断点后,值发生了变化。
我们再看看内存视图。
可以看出有4个字节的内存已经设置为了8。顺便说下,这里2个数字代表一个字节,这也是为什么我们用十六进制来看的原因。因为如果我们那样做,每两个十六进制数与一个byte字节对齐,这样就能分辨。
这8个是十六进制数字对应4字节的内存
。可以看到变量被设置为了8。
这就是我们现在所做的,我们暂停了程序,然后看程序的状态state,我们正在读取它的内存信息。
接下来,我们再次按下F10或点击
会执行a++;
,变量a的值加1,监视窗口这里的值也被设置为了9。
你可以看到字符串string仍然在未初始化的栈内存当中。因为const char* string = "Hello";
这行还未执行,将要执行。
我们继续按下F10或点击
从监视窗口看,字符串string被初始化了。
因为它是一个实际的指针。监视窗口的值也告诉了我们这个字符串的内存地址。所以我们双击字符串的值然后复制它
然后在内存视图的地址中粘贴上
按下回车
看下这个,这些字节是ASCII码,翻译过来就是Hello。
这里真正有趣的是,如果你继续阅读。
可以看到,Hello
后面相邻的内存说的是Stack around the variable'.' was corrupted
,变量'.'
在未初始化的情况下使用,显然,我们的程序在内存中包含了项Stack around the variable
这样的字符串。这种情况下这release模式下是不存在的,这是另一个很多额外的东西在调试模式下发生的例子,编译器做这些额外的操作以便帮助咱们调试程序。
接下来,我们遇到了for循环。如果我们继续下一步会发生什么。我们还没有讲到循环或任何类型的控制流语句,后面的文章中会继续讲到。这里想先介绍调试器,然后我们就可以在后面单步执行这些控制流语句,看看他们是如何工作的。
现在,你已经知道如何使用这些debug视图。这个for循环其实就是我们需要做debug这类的事情很多次,接下来我们按下F10或点击
可以看到变量i被设置为0,然后这个string[0]
会取出string字符串中索引0对应的字符,是字符串的第一个字符H
我们继续按下F10或点击
鼠标悬停在变量c上
看到c已经被赋值为H
。
我们也可以到监视窗口,添加监视变量c
回车
可以看到变量c的值。
到现在,控制都没有输出内容
接下来,我们继续按下F10或点击
std::cout << c << std::endl;
这行会执行,将变量c的内容输出到控制台。
接下来,我们继续按下F10或点击
可以看到又回到了for (int i = 0; i < 5; i++)
这行。
我们继续按下F10或点击
可以看到变量i经过i++
被赋值为了1。然后重复做一遍一样的事情。
到内存视图中。我们在地址栏输入&c
,回车
可以看到变量c在内存中十六进制值。
现在,我们要这个for循环执行完,而不是F10或点击,看它一步步执行for循环,而是想要到
Log("Hello World");
,应该怎么做。如果我们按Shift F11
或点击,这将跳出整个函数,这不是我们想要的。
可以在你想让程序停下的地方加一个断点
然后,我们按下F5或点击按钮,它会继续运行直到遇到下一个断点,在这个例子中就是这个log函数
Log("Hello World");
。
现在在内存视图中可以看到用来存放c变量的内存已经变成了字母o
会发现变量c所在的这部分内存依然活跃,虽然我们已经退出了循环。变量c内存的最后的字符是字符串Hello
的最后一个字符o
我们来看下控制台
可以看到已经完整的打印了Hello
单词。
没有打印Hello World
,因为,黄色箭头指向的这行
Log("Hello World");
还没有执行。
我们继续按下F10或点击
来看下控制台。
看到输出打印了Hello World
。
现在我们暂停了程序的继续执行,因为黄色箭头代表这一行
std::cin.get();
将要执行但还没有执行。
所以我们在控制台按下回车键,是什么也不会发生。
然后,我们回到代码,按下F5或点击,我们的程序就会关闭,因为它仍然会检查到,enter回车键已经被按下了。
这就是一个非常简单的,基本的调试过程。这里面还有很多东西,但这里只作为实际调试代码的基础。
记住,一个程序就是由内存组成的,甚至是指令指针。在我们的程序中,我们实际上是在执行代码,我们实际执行的代码,所有的这些都存储在内存中。所以,能看到我们的内存是最重要的。通过设置断点,我们可以暂停程序,在给定的时间在给定的代码行,检查一下,看看,我们所有的变量信息。这对你运行的代码会非常有用。