如何在Visual Studio中调试代码

874 阅读18分钟

上一篇:C++头文件

下一篇:C++条件与分支

1. 概述

调试还是非常重要的,这也是编程的一部分,不仅仅是编程,也是学习的一部分。因为如果你知道如何调试你的代码,你会明白这个程序是如何工作的,计算机如何实际运行你的代码。所以接下来会讲讲调试。和前面的篇章一样,我们将继续使用Visual Studio来讲解调试,这些调试概念也几乎适用于其他的IDE。大多数IDE都会支持我这篇文章中将要展示的内容。但基本上我们会讲到两个重要的特征,我们可能会用断点,来将东西分成2个部分。因为断点是调试和在内存中查找数据的重要部分。断点和读取内存,这是调试的两大部分。当然,你会同时使用它们。换句话说,你要设置断点就是为了读取内存。那么调试的意义是什么呢?debug这个单词的意思是de bug,对吧,就是为了将代码中的错误清除。要想从我们的代码中删除一个bug,我们就必须要诊断出我们的代码错在哪儿了。这部分实际上是很棘手的,即使你在这门语言上很有经验。最终你要记住,电脑永远是对的,在99%的情况,不可能出现,你做了正确的事情,而电脑却不工作了。通常是你编写的代码出了错误,而不是计算机的问题。意识到这点很重要,对程序员来说很重要。你很快就会知道,计算机总是对的。所以调试这一切都是为了找出你的错误。我到底做了什么,才会犯错。好了,接下来来看看案例

2. 案例

1. 准备项目

准备一个简单的项目,项目中有三个文件,分别是Main.cpp、Log.cpp和Log.h,具体内容如下

image.png

image.png

image.png

2. 断点

接下来,我们首先要做的是,设置一个断点,然后逐步执行我们的程序。

那么什么是断点呢?

断点是程序中调试器将中断的点,这里中断的意思是暂停。我们可以在我们的程序中任何代码行上设置断点。当程序执行到这个设置了断点的代码行时,程序将暂停。在我们这个例子的整个项目中,他会挂起执行线程,以便让我们来看看这个程序的状态,这里的状态指的是内存。我们可以暂停我们的程序,看看在它的内存中发生了什么。记住,一个运行中的程序所需的内存是相当大的,包括你设置的每个变量,包括要调用的函数,包括所有。当你将程序中断后,内存数据实际上还在,这使得我们能够查看内存,以便诊断程序出现的问题。通过查看内存,你可以看到每一个变量的值,可以判断这个变量是不是应该设置为这个值,可以看到一些显然的错误。你还可以单步逐行运行你的代码,如,我们可以设置一个断点到第5行,然后点击一个按钮,程序将只前进一行到第6行。你也可以使用步入(step into)到函数内,看看函数会运行到哪里。你可以用断点做很多事情,这很很神奇,而且非常常用。如果你在编程时不用断点调试,那我就不知道你在干什么了。

回到VS

我们可以通过键盘F9在你光标停留行设置断点断点,在按一次,删除断点

image.png

或者你可以点击这个侧边栏上的任意地方,点击一次就可以打上断点,再次点击就可以移除断点。

image.png

image.png

显然,如果你在第三行打上断点断点

image.png

因为第三行什么也没有,所以这个断点不起作用。因此,请确保你是在将会被执行的代码行上打上的断点。比如在第6行就可以打上断点,因为它是我们程序执行的第一行代码。

image.png

接下来要做的就是运行代码,通过F5或是点击工具栏的本地Windows调试器本地Windows调试器

这里有一点要注意,调试断点,要确保你现在处于debug模式

image.png

因为如果你处于release模式,编译器实际上会改变你的代码,断点可能永远不会被击中,因为你的程序被重新安排了。以后会更深入的讨论什么是release模式。现在最重要的是,确保运行程序时是出于debug模式。

3. 本地Windows调试器

如果我们点击点击工具栏的本地Windows调试器本地Windows调试器按钮或按下F5,这确保我们在运行时附加了调试器。

image.png

你可以看到,我们VS的界面变成了如下的布局。

image.png

image.png

4. 程序暂停位置指示图标

在断点上带有一个大的黄色箭头圆圈黄色箭头,指示了在我们的程序中,当前指令所在的位置。

image.png

5. 调试相关功能按钮

我们来看下工具栏

可以看到之前的本地Windows调试器本地Windows调试器变成了继续继续,点击它会继续正常执行我们的程序,直到遇到下一个断点。

image.png

然后后面还有一堆按钮。

image.png

F11或点击这个按钮步入逐语句让我们进入(step into)函数。

image.png

F10或点击这个按钮 跨过逐过程跨过当前行。

image.png

Shift F11或点击这个按钮 跳出跳出跳出函数。

image.png

这三个按钮会控制什么?

步入step into(逐语句)的意思是进入到当前指示行代码的函数里面,如果这行有函数,案例中这行有函数也就是Log函数。

image.png

我们如果点击步入或者F11,我们将步入进log函数,然后就可以看到log函数到底做了什么。

跨过setp over就是从当前函数指示行跳到下一行代码。

image.png

跳出step out的意思是跳出当前函数,回到调用这个函数的位置,在这个例子中,因为是回到调用main函数的位置,也就是C++标准库函数的位置。

让我们按下F11步入log函数中,也可以点击工具的按钮步入

image.png

可以看到,在stack栈帧的最开始,我们没有开始执行任何代码。这里,我们只是设置函数栈帧结构

我们可以将鼠标悬停在参数message变量上

image.png

可以看到,这个message被设置为了"Hello World",这就是调试的第二部分。对,我们现在在读内存

如果我们继续按下F10或点击工具栏跨过,它跳到std::cout << message << std::endl;如下

image.png

实际上,黄色箭头在这里黄色箭头,意味着这一行的代码实际上还没有执行。是的,箭头在这便是将要执行这句代码了

当我按下按钮F10跨过或者Shift F11跳出,亦或者按下F5或点击继续继续执行我们的程序。我们只要按下其中一个,就会执行std::cout << message << std::endl;这行代码,甚至更多代码会被继续执行。

黄色箭头黄色箭头表示它在这一行代码这里,但是还没有真正执行这行代码。

如果我现在打开我们的程序

image.png

从控制台来看,并没有打印输出Hello World,所以这行代码还没有执行std::cout << message << std::endl;

但是我们按下F10或点击跨过,然后回来检查。

image.png

可以看到,我们的控制台打印了Hello World,因为我们已经执行std::cout << message << std::endl;这行代码。

通过设置断点,在我们的程序中,我们可以逐行运行整个程序,这是很有用的,当你想弄清楚你做错了什么时。

回到代码,如果我们继续F10或点击跨过

image.png

你会看到,最终会回到我们的main函数,因为Log函数执行完了。

继续按F10或点击跨过

image.png

这将再次带我们进入到main函数内的下一行代码。如果我继续按这些代码,将继续进行下去。

如果我按F5,将继续正常运行我们的程序,按下F5或点击继续

image.png

在控制台按下回车键关闭我们的程序。

以上,基本上就是调试的全部,我的意思是已经展示了几乎所有的东西。下面会给出更多的例子。

我们再在main函数中做一些变量,定义一个整数变量a并赋值8,然后给变量一个自增a++,它会增加1,然后创建一个const char* 指针string为"Hello",后面在写一个简单的for循环,遍历这个字符串并在每一行中打印没有字符。如下

image.png

这次没有打任何断点,我们直接F5运行程序

image.png

可以看到我们得到的东西。

现在,我们单行执行看看。首先我们在int a = 8;这行按下F9设置断点断点

image.png

按下F5或点击本地Windows调试器

image.png

现在,我们来将鼠标悬停在变量a上面。

image.png

看到a的值是负的8亿多,为什么是负的8亿多。还记得吗,那个黄色箭头圆圈黄色箭头指示的那行并不是已经执行了,而是将要执行。我们现在还没有执行到这第6行int a = 8;。这行代码是创建并设置了a变量的值。

调试器当前显示的是

image.png

a将要被设置的内存位置的数字被显示出来了,因为我们没有把这个变量设置成任何东西。它只是未初始化的内存,这意味着这个值只是给我们展示了内存中实际包含的内容。这将是一个极好的时间。

6. 调试辅助窗口介绍

可以看到最底部的窗口,这里有几个比较重要的窗口:自动窗口,局部变量和监视。

image.png

自动窗口局部变量向咱们展示可能对我们很重要的局部变量或变量。

image.png

image.png

监视让我们可以观察变量。

image.png

比如,我们在监视名称下输入函数中的变量a,然后回车。

image.png

你可以看到显示的值。如果我们还想看字符串,我们也可以将main函数中的string变量输入进去,如下

image.png

然后可以告诉我们字符串是什么。当然,这里还没有初始化内存,因此目前是完全无用的。但是随着我们一步步向下执行我们的程序,这些值将更新显示内存中的值。

1. 查看内存视图

说到内存,有一个内存视图,可以查看我们程序的内存。

我们到菜单栏中找到调试。调试->窗口->内存->内存1(1)。

image.png

点击它

image.png

我们将会看到这个奇怪的面板在这里。这个就是内存视图,将展示我们程序的所有内存,所以在最左边我们看到内存地址。

image.png

在中间,我们看到实际的数据,以十六进制格式表示的实际值。

image.png

我们看到ASCII码对这些数字的解释在最右侧

image.png

1. 获取变量内存地址

如果你想在内存视图中找到main函数中a变量存储在程序的内存中的位置,那么你需要知道a变量的内存地址。要做到这一点,我们只需要在a变量前面加上&,就像这样&a

我们可以在内存视图的地址栏那里输入&a

image.png

然后按下回车键,会得到变量a的内存地址0x008FFA80(每次运行都不一样),内存视图便会定位到变量a的内存地址。

image.png

在这个例子中,a变量在内存中的内容是一大堆c

2. 计算十六进制数据

这个cc数字实际上是十六进制数。如果你想知道它是多少,你可以使用计算器。

按下win键win键,输入计算器回车。

image.png

image.png

切换为程序员视图。

image.png

image.png

我们点击HEX那行的十六进制

image.png

image.png

输入CC

image.png

可以看到十进制是204。

为什么变量a的内存内容是一堆cc,为什么是这些数字。这些内存不应该是随机的吗。一堆cc,看上去就是很明确的。

这就是调试模式debug,调试模式dubug会减慢我们的程序。这是编译器会让我们的程序做某些事情,一些额外的东西会让我们的调试更加轻松。

例如,这个a变量的内存是一堆cc,意味着它是未初始化的栈内存。这实际发生的是,编译器知道我们准备做一个变量,但我们还没有初始化它,所以编译器要做的就是用cc把它填满。这样,如果我们在调试代码,一旦出了问题,我们就可以去看看内存,若看到这个变量被设置为cc,我们就可以知道,我们还没有初始化过这个变量,这样就可能会知道为什么这样做会出错等等判断。像这样的额外的东西,比如在我们初始化内存之前设置它为cc,显然,我们的程序正在做一些额外的事情,这会减慢速度,我们不想在release模式中做这样的事情。当我们release我们的程序,发布我们的应用时,我们不需要这些额外的东西。但是在调试时,这是非常有用的。

2. 监视窗口(watch窗口)

下面再来看看监视窗口(watch窗口)

image.png

可以选择变量a那行,鼠标右键点击十六进制显示(H)

image.png

image.png

现在你可以看到a的十六进制值是0xcccccccc,是一大堆cc

这当然意味着变量a当前正在栈内存初始化

让我们将变量a在监视窗口中回到非十六进制,也就是十进制,取消十六进制显示(H)选中。

image.png

image.png

7. 继续调试

回到代码,现在黄色箭头圆圈黄色箭头指向int a = 8;这行,表示这行还没有执行,将要执行

我们按下F10或点击跨过,这会发生很多事情。

image.png

首先黄色箭头圆圈黄色箭头指向了a++;这行,代表这行代码还未执行,将要执行。

image.png

再来看看监视窗口,我们变量a的值变成了8

image.png

image.png

8的值显示为红色,表示它自上一个断点后,值发生了变化。

我们再看看内存视图。

image.png

可以看出有4个字节的内存已经设置为了8。顺便说下,这里2个数字代表一个字节,这也是为什么我们用十六进制来看的原因。因为如果我们那样做,每两个十六进制数与一个byte字节对齐,这样就能分辨。

这8个是十六进制数字对应4字节的内存 image.png。可以看到变量被设置为了8。

image.png

这就是我们现在所做的,我们暂停了程序,然后看程序的状态state,我们正在读取它的内存信息。

接下来,我们再次按下F10或点击跨过

image.png

会执行a++;,变量a的值加1,监视窗口这里的值也被设置为了9。

image.png

你可以看到字符串string仍然在未初始化的栈内存当中。因为const char* string = "Hello";这行还未执行,将要执行。

我们继续按下F10或点击跨过

image.png

从监视窗口看,字符串string被初始化了。

image.png

因为它是一个实际的指针。监视窗口的值也告诉了我们这个字符串的内存地址。所以我们双击字符串的值然后复制它

image.png

然后在内存视图的地址中粘贴上

image.png

按下回车

image.png

看下这个,这些字节是ASCII码,翻译过来就是Hello。

这里真正有趣的是,如果你继续阅读。

image.png

可以看到,Hello后面相邻的内存说的是Stack around the variable'.' was corrupted,变量'.'在未初始化的情况下使用,显然,我们的程序在内存中包含了项Stack around the variable这样的字符串。这种情况下这release模式下是不存在的,这是另一个很多额外的东西在调试模式下发生的例子,编译器做这些额外的操作以便帮助咱们调试程序。

接下来,我们遇到了for循环。如果我们继续下一步会发生什么。我们还没有讲到循环或任何类型的控制流语句,后面的文章中会继续讲到。这里想先介绍调试器,然后我们就可以在后面单步执行这些控制流语句,看看他们是如何工作的。

现在,你已经知道如何使用这些debug视图。这个for循环其实就是我们需要做debug这类的事情很多次,接下来我们按下F10或点击跨过

image.png

image.png

可以看到变量i被设置为0,然后这个string[0]会取出string字符串中索引0对应的字符,是字符串的第一个字符H

我们继续按下F10或点击跨过

image.png

鼠标悬停在变量c上

image.png

看到c已经被赋值为H

我们也可以到监视窗口,添加监视变量c

image.png

回车

image.png

可以看到变量c的值。

到现在,控制都没有输出内容

image.png

接下来,我们继续按下F10或点击跨过

image.png

std::cout << c << std::endl;这行会执行,将变量c的内容输出到控制台。

image.png

接下来,我们继续按下F10或点击跨过

image.png

可以看到又回到了for (int i = 0; i < 5; i++)这行。

我们继续按下F10或点击跨过

image.png

可以看到变量i经过i++被赋值为了1。然后重复做一遍一样的事情。

到内存视图中。我们在地址栏输入&c,回车

image.png

可以看到变量c在内存中十六进制值。

现在,我们要这个for循环执行完,而不是F10或点击跨过,看它一步步执行for循环,而是想要到Log("Hello World");,应该怎么做。如果我们按Shift F11或点击跳出,这将跳出整个函数,这不是我们想要的。

可以在你想让程序停下的地方加一个断点

image.png

然后,我们按下F5或点击继续按钮,它会继续运行直到遇到下一个断点,在这个例子中就是这个log函数Log("Hello World");

image.png

现在在内存视图中可以看到用来存放c变量的内存已经变成了字母o

image.png

会发现变量c所在的这部分内存依然活跃,虽然我们已经退出了循环。变量c内存的最后的字符是字符串Hello的最后一个字符o

我们来看下控制台

image.png

可以看到已经完整的打印了Hello单词。

没有打印Hello World,因为,黄色箭头圆圈黄色箭头指向的这行Log("Hello World");还没有执行。

我们继续按下F10或点击跨过

image.png

来看下控制台。

image.png

看到输出打印了Hello World

现在我们暂停了程序的继续执行,因为黄色箭头圆圈黄色箭头代表这一行std::cin.get();将要执行但还没有执行。

所以我们在控制台按下回车键,是什么也不会发生。

image.png

然后,我们回到代码,按下F5或点击继续,我们的程序就会关闭,因为它仍然会检查到,enter回车键已经被按下了。

image.png

这就是一个非常简单的,基本的调试过程。这里面还有很多东西,但这里只作为实际调试代码的基础。

记住,一个程序就是由内存组成的,甚至是指令指针。在我们的程序中,我们实际上是在执行代码,我们实际执行的代码,所有的这些都存储在内存中。所以,能看到我们的内存是最重要的。通过设置断点,我们可以暂停程序,在给定的时间在给定的代码行,检查一下,看看,我们所有的变量信息。这对你运行的代码会非常有用。

上一篇:C++头文件

下一篇:C++条件与分支