C++是如何工作的

1,034 阅读14分钟

下一篇:编译器是如何工作的

1. 概述

写c++程序的基本工作流程是你有一些C++的源文件,然后将这些源文件给到编译器,编译器将其转变成二进制的东西,二进制的东西可能是某种库,或者是可执行的程序(.exe)。本篇以创建项目结合案例的方式讲述。

  • 头文件(.h)
  • 源文件(.cpp)
  • 头文件预处理
  • 编译器编译源文件生成目标文件
  • 链接器链接生成的目标文件
  • 生成可执行程序文件(.exe)或库

2. 工程创建

下面以创建工程为例

1. 创建一个helloworld工程

以vs2015为例,打开vs,新建项目工程

image.png

选择空项目,并给项目起一个名称,我这里起的是HelloWorld勾选为解决方案创建目录

image.png

创建好项目,项目的解决方案资源管理器树结构

image.png

可以进入到项目所在文件浏览器,观察生成的文件

image.png

image.png

vs解决方案资源管理器中有一个按钮,显示所有文件按钮

image.png

点击该按钮后,便可在项目根目录下创建src的目录

image.png

在src目录项创建一个main.cpp的源文件

image.png

image.png

创建好Main.cpp源文件,编写主入口程序(main函数)

image.png

2. main.cpp文件解析

  • #include <iostream> 预处理语句

    #符号之后的语句,都是预处理语句

    编译器收到源文件后,一旦扫描到这些语句,就优先处理这些预处理语句。这也是为什么叫预处理了,因为他在实际编译发生之前就被处理了。

  • include,表示需要找到一个文件,在这个例子中,需要找到叫iostream的文件,然后将该文件中的所有内容拷贝到现在的文件内。这些所包含的文件通常被称为“头文件”。这里之所以要包含iostream这个头文件,是因为我们在main函数中需要一个被调用的函数的声明:std::cout(可以让我们在终端上打印东西)

  • main函数 程序入口

    main函数是程序的入口,当我们运行程序时,计算机就从这个函数开始执行代码,当程序还在运行,计算机会逐行执行我们的代码。当然,程序也可以中断或者改变执行的顺序。他们是控制语句或者是函数调用,但最主要的还是一行一行的执行。因此,我们的程序首先执行的是std::cout << "hello world!" << std::endl;这句,然后是std::cin.get();这句。运行完main中的所有东西,我们的程序结束了。

      这里需要注意的是,我们在main函数中并没有返回int值,虽有函数声明中有需要返回int值
      但是main函数比较特殊。如果我们没有指定返回值,它会默认返回0。
      这种情况只对main函数适用
    
  • << 一种重载运算符

    这其实是一种重载运算符,可以把它理解成一个函数。将字符串hello world!推送到cout流中,然后打印到终端,然后推送一个行结束符std::endl,endl告诉终端跳到下一行。

  • std::cin.get();

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

到现在已经写完main.cpp源代码文件,我们怎么把它转换成可运行的二进制文件。

3. 预处理阶段

#include <iostream>这是预处理语句,编译器先处理这些语句。在这个例子中,编译器会将iostream文件中的内容全部包含进来,也就是拷贝粘贴内容到代码文件中。包含进来后,就可以使用std::coutstd::cin这些函数了。

4. 编译阶段

当预处理语句处理完了之后,我们的文件将被编译,这个阶段,编译器将所有c++代码转化为实际机器代码,这里有些非常重要的设置决定我们怎么转化代码。

1. vs一些配置简单介绍

这里来介绍下vs界面的一些配置

1. 解决方案配置解决方案平台

可以看到vs中有两个重要的下拉菜单,分别是解决方案配置解决方案平台,默认是debugx86(win32),这里特别说明一下,x86和win32是一个意思。

image.png

image.png

点击解决方案配置下拉选项,可以看到DebugRelease2个选项。所有vs项目都默认有这两个选项。

image.png

点击解决方案平台下拉选项,可以看到x64x86两个选项。这些配置只是默认的。

image.png

    配置:只是构建项目的时候的一系列规则而已。
    解决方案平台:是指你编译的代码的目标平台
    x86:意思是指目标平台是windows 32位,也就是说会生成32位的windows应用程序,

其他复杂的项目的目标平台也不相同,你可能在下拉菜单中看到Android平台,如果你想构建、部署、调试android,如果你想改变到android目标平台,解决方案配置这里需要设置一系列针对目标平台的规则。

2. 项目属性配置

接下来看下解决方案配置规则,我们可以在工程中更改在解决方案资源管理器中选中项目,右键选择属性点击

image.png

可以看到项目的属性配置,这里定义的规则用来构建解决方案配置及解决方案平台。

image.png

首先需要注意的是配置平台,要设置成你实际想要的配置和目标平台

image.png

你在设置release模式的话,对现在你用的debug模式没有任何影响,如果你忘记了这一点,你会奇怪为什么改了设置没起到一点作用。

image.png

这里调整到debug模式,平台这里是win32

win32 等同于 x86 ,因为某些原因搞了不同的名字,但他们是一样的东西。

image.png

  • 常规属性

包括目标平台版本(SDK版本),输出目录,中间目录等等。

项目默认值中比较重要的一个配置

配置类型`英文对应Configuration type应用程序(.exe) 英文对应 application(.exe)

编译器会生成二进制文件,如果我们希望生成可执行的二进制程序,就是设置为应用程序(.exe)

如果想做一个生成库文件的项目,可以修改这里配置,选择下拉选项中的其他的选项。

image.png

  • c/c++编译器方面的设置

image.png

这里有很多重要的设置项,比如include目录设置,优化(optimization)设置,代码生成设置,预处理定义等等一些配置。

这些规则控制我们的文件如何被编译,你可以看到debug和release的区别。如进入优化配置页面,可以对比如下

image.png

这就是为什么默认的debug模式会更慢的原因,因为对比release模式很多优化都被关掉了,但是关掉优化的好处是让我们可以调试代码。

项目中每一个.cpp文件都会被编译,但头文件(.h)不会被编译,仅仅是cpp文件。
原因是之前说过的,头文件的内容在预处理是包含进来了。
cpp文件被编译的时候,包含进来的文件一起被编译了。
因此我们有了一堆将要被编译的cpp文件,分别被编译器编译,
每一个cpp文件都被编译成了一个object file(目标文件)。
如果使用的是vs,生成的目标文件后缀是`.obj`        
当我们有了这些cpp编译后生成的独立的obj文件后,我们需要把这些obj文件合并成一个执行文件,
实现这个就需要用到链接器(link)
  • 可以看到vs中链接器的配置

image.png

基本上,链接就是将所有的obj文件,黏合到一起,把所有的obj文件合并成一个.exe文件

2. 编译文件

回到例子

首先,我们要编译这个文件

image.png

单独编译Main.cpp这个文件,可以使用快捷键ctrl+F7或选中文件鼠标右键选择编译。

   单文件编译(也叫生成)方式:
   1. ctrl + F7快捷键方式
   2. 选中文件,鼠标右键点击编译
   3. 工具栏中找到单文件编译按钮,点击编译

image.png

可以看到输出窗口显示main.cpp编译成功了

如果不想使用ctrl+F7或选中文件鼠标右键选择编译的方式,可以在工具栏中找到如下图标,点击也是编译单个文件。

image.png

如果你的vs工具栏中没有这个图标,可参考工具栏添加移除功能按钮

如果我们搞点语法错误,例如,忘记写一个;

image.png

单独编译文件,看到输出信息将输出报错。

image.png

ps 如果你的vs没有错误列表(erro list)展示,可以依次按下ctrl \ e三个按键或在菜单栏中找到视图选项,点击错误列表

image.png

vs有很多不同的方法显示错误信息,一种是错误列表(error list),另一种是输出(output)。 错误列表经常会缺失一些信息,所以不能绝对依赖错误列表,错误列表基本上就是分析输出(output)窗口,然后找到error这个单词,并抓取这部分信息展示出来,然后放到错误列表(error list)中,所以错误列表只能用来做参考,这只是个大概,如果需要细节,需要所有的出错信息,那就看输出窗口。

从输出窗口中,我们可以看到有一处语法错误,main.cpp文件的第10行缺失}的前面,我们双击这一行错误信息。它会跳转到你的代码出错的位置。

image.png

我们修复这个问题,加上缺失的;,重新编译ctrl + F7

image.png

现在我们已经编译了一个文件,当我们单独编译的时候,链接还没有发生,很明显我们单独编译一个文件,不会进行链接,然我们看看编译器编译后都生成了什么

选中解决方案中的项目右键打开项目所在目录

image.png

默认情况下,vs会输出构建文件到debug文件夹

image.png

进入debug文件夹

image.png

我们会看到Main.obj文件,这是我们的编译器生成的目标文件

对于项目中的每一个c++文件,编译器编译都会生成一个obj文件。

5. 链接阶段

回到vs

选择生成项目,这次就不是编译单个文件了,而是构建整个项目

image.png

image.png

如果你的输出窗口展示的信息量较少,想要展示更多内容,可参考输出窗口信息级别设置

实际上你会看到生成了.exe文件,我们回到项目所在文件目录,在你的解决方案HelloWorld.sln目录下的debug目录

image.png

进入debug目录,可以看到我们生成的.exe文件

image.png

双击.exe文件,可以运行它并打印结果。

image.png

如果是多个c++文件

我们来看个简单的例子 我们对main函数做下改造,不在main函数中使用std::cout,用自己logging函数来包裹cout函数,因此,我们建立一个函数 这里的const char*表示一个包含字符串的类型

image.png

重新运行

image.png

image.png

可以看到可以运行,现在将这个Log函数放到另外一个文件中,我们不希望一个main文件包含所有东西,希望把我们的代码分到很多文件当中,这样可以保证代码干净整洁。

我们新建一个Log.cpp文件

image.png

image.png

将main.cpp文件中的Log函数剪切到Log.cpp文件中,需要加上iostream头文件

image.png

image.png

这时,我们生成Log.cpp单文件,可以看到会有一个报错

image.png

cout不是std的成员,这是编译器告诉我们他不知道cout是什么东西,原因是我们没有声明它,在c++中,任何符号都需要声明,cout在main.cpp中已经声明了,声明它的文件显然就是iostream,因此,我们要将iostream添加到Log.cpp文件的最上面。这样的话,我们就可以得到cout函数声明,让我们再次编译Log.cpp单文件,可以看到编译通过了。

image.png

回到main.cpp文件,编译main.cpp单文件,会看到找不到Log标识符。

image.png

所以,我们将一个函数移动到另一个文件,然后我们对每个文件进行单独的编译,main.cpp文件并不知道还有个叫Log的函数,因为不认识它,所以报错,我们可以通过调用声明来修复此错误。申明就像宣布有个叫log的函数是存在的,这就像是一个承诺,告诉编译器,这里有一个叫Log的函数,编译器只需要相信我们就好了,这对于编译器是个好事情,因为编译器并不关心这个函数是在哪定义的 这里有两个名词声明定义

声明:这个符号、这个函数是存在的,可以不写出函数体。
定义:这个函数到底是什么,是函数的函数体。

回到main.cpp,声明和实际的定义很相似。声明没有函数体

image.png

现在进行编译,可以看到编译通过了

image.png

所以,编译器是如何知道log函数会在另一文件中的呢,答案是我们做了声明告诉了编译器,那么它是怎么实际运行到正确的代码,这里就需要链接了,当我们构建整个工程时,不是单个文件,而是项目生成,直接F7或在解决方案的项目中右键生成,我们的所有文件都会被编译,链接器会找到正确的log函数的定义在哪里,将函数定义导入到log函数中,让我们在main.cpp中调用,如果我们找不到定义,将会出现链接错误。

image.png

image.png

接下来,我们看例子 我们将Log.cpp文件中Log函数改下名字改成Logr保存如下

image.png

回到main.cpp文件,我们单独编译main.cpp文件,ctrl+F7,可以看到编译通过了,因为我们声明了Log函数,编译器相信了,这是单文件编译,还没有到链接阶段。

image.png

然而我们构建整个项目,点击项目,右键生成

image.png

可以看到出现了链接错误

image.png

这个链接错误看上去有些吓人,因为有一些我们自定义函数的额外信息,例如,根据惯例会显示调用函数的系统内部命名。

image.png

其实也就是告诉我们,这个无法解析的外部函数log,他的返回值和参数是什么,相对于main.cpp文件来说。无法解析的外部符号是指,链接器无法解析这个符号。链接器的工作就是连接函数,但它找不到log函数连接到哪了,因为我们并没有一个叫做Log的函数定义,一个拥有函数体的Log函数。因此我们修复这个问题,要提供一个Log函数的定义,也就是需要为Log函数写函数体。Log函数的函数体不一定要在Log.cpp文件里,也可以在main.cpp文件里,也可以在其他地方。我们修复回来,提供Log函数定义。

image.png

然后再次构建项目,项目生成,可以看到编译通过了。

image.png

回到文件浏览器中,进入到项目里面生成的Debug目录,看到里面的目录会有2个.obj文件,因为编译器会为每一个源文件生成一个obj文件,链接器会将他们合并成一个.exe文件。在这个例子中,我们有Log的定义,在Log.obj文件里面。我们的main函数在main.obj里面,链接器从Log.obj文件中,拿出Log函数的定义,放入二进制文件中,也就是我们的helloworld.exe文件,包含Log和main的函数定义。

至此,这就是c++如何工作的最基本的东西。

image.png

下一篇:编译器是如何工作的