How C++ Works

How C++ Works

C++ 工作原理


介绍

首先编写以下代码

#include <iostream>

int main()
{
	std::cout<<"Hello World"<<std::endl;
	std::cin.get();
}

预处理语句

首先我们现在有这个#include iostream语句。这就是所谓的预处理器语句,任何以#开头的东西都是预处理器语句。编译器在收到源文件时要做的第一件事就是预处理所有预处理器语句,这就是为什么他们调用预处理器语句,因为它们发生在实际编译之前。当前例子中的预处理语句就是叫#include的东西。#include会做的就是找一个文件当前例子中,我们正在寻找一个名为iostream的文件,并将该文件的所有内容粘贴到此当前文件中。#include通常称为头文件,随着系列的进行,我们将更深入地讨论它们。

#include <iostream>

我们之所以包含名为iostream的文件,是因为我们需要一个名为cout的函数的声明,该函数可以让我们向控制台输出东西。

std::cout

入口点

接下来我们看这个 main 函数。main函数非常重要,因为每个C++程序都有这样的函数。main函数称为入口点。它是我们应用程序的入口点,这意味着当我们运行应用程序时,我们的计算机开始执行在此函数中的代码,因为程序正在运行,我们的计算机将按顺序执行我们输入的代码。当然,有些事情可能会破坏或改变执行顺序,这些主要称为控制流语句或对其他函数的调用,但要点是我们的代码逐行执行。所以在我们的应用程序中执行的第一件事就是这个 hello world 语句。然后接下来的事情是结束函数,然后由于这是我们在 main 函数中拥有的所有内容,我们的程序现在将终止,你可能会注意到 main 的返回类型实际上是 int。但是我们不返回整数,那是因为 main 函数实际上是一个特例,您不必从 main 函数返回任何类型的值。如果你不返回任何东西,它将假设你的返回为零。这仅适用于main 的一个特殊情况。

int main()
{
	std::cout<<"Hello World"<<std::endl;
	std::cin.get();
}

特别案例

再看下main函数中的内容实际是在做什么。对于刚接触C++的人来说,这种<<可能看起来很奇怪,而且不幸的是,它实际上是这样写的,因为当你第一次看到它时它没有太大意义,但基本上这些左尖括号看起来有点像位移左运算符实际上只是一个重载的运算符。所以你需要把它们看作是一个函数。现在我知道他们看起来像一个操作符,但这里的东西运算符只是函数。所以在当前例子中,这实际上与print函数相同的。然后 hello world 是我们的参数,你可以将这些运算符视为函数,如果你这样想它们,那么它更有意义。所以我们在这里实际做的是我们将这个 hello world 字符串放入这个 cout,然后将hello world打印到控制台上,然后我们放入一个endl 结束行,这只是告诉我们的控制台前进到下一行。

std::cout<<"Hello World"<<std::endl;
std::cout.print("Hello World").print(std::endl);

//上述两句等价

我们cin.get 函数的场景基本上只会等到我们按 ENTER 键再前进到下一行代码。所以基本上我要说的是我们的程序执行将暂停在这一行,直到我们按 ENTER 键,因为这个函数只会等待我们按 ENTER 键 。这就是我们的整个程序

std::cin.get();

预处理

我们实际上有一个名为main.cpp的源文件我们如何从这个文本获取到一个实际的可执行二进制文件。我们经历了几个阶段。

#inlcude iostream

首先这个 #inlcude iostream。这个叫做预处理器。所以这个预处理器在我们编译文件之前会被评估。在这种情况下,它将iostream文件的所有内容都包含到此文件中,它只是将该文件复制到当前文件中。我们将在未来更深入地讨论如何进行文件,因此如果您不了解这一点,请不要太担心。所以现在你需要知道的只是我们#include这个文件,这样我们就可以使用 cout 和 cin 函数,一旦我们的预处理器语句被评估,我们的文件就会被编译。

编译

在编译阶段,我们的编译器将所有这些C++代码转换为实际的机器代码,有几个重要的设置可以确定这实际上是如何发生的,所以让我们在Visual Studio中简要看一下它们。
在这里插入图片描述
一个是解决方案配置,一个是解决方案平台,默认它可能会被设置为Debug,平台是x86或win32,如果我们下拉调试,你会看到我们有两个选项调试和发布。这两个选项是Visual Studio中任何新项目的默认值,然后在解决方案平台下,您将看到我们将x64和x86作为我们的两个选项,这些只是默认的配置只是一组适用于项目构建的规则,而解决方案平台是我们当前编译的目标平台。所以一个很好的例子将是x86是针对Windows 32位的,这意味着我们将为Windows生成一个32位的应用程序。对于更复杂的项目,您可能针对不同的平台,您可以在该下拉列表中将Android作为平台,然后如果您想在Android上构建,部署和调试,请将您的平台更改为Android。解决方案配置是一组定义该平台编译的规则,因此让我们看一下我们可以更改的一些规则。

属性

VS中右击项目选择属性
在这里插入图片描述
这些定义了用于在配置和平台中构建它们的规则,您需要注意的第一件事是此配置和平台区域。确保您的配置和平台设置为您出于某种原因实际想要修改的平台,有时它可能设置为发布,但您显然正在构建调试,这意味着这些更改都不会生效,实际为Debug
在这里插入图片描述
所以我们有我们的调试配置,你可以看到我们有Winx32,Winx32与x86完全相同.好吧,它们是相同的,由于某些原因,它们有不同的名称,但它们在这里是相同的。
在这里插入图片描述如果我们希望它成为一个库,我们的配置类型设置为应用程序,我们可以在这里更改它,但它基本上是二进制文件,有一个兼容的输出,因为我们要有一个可执行的二进制文件,我们会将其设置为应用程序.exe。
在这里插入图片描述
编译器设置位于 C/C++ 块下 我们这里有重要的设置,例如包含目录优化设置。我们可能希望使用代码生成设置、预处理器定义和很多我们甚至不会很快接触的东西。默认的 Visual Studio 配置很好,所以我们实际上不需要做任何事情,但这些是控制如何编译文件的规则。
在这里插入图片描述
你可以看到调试和已发布配置之间的差异非常好,如果您进入优化下的优化选项卡,如果我将其更改为Release,您将看到优化设置为最大化而,在Debug中,它设置为已禁用,这是一个很好的例子,说明为什么Debug模式比Release模式慢得多,因为优化被关了,当然关闭优化会有助于我们debug,之后我们就会发现了。

所有的.cpp文件都会被编译,而头文件(header file)不会,只有cpp文件。请记住,头文件通过称为 include 的处理器语句包含在 CPP 文件中,这就是它们被编译的时候,所以我们有一堆我们已经编译的 CPP 文件,它们实际上是单独编译的。每个 CPP 文件都会被编译成object 文件。使用Visual Studios编译器的扩展名是obj.一旦我们有了所有单独的obj文件,这些文件是编译CPP文件的结果,我们需要一些方法将它们拼接成一个exe文件,这就是链接器得作用。您可以在此链接器选项卡下看到链接器设置。链接器它需要所有这些 OBJ 文件并将它们粘合在一起
在这里插入图片描述
所以链接器的工作是获取我们所有的 OBJ 文件并将它们拼接成一个 exe 文件,当然它这样做的方式实际上是编译的。

编译实战

我要做的第一件事其实就是编译这个Main.CPP文件。在Visual Studio中,您可以通过按Ctrl + f7单独编译文件。
在这里插入图片描述
您可以在此处看到我们的输出显示我们实际上正在构建这个 main.cpp 文件并且它成功了,如果您不想按 ctrl F7
在这里插入图片描述
您实际上可以调出此编译按钮,您可以通过右键单击此处并单击构建然后出去或删除自定义按钮,然后在构建中添加一个名为 compile 的命令来实现。因此,如果我们点击该按钮,您可以看到我们正在编译文件.
在这里插入图片描述
例如,如果我们在这里犯一些语法错误,我忘记了一个分号,如果我编译该文件,你会看到我们得到一个错误。现在Visual Studio以许多不同的方式向我们展示了错误,其中一种是此错误列表,另一种是在这个输出窗口中。我现在要告诉你们,对于像这样的小事情,它似乎是可读的,你从来没有想过依赖它很多时候它实际上缺少信息错误列表的权重工作是它基本上通过我们的输出窗口寻找单词错误,然后从那里获取它可以找到的信息并将其放入此错误列表中。
在这里插入图片描述因此,这是一个很好的但是如果您想了解有关刚刚查看输出窗口的所有信息,则需要更多详细信息,到目前为止,本系列的其余部分我实际上将查看此输出窗口以获取错误消息以习惯这一点。你可以看到我们这里有一个箭头,它说完整的箭头缺少大括号前的分号,它会告诉你错误所在的行号,如果你双击这个实际的行,你将被带到源代码中的错误所在,所以让我们继续通过添加一个分号来解决这个问题,然后按 Ctrl+F7 或编译以构建这个文件
在这里插入图片描述
因此,当您单独编译文件时,我们已经编译了一个文件,没有发生链接,显然您只是在编译单个文件,因此链接器是没有调用的。让我们继续检查编译器实际生成的内容
在这里插入图片描述
在文件中打开项目文件
在这里插入图片描述
在这里插入图片描述
通过定义Visual Studio 将我们的构建文件输出到调试文件夹中,您可以看到如果我们进入那里,您可以看到一个main.obj文件,这是目标文件,这是我们编译器生成的对象文件,同样,对于项目中的每个简单文件,您都将有一个。
在这里插入图片描述
在这里插入图片描述
我们构建实际项目,所以我在这里做的不仅仅是构建一个文件。我实际上正在构建整个项目,您可以看到我们实际上得到了一个exe文件。
在这里插入图片描述
如果我们返回我们的文件资源管理器,它实际上将位于您的解决方案目录中得Debug目录下(这取决去你的Visual Studio得构建目录),可以看到HelloWorld.exe,双击可以执行。

多文件

所以这是一个非常简单的概述,但是当我们有多个 CPP 文件时会发生什么,让我们看一个简单的例子。所以假设我们已经把我们的 helloworld 打印到控制台,但我不想使用 cout 函数,我想使用我自己的日志记录函数,然后也许这会封装这个 cout 函数

void Log(const char* message)
{
	std::cout << message << std::end;
}

因此,让我们创建一个名为 log 的函数,它将接收一个名为 message 的字符串并打印该消息。如果您不确定什么是const char*,请不要担心,我们将在接下来中讨论字符串。目前你所知道的,一个 const char* 基本上只是一个可以容纳string的类型。所以现在我们可以重写我们的代码,以便我们调用这个日志函数,而不是调用 cout 然后打印 helloworld。

#include <iostream>

void Log(const char* message)
{
	std::cout << message << std::end;
}

int main()
{
	Log("Hello World");
	std::cin.get();
}

传入helloWorld作为参数,我们可以继续点击本地Windows调试器按钮,以确保它能正确运行。
太棒了,我们已经编写了我们的第一个函数,这很容易,所以现在让我们把这个函数和它的内容放到一个不同的文件中,因为我不想让这个主文件包含我的所有代码,我想将我的代码分成多个文件,以保持代码得干净和有条理。
在这里插入图片描述

//Log.cpp
void Log(const char* message)
{
	std::cout << message << std::endl;
}

我们将在源文件下创建一个新文件,例如右键单击添加新项,我们将创建一个 cpp 文件,我们命名log.cpp然后单击添加,所以我在这里要做的是我要回到main.cpp我要剪切这个日志函数并将其粘贴到Log.cpp中,所以现在我们的Log.cpp文件中有一个名为log的函数。让我们尝试编译这个文件,好的,检查一下,我们得到了一堆错误,如果我们查看输出窗口,您会发现 cout 不是 std 的成员,基本上是在告诉我们它不知道 cout 是什么原因是因为我们没有包含 cout 的声明。
在这里插入图片描述
C ++中的每种符号都需要某种声明,cout是在我们包含在main.cpp 文件中定义的 ,该文件当然是iostream,让我们继续把iostream并将其放在该文件的顶部,以便我们包含iostream,通过这样做,我们包含一个声明,告知这是我们的函数,让我们再次编译Log.cpp,结果就成功了。

//Log.cpp
#include <iostream>

void Log(const char* message)
{
	std::cout << message << std::endl;
}

以回到main.cpp我想调用这个日志函数,我可以按ctrl + f7,因为找不到日志,我们也收到了关于cin的错误,但我们已经知道那是因为我们删除了包含的iostream,先把iostream还原
在这里插入图片描述
我们已经将一个函数从一个文件移动到另一个文件中,并且我们正在单独编译每个文件,因此这个 main.cpp 文件不知道某处有一个名为 log 的函数,并且由于它无法识别 log 是什么,它给了我们编译错误,我们可以通过提供称为声明的东西来解决此问题,声明正是我们所说的一种叫做 log 的东西存在,现在这几乎就像一个承诺,因为我们可以告诉编译器有一个名为 log 的函数,但是编译器只会相信我们,这就是编译器的伟大之处,它会是的,我很酷,我完全信任,因为编译器不关心解决该日志函数实际定义的位置,我们这里有两个不同的词声明和定义, 声明只是一个语句,它说这个符号这个函数存在,然后定义是说这就是这个函数是什么这是这个函数的主体需要的东西。

所以让我们继续为我们的日志函数写一个声明,一个声明看起来与实际定义非常相似,log.cpp中得log叫做定义,因为你可以看到我们不仅用名称 log 函数和名称 log 声明了一些东西,我们还给了它一个主体,它实际上包含了我们调用这个函数时将运行的代码。

void Log(const char* message);

所以回到main让我们写一个声明,声明看起来与定义非常相似,但是它没有的是实际的主体,所以你可以看到我可以在这个末尾放一个分号,这就是结尾,事实上你甚至不必指定参数的名称,因为它无关紧要。但是还是把参数写上,这样更有意义。我们重新编译main.cpp,编译成功

所以你可能想知道在这个阶段,如果我们只是编译了一个文件,编译器会知道我们实际上在另一个文件中有一个日志函数,答案是它不仅仅是它信任我们,那么你的第二个问题应该是它如何实际运行正确的代码,这就是链接器在我们构建整个项目时出现的地方,而不仅仅是这个文件,但如果我实际上右键单击并在编译文件后点击构建,链接器实际上会找到该日志功能的定义并将其连接到我们在main.cpp中调用的日志函数,如果找不到该定义,那就是我们收到链接器错误的时候,现在链接错误或其他东西看起来很可怕,很多人会害怕,以现在如果我只是运行我的程序你会看到它仍然打印出文本 Hello World 并且一切都运行成功。

//Log.cpp
#include <iostream>

void Logr(const char* message)
{
	std::cout << message << std::endl;
}

在这里插入图片描述
但是,让我们删除它或至少对其进行一些更改,例如,我会将其更改为 Logr,保存文件回到主文件.cpp 我将尝试自行编译此文件,您可以看到我们没有任何问题。
在这里插入图片描述
生成项目,出现了无法解析得外部符号错误。未解析的外部符号意味着链接器无法解析符号,记住链接器的工作是解析符号,它必须连接函数并且找不到将Log连接到什么,因为我们没有实际定义的具有主体的称为Log的功能。

修复错误

因此,我们可以解决此问题的方法是,我们需要为此日志函数提供一个定义将方法名Logr 换成 Log,重新生成,成功。
在这里插入图片描述
如果回到文件管理器,看看我们在中间文件夹(编译器生成obj等文件)中的内容,你会看到我们有两个 obj 文件,因为编译器为每个 cpp 文件生成一个对象文件.然后链接器会将它们一起放入一个 exe 文件中,所以在我们的示例中,我们在这个 log.obj 文件中有我们的日志定义,在我们的 main.obj 文件中有我们的 main 函数,所以链接器基本上会从日志中获取该日志定义并将其放入一个通用二进制文件中,这是我们的 helloworld.exe 文件,其中包含主和日志的定义

这是C++工作原理的基本概述

我强烈建议您查看有关编译和链接如何工作的深入视频,因为它们可能比这个教程有更多的信息,这个只是为了向您展示如何从源文件到实际二进制文件的管道概述。

原理总结

源文件经过以下3个步骤编译成可执行文件

  1. 预处理(处理预编译语句)
  2. 编译(将每个cpp源文件编译成单独得obj文件)
  3. 链接(将所有obj文件链接起来)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值