002 - C++是如何工作的

欢迎来到 C++系列的新章节,今天我们要学习 C++是如何工作的。

现阶段我们尽量简单点说,学习如何从源文件开始,也就是实际的文本文档到可执行的二进制代码的过程。

写C++程序的基本流程,是你有一些 C++的源码文件,然后将这些源码文件给到编译器,编译器将其转变成二进制文件,二进制文件可能是某种库,或者是可执行的程序,今天我们主要讲可执行程序的部分。

01 认识一段程序

好的,我们打开VS(建议使用VS,需要的话后期会专门出一节讲解安装过程),vs 上我们已经写了一个输出 ’Hello world!‘ 的程序,它是个非常基础的程序,其中也包含不少的知识点。
请添加图片描述
首先,我们有#include iostream 语句,这个叫预处理,在#符号之后的都是预处理语句,编译器收到源文件后,一看到这条语石,就先处理这些个预处理语句,这也是为什么叫做预处理了,因为它在实际编译发生之前就被处理了。

include 的含义是,它需要找到一个文件,在这个例子中,需要找到叫iostream的文件,然后将该文件所有内容,拷贝到现在的文件内,这些所包含的文件通常被称为头文件,后期课程我们会深入探讨头文件。

我们之所以要包含 iostream 这个头文件,是因为我们需要一个被调用的函数的声明,std:cout ,它可以让我们在终端打印东西。

接下来是main 函数,main 函数非常重要,因为任何一个 C++ 程序都有main 函数。

main 函数是程序的入口,意思是当我们运行程序时,计算机就从这个函数开始执行代码,当程序运行时,计算机会逐行执行我们的代码,当然,程序也可以通过控制语句或者是函数调用中断或者改变执行的顺序,但最主要的还是一行一行的执行,因此,我们的程序首选被执行的是,std::cout << helloworld << std::endl这句,然后是std::cin.get()

运行完 main 中的所有东西后,我们的程序就结束了。对于那些了解函数的人会发现,main 函数的返回是int 类型,然而,我们并没有返回int,这是因为main函数比较特殊,它不一定需要返回值,如果你不返回值的话,它会默认你返回了0,这个只对main 函数适用,是一种特殊情况。

接下来,我们再讨论更多细节,有2个左箭头符号(<<)看上去很奇怪,其实他们只是写成这个样子,并没有更多的实际含义,这些看上去像左移运算符的东西,叫做重载运算符,你可以把它理解为一个函数,在这个例子中,和cout.print 一个意思,hello world! 就是这个函数的参数,另一层含义是,将字符串hello world! 推送到cout 流中,然后推送一个行结束符(endl 告诉终端跳到下一行)。

cin.get() 函数是等待我们按下enter 键,在前往下一句代码之前等待,这个时候程序暂停执行,直到我们按下回车键后,程序继续运行下一行,但已经没有下一行了,所以程序返回0,意味着代码执行完了,这就是整个程序。

02 学会设置

好了,我们现在写完了源代码文件main.cpp,我们怎么把它转换成可运行的二进制文件呢?

这有几个步骤,首先我们已经 include iostream,这是预处理语句,编译器先处理这些语句,在这个例子中,编译器会将 iostream 文件内容全部包含进来,也就是拷贝其全部内容黏贴到代码文件中,(以后还会讲头文件的内容,所以不需要担心现在不理解,现在只需要知道包含了iostream 这个文件,我们就可以用cout,cin 这些函数就可以了)。

当预处理语句处理完了之后,我们的文件将被编译,这个阶段,编译器将所有 C++ 代码转化为实际机器代码,这里有些常重要的设置决定我们怎么转化代码,让我们看下 Visual Studio。
请添加图片描述

这里有两个重要的拉下菜单,一个叫解决方案配置,一个叫解决方案平台,有一些默认的选项,默认的是Debug和x64,也有显示为×86或win32,x86和win32是一个意思,点开Debug下拉,你可以看到2个选项,Debug 和Release。所有vs项目都默认有这2个选项,在解决方案平台这里,可以看到X64、 X86两个选项。

强调下,配置只是构建项目的时候的一系列规则而已,解决方案平台是指你编译的代码的目标平台,X86的意思就是目标平台是windows32位,也就是说会生成32 位的windows 应用程序,其他复杂的项目的目标平台也不相同,你可能在下拉菜单中看到 Android 平台,如果你想构建、部署、调试Android,如果你想改变到Android 目标平台,解决方案配置这里需设置一系列针对目标平台的规则,感兴趣的同学可以看下让看看这些规则。

我们可以在工程中更改,选中工程,我们右键点击属性,打开Visual Studio 属性页面。

请添加图片描述

这里定义的规则用来构建解决方案配置及解决方案平台,首先需要注意到的是配置(configuration)和平台(platform)区域,要设置成你实际想要配置与目标平台,你在这里设置release 模式的话,对现在你用的debug 模式没有任何影响,这是我们的debug 模式的设置,你在这里还可以看到win32,Win32等同于 X86,他们因为某些原因搞了不同的名字,但他们其实是一样的东西。

我们看到这里是常规(general)的信息,包括 SDK 版本,输出目录或者中间目录等等,这里的配置类型比较重要一点,设置为应用程序,如果我们想做一个生成库文件的项目,我们可以在这里做相应修改,编译器会生成二进制文件。

编译器方面的设置在c/c++项,这里有很多重要得到设置,比如包含目录(include)设置、优化设置、代码生成设置、预处理定义以及一堆现在还用不上的配置项,其实啥也不需要做,vs默认的设置就很好了。

请添加图片描述

这些规则控制我们的文件如何被编译,你可以看到debug 和release 的区别,如果你在release下进入到优化(optimization)页面,你会看到 优化(Optimization)这一项是最大优化,这就是为什么默认的debug 模式会更慢的原因,比release 慢很多是因为优化都被关掉了,但是关掉优化的好处是让我们可以调试代码(如果你想知道编译器的更多细节,以后会专门做讲解)。

03 开始编译

项目中的每一个 cpp 文件都会被编译,但头文件不会被编译,仅仅是cpp文件,原因是之前说过的,头文件的内容在预处理时包含进来了,CPP 文件被编译的时候,包含进来的文件一起被编译了,因此我们有一堆将要被编译的cpp文件,分别被编译器编译,每一个cpp 文件都被编译成了一个object file (目标文件,如果你用 VS,生成的文件后是.obj,当我们有了这些cpp 编译后生成的独立的 obj 文件后,我们需要把这些文件合并成一个执行文件,该轮到我们的朋友链接器出场了,你可以看到链接器的设定,在链接器设置下可以看到相关设置, 链接就是将所有的obj文件,黏合到一起,把所有的obj 文件合并成一个exe 文件,当然, 这个过程可能有点复杂。

好了,我们实际来看这个例子,首先我们要编译这个文件,单独编译这个文件,可以按快捷键ctrl+ F7

请添加图片描述

你看到输出窗口显示,main.cpp 编译成功了,在这里叫做生成。如果你不想按快捷键,也可以用编译按钮。你可以在工具-自定义中选上生成,工具栏会出现一组按钮,如果你点击第一个按钮,你可以看到代码被成功编译了。

如果我们搞点语法错误,例如,我忘记写一个分号,你将看到报错,Vs 有很多不同的方法显示错误信息,一种是错误列表 ,另一种是输出窗口。

请添加图片描述
请添加图片描述
错误列表只能用来做参考,它就是个信息大概,如果你需要细节,需要所有的出错信息,那就看输出窗口,你可以看到,这里有个错误,语法错误在之前缺少了分号,它告诉你了出错行,你双击这一行出错信息,它会跳转到你的代码出错的位置,我们修复这个错误,然后按ctrl+ F7编译,我们现在已经编译了一个文件。

04 链接

当我们单独编译的时候,链接还没发生,很明显我们编译单独一个文件,不会进行链接,让我们看看编译器编译后都生成了什么,如果我们右键点击项目, 你会看到在资源管理器中打开文件夹按钮,这会打开文件管理器,默认情况下 VS会输出构建文件到debug文件夹,我们在debug文件夹下会看到编译器生成的目标文件。

请添加图片描述

对于项目中的每一个 C++文件,都会生成一个obj文件,回到 Visual Studio 在项目上右击,选择生成项目,这次就不是编译单个文件了,而是构建整个项目,实际上你会看到生成了exe 文件,我们回到文件浏览器,可以直接点击运行 exe 文件,打印 ’hello world‘。有些不同版本的VS生成的位置可能略有差别。

请添加图片描述

总的来说很简单,但如果是多个 C++文件呢。

我们看个简单的例子,假设我们仍然要打印 ‘Hello World!’,但我不想用cout 函数,我想用自定义log函数来包裹 cout 函数。因此我们建立一个函数,参数是 message,打印这个message到终端。

请添加图片描述

不用担心你现在搞不懂 const char *是什么,我们将在另一个课程中讲解 string,现在你只需要知道,const char*是一个包含字符串的类型,现在我们可以重写代码,不是调用 cout 函数来打印 ‘Hello world!’,而是调用log函数,Hello world! 作为参数输入,我们点击本地Windows 调试器,看看程序是否可以运行,你看到了可以运行,我们已经写了第一个简单的函数。

现在我们把我们自己的函数放到另外一个文件中,main 文件包含所有东西不是一件好事,代码分到很多文件当中可以保证代码干净整洁。

新建一个新的文件,在资源文件这里,点击右键->添加新项,选择C++文件,命名为log.cpp,点击添加,可到 main 文件中,将 log 函数剪切以及头文件信息复制,粘贴到这里,然后 log.cpp 文件中有了 log 函数,我们编译 log.cpp,可以编译成功。

请添加图片描述

回到main,我想要调用 log 函数,做得到吗?按下Ctrl+ F7,不行,找不到 log 标识符,这是什么问题呢?我们将一个函数移动到另一个地方,然后我们对每个文件进行单独的编译,main.cpp 文件并不知道还有个叫做log 的函数,因为不认识它,所以报错。

我们可以通过调用申明来修复此错误。

请添加图片描述

申明就像我们宣布有个叫 log 的函数是存在的,这就像是一个承诺,告诉编译器,这里有个名叫 log 的函数,编译器只需要相信我们就好了,这对于编译器是个好事情,这就像,我完全相信你,因为编译器并不关心这个函数是在哪儿定义的,这里有2个术语,申明定义,申明就是说,这个符号、 这个函数是存在的。定义的意思是说,这个函数到底是什么,是函数的函数体。

声明与实际的定义很相似,之所以并叫做定义是因为定义操作不仅申明了log 函数名,而且包含了函数体,告诉了这个函数是要干什么。申明与定义很相似,但是它没有函数体,仅仅加了个分号在末尾,实际上,你甚至不用指定参数的名字,因为这不重要,你可以这样写,但是作为经验,还是建议指定参数名字,这样更加清晰,让我们编译看看结果,编译没问题。

你可能有一些疑问,编译器是怎么知道,log 函数会在另一个文件中呢,答案就是编译器相信我们,然后你的第二个疑问是,它怎么实际运行到正确的代码,这里就需要链接了,当我们构建整个工程时,不是单个文件,所有文件都会被编译,链接器会找到正确的 log函数的定义在哪里,将函数定义导入到 log 函数中,让我们在 main.cpp 中调用。如果找不到定义,将会出现链接错误。

可以这样理解:链接器的工作,是连接函数,它找不到 log 函数连接到哪儿,因为我们并没有一个叫做 log 的函数,我们只拥有函数体的叫 log 的函数。

因此我们需要修改代码,修复函数,我们需要提供一个log 函数的定义,换句话说,我们需要为 log 函数写函数体。

log 函数不一定要在这个文件里面,它可以在main.cpp中,也可以在其他地方,如果我们编译它,你会发现没有问题了,回到文件管理器看看,你会看到2个obj文件,因为编译器会为每一个源文件生成一个obj 文件,链接器会将他们合并成一个exe 文件。 在这个例子中,链接器从 Iog.obj文件中拿出log的定义,放入二进制文件helloworld.exe中,exe文件包含logmain 的函数定义。

05 建议

这就是 C++ 如何工作的最基本的东西,再次建议你找些编译与链接方面的深度资料,因为他们可能讲的更加深入,这个课程主要是讲了一个整体的过程,从源文件到二进制文件。

下一讲,我们学习编译器是如何工作的,就这样,下期见。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值