坎德人的小包包

欧剃,游荡的坎德人,在他的旅途中收集了许许多多有趣的东西。

发表日期:2020-03-20

10 分钟看懂 C++ 编译过程

—— 看不懂包退

作者:Stephen Welch


C++ 是一门高性能编程语言,它被广泛应用于世界最前沿的技术应用当中——从数据挖掘、大数据,到自动驾驶汽车、机器人,再到电子游戏和视频处理,C++ 的身影可谓无处不在。作为一名 C++ 程序员,你已经相当精通这门语言,对多线程和并行编程等主题也有了一定的了解。但你是否曾经揭开过编译器那神秘的面纱,是否好奇过在编译期间到底发生了什么事?

这是一个非常值得讨论的问题,今天这篇文章里,我们将详细聊聊你最需要了解的几个重点细节。对编译器内部工作原理的了解能帮你更深刻地理解代码,避开许多常见的“坑”,从而进一步提高你的编程水平。

编译流程

现在,让我们一起打开编译器的“小黑盒”,用最简明的办法来解释 C++ 编译器到底都对你的代码施了什么“魔法”吧。

作为一门高级编程语言,C++ 让程序员的编程工作变得更加容易——低级机器语言一板一眼的本性难以用于编写足够复杂的现代应用程序。编译器通过将 C++ 源码转换成计算机可以执行的二进制文件,填补了高级 C++ 语言和机器语言之间的空白。

总的来说,编译过程还比较复杂,一般可以分为三个阶段:

预处理 Preprocessing

在实际编译工作开始之前,预处理器指令指示编译器对源码进行临时扩充,以为之后的步骤做好准备。

在 C++ 中,预处理器指令以 # 号开头,比如 #include#define#if 等。在这一阶段,编译器逐个处理 C++ 源码文件。对于 #define 指令,编译器将源码中的宏替换成宏定义中的内容;对于 #if#ifdef#ifndef 指令,编译器将有选择地跳过或选中部分源代码;而对于 #include 指令,编译器将把对应的库的源码插入到当前源代码中——这通常是一些通用的声明。被 #include 指令引入的头文件( .h )往往会包含大量的代码,你引入的越多,最后生成的预编译文件就越大。总的来说,预编译过的文件会比原来的 C++ 源码更大一些。

通过上面这些替换和插入操作,预处理器产生的是被合为一体的输出文件。预处理器还会在代码中插入记号,使编译器能分辨出每一行来自哪个文件,以便在调试过程中能生成对应的错误信息。在开发调试你的 C++ 程序时,这些错误信息能给你很多帮助。

编译和汇编 Compilation & assembly

在这一阶段,编译器通过两个连续的步骤,将预处理器产生的代码编译成目标文件(object file)。

首先,编译器将去除了预编处理器指令的纯 C++ 代码编译成底层汇编代码。在这一步中,编译器会对代码进行检查优化,指出语法错误、重载决议错误及其他各种编译错误。在 C++ 中,如果一个对象只声明,不进行定义,编译器仍然可以从源代码产生目标文件——因为这个对象也可以指向某些当前代码中还未定义的标识符。

其次,汇编器将上一步生成的汇编代码逐行转换成字节码(也就是机器码)。实际上,如果你希望把代码的不同部分分开编译的话,编译过程在这一步之后就可以停止了。这一步生成的目标文件可以被放在被称为静态库的包中,以备后续使用——也就是说,如果你只修改了一个文件,你并不需要重新编译整个项目的源代码。

链接 Linking

链接器利用编译器产生的目标文件,生成最终结果。

在这一阶段,编译器将把上一阶段中编译器产生的各种目标文件链接起来,将未定义标识符的引用全部替换成它们对应的正确地址。没有把目标文件链接起来,就无法生成能够正常工作的程序——就像一页没有页码的目录一样,没什么用处。完成链接工作之后,链接器根据编译目的不同,把链接的结果生成为一个动态链接库,或是一个可执行文件。

链接的过程也会抛出各种异常,通常是重复定义或者缺失定义等错误。不只是没进行定义的情况,如果你忘记将对某个库或是目标文件的引用导入进来,让链接器能找到定义的话,也会发生这类错误。重复定义则刚好相反,当有两个库或目标文件中含有对同一个标识符的定义时,就可能出现重复定义错误。

理解编译过程有什么用?

当你对编译过程的各个阶段有了新的理解,你就能更好地理解编译错误或连接错误产生的原因,并避免这些与编译相关的潜在问题。比如,如果你理解了预处理过程,你就能利用头文件保护符(用于保护头文件内容不被错误地多次包含的预编译器指令)防止一些编译错误的出现。

对 C++ 编译细节的充分了解,能使你从一个完全不同的角度看待整个编程过程,也让你对原先以为是理所当然的编译过程有了新的认识。

如何使用 C++ 编译器

构建并运行一个 C++ 程序所需的基本步骤有:

  1. 使用一个编辑器或是编程环境(IDE),构建一个语法正确的 C++ 源文件。
  2. 运行编译器对源文件进行编译,生成可执行文件。
  3. 运行生成的文件。

编译器的特性差异很大,即使在同一个编译器的不同版本之间也是这样;同样,它们的选项也非常丰富,比如在代码生成、调试、浮点数行为、库处理等方面,都有着相当多的选项。

C++ 编译器纵览

现在你已经对 C++ 的编译有了一定的了解,那么你该用哪种编译器呢?

总体上说,你可以按编译器的许可类型(免费或是收费),使用方式(本地安装或是在线编译)以及所支持的操作系统(Windows、OS X 或 Linux)来分类。

下面是几点建议:

  • 如果你在 Linux 上进行编程,GNU 编译器套装(GCC)是个非常流行的选项。它是免费的,而且你所用的 Linux 发行版的软件包仓库里通常就有。
  • 对于 macOS 来说,Clang 是个默认选项,它随 Xcode 的命令行工具一起安装。使用 Clang 也是免费的。
  • Cygwin 项目为 Windows 系统提供了一系列 Linux 工具集,包括 GCC 在内。你可以使用 Cygwin 来运行 GCC 或 Clang,但请注意,用这种方式生成的代码需要 Cygwin 才能运行。
  • Windows 系统的另外一种选择是 MinGW,它不依赖于 Cygwin,而且能生成可原生在 Windows 上运行的可执行程序。

有些 IDE 本身在代码编辑器之外就已经包含了编译器。比如 macOS 上的 Xcode,以及 Windows 上的 Visual Studio 等。此外,还有许多专业化的编译器,比如英特尔 C++ 编译器等,为特定的需求专门定制了一些特性。比如,英特尔的编译器在自家的处理器上能更有效地利用多核心架构,产生的代码在英特尔的硬件上运行速度更快。这类专业化的编译器常常需要用户购买价格不菲的授权才能使用。

如果你发现自己正在考虑使用某种不是很流行的编译器,请认真了解它的标准依从性。避免使用那些不符合 ISO 标准,或不提供可靠实现的标准库的编译器。这里提到的“标准库”是 C++ 自带的大量库文件;而“库文件”,则是已经“打包”好,可以在其他程序中重复使用的预编译代码的集合。

有些编译器和库文件一起被嵌入在软件开发工具(IDE)提供的框架中。这些框架很有用,但如果你打算更换你的工具链,你可能很难脱离它们。

在线 C++ 编译器

在线编译器是种很有用的工具,它能让你快速编译代码,而不需要在电脑上安装完整的编译工具链。这让程序员能更轻易的摆弄代码,熟悉最新的语言特性,或是在线分享代码片段,实时合作编辑,以及测试各种不同的编译器等。除了狭义的“编译”功能之外,大部分在线编译器还会执行编译完的程序,并将输出结果显示出来。

和离线编译器一样,在线编译器支持的 C++ 标准版本和提供的特性也千差万别,从使用 flag 标识来定义编译参数,到处理标准输入并传入命令行和运行时参数等待,不一而足。

常用的在线编译器有下面这几个:

  • Compile Explorer
  • Repl.it
  • IDEone
  • Codepad

你还可以在这里看到关于更多在线编译器的列表,已按特性进行分类。

总结

本文中,我们介绍了 C++ 编译过程的各个阶段,更加详细地了解了整个过程。通过学习俗如何使用 C++ 编译器,并对各种 C++ 编译器进行概述,你得以一窥编译过程的幕后细节,并对它有了一些深入的了解,希望能给你带来帮助。

希望了解更多关于 C++ 编译过程的详细信息?想要学习更多 C++ 知识?欢迎报名参加我们的 C++ 纳米学位课程!

(本文已投稿给「优达学城」。 原作: Stephen Welch 翻译:欧剃 转载请保留此信息)

编译来源: https://blog.udacity.com/2020/02/c-compilers-explained.html

标签:UdacityTranslateC++Nanodegree

Powered by Jekyll on Github.io
2022 © 欧剃