在大学课堂上学习 C++ 时,老师并没有过多涉猎 C++ 语法背后的知识。也就是说,初学 C++ 时,哪怕写出了代码,我也并不知道从代码到程序的过程中究竟发生了什么。我也曾尝试了解,但作为初学者,面对一堆晦涩难懂的词汇,我也只是看了几眼便放弃了。但知道背后的过程无疑是非常重要的。今日,谨以此文回答我自己学习过程中遇到的问题:C++ 代码是如何变成程序的。


初探 C++ 编译过程

C++ 程序编译过程一般可以分为三个步骤:

  1. 预处理 Pre-Processing
  2. 编译 Complication
  3. 链接 Linkinig

其中第二步“编译”的意思并不是指从代码到可执行文件的全过程,而是从 C++ 到 machine code 的过程。我们先对这三个步骤进行初步了解:

预处理

预处理器 Pre-Processor 接收 C++ 源码文件,处理其中的预处理指令。然后输出一个预处理完毕的文件,文件后缀通常为 i。

编译

编译器 Compiler 获取预处理器的输出,将其翻译成汇编语言,汇编语言文件通常以 s 作为后缀。然后再将得到的汇编语言文件翻译成 machine code,得到 object file,也就是目标文件,通常以 o 或者 obj 作为后缀,是二进制文件。

链接

链接器 Linker 获取编译器生成的所有目标文件,然后链接程序所需的外部库文件,最终得到一个可执行文件或者库文件。

值得注意的是,编译阶段实际上可以再分为从 C++ 到汇编,从汇编到 machine code 两步。因此我们最终细分得到的 C++ 程序编译过程如下:

  1. 预处理 Pre-Processing
  2. 编译 Complication
  3. 汇编 Assemble
  4. 链接 Linkinig

该过程如图:

接下来,让我们更详细地了解以上四个阶段。


预处理

预处理阶段获取源文件处理其中的预处理指令,常见的预处理指令有:

#include // 包含文件
#ifdef   // 如果定义了 xxx
#ifndef  // 如果没有定义 xxx
#define  // 定义宏
#undef   // 移除原先定义的宏
#endif   // 结束 if 判断

预处理指令均以“#”开头,更多预处理指令请参考:

Preprocessor directives | Microsoft Docshttps://docs.microsoft.com/en-us/cpp/preprocessor/preprocessor-directives?view=msvc-170&viewFallbackFrom=vs-2019        在这一阶段,预处理器会根据预处理指令执行相应的操作。例如,#include 指令会让指定文件被包含进预处理文件。宏也会在这一阶段被替换和展开。预处理器处理完所有预处理指令后,生成一个预处理完毕的文件


编译

这一阶段主要获取预处理阶段生成的文件,然后将其翻译为汇编代码,得到汇编代码文件。通常来说,我们收到的编译器警告和错误都来自这一阶段。这些警告和错误是由语法错误导致的。


汇编

这一阶段会获取编译阶段生成的文件,将其进一步翻译为 machine code,得到 object file,也就是目标文件,目标文件本身是二进制文件。此外,如果你一次编译了多个独立文件,那么每一个文件都会生成对应的目标文件。


链接

链接阶段获取汇编阶段生成的目标文件,将所有目标文件编译成可执行文件库文件。然后链接器解析依赖项,链接外部静态库,得到最终的可执行文件库文件。这一阶段最常见的错误是缺少定义或者重复定义,前者由定义不存在或目标文件未给出链接器导致,后者由同一符号在多个目标文件或库文件中重复定义导致。


示例代码

接下来,我们以 GCC 为例,通过示例,生成各个阶段对应的文件,对编译的过程进行更详细的了解。示例包含三个文件:math.hpp,math.cpp,main.cpp,你可以在任意位置生成它们。

math.hpp:

#ifndef MATH_HPP
#define MATH_HPP/* Test Comment */
int add (int lhs, int rhs);#endif

math.cpp:

#include "math.hpp"int add (int lhs, int rhs)
{return lhs + rhs;
}

main.cpp:

#include "math.hpp"
#define a 10
#define b 20int main()
{int c = add(a, b);return 0;
}

文件结构如图,math.cpp 文件和 math.hpp 文件位于 include 文件夹内:


预处理文件

打开命令行,定位到文件所在目录,以我的电脑为例,文件目录为。

D:\Workflow\WorkSpace\QuickStart\Cpp

我们使用如下指令对 main.cpp 文件进行预处理:

g++ -E -Iinclude main.cpp -o main.i

上面这条指令中各参数作用如下:

-E        告诉编译器执行完预处理就退出
-Iinclude 指定文件夹 include 为头文件目录,此处使用 -I./include 同理
-o        指定输出文件名

进行预处理之后会得到预处理文件 main.i,位于 main.cpp 文件同级目录:

打开 main.i 文件,其中内容如下,(为了便于阅读,我删除了多余空行):

# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.cpp"
# 1 "include/math.hpp" 1int add (int lhs, int rhs);
# 2 "main.cpp" 2int main()
{int c = add(10, 20);return 0;
}

我们重点关注第七行和第十二行。第七行,预处理器在检测到 #include "math.hpp" 后,将 math.hpp 文件的文件体替换到了 main.cpp 文件内。第十二行,预处理器在检测到了宏 a 和 b 时,将其文本替换为 10 和 20。因此我们可以发现,宏会在预处理阶段被处理,宏变量会被替换,宏函数也会进行相应文本替换。此外,我们发现 math.hpp 中的注释也被删除了。


汇编代码文件

预处理的下一阶段是编译阶段,为了探究生成的汇编代码,我们使用如下指令编译文件:

g++ -S -Iinclude main.cpp -o main.s

上面这条指令中各参数作用如下:

-S        告诉编译器执行完编译就退出
-Iinclude 指定文件夹 include 为头文件目录,此处使用 -I./include 同理
-o        指定输出文件名

进行编译阶段后,我们会得到文件 main.s:

打开 main.s 文件,内容如下:

 .file   "main.cpp".text.def   __main; .scl    2;  .type   32; .endef.globl    main.def    main;   .scl    2;  .type   32; .endef.seh_proc main
main:
.LFB0:pushq %rbp.seh_pushreg    %rbpmovq    %rsp, %rbp.seh_setframe %rbp, 0subq $48, %rsp.seh_stackalloc    48.seh_endprologuecall  __mainmovl  $20, %edxmovl   $10, %ecxcall   _Z3addiimovl    %eax, -4(%rbp)movl  $0, %eaxaddq    $48, %rsppopq   %rbpret.seh_endproc.ident   "GCC: (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0".def  _Z3addii;   .scl    2;  .type   32; .endef

这一步得到的是汇编代码,你可以看到我是在 Windows 环境下编译的。


目标文件

汇编阶段,我们利用如下指令得到目标文件 main.o:

g++ -c main.s -o main.o

上面这条指令中各参数作用如下:

-c        告诉编译器执行到汇编操作退出
-o        指定输出文件名

此时我们得到了 main.o 文件:

main.o 二进制文件,文本编辑器中只能看到乱码:


链接

我们输入如下指令尝试输出可执行文件:

g++ main.o -o main

很遗憾,报错了:

main.o:main.cpp:(.text+0x18): undefined reference to `add(int, int)'
collect2.exe: error: ld returned 1 exit status

其原因是比较显然的,链接这一步是链接目标文件外部库的。我们在上述的步骤中,自始至终只对 main.cpp 文件进行了编译,而其依赖项 math.hpp 并不是一个外部库文件,我们需要对其进行额外操作。由于 math.hpp 内部函数的实现在 math.cpp 中,我们需要编译 math.cpp 文件,把它转换成一个目标文件,然后链接所有目标文件。

我们使用如下指令编译 math.cpp 文件得到目标文件 math.o:

g++ -c ./include/math.cpp -o math.o

链接所有目标文件得到可执行文件:

g++ main.o math.o -o main.exe

这样,我们就得到了最终的可执行文件 main.exe:

上面给出的编译过程复杂且文件繁多,一般来说,我们编译示例中的代码不需要那么复杂,只需要如下一条指令即可:

g++ -Iinclude main.cpp ./include/math.cpp -o main.exe

总结

通过以上示例,我们可以了解到,C++ 代码编译过程中,会依次生成

  1. 预处理文件 .i
  2. 汇编代码文件 .s
  3. 目标文件 .o/.obj
  4. 可执行文件 .exe/.out 或库文件 .a/.lib

C++ 代码的编译过程不可谓简单,但是了解编译背后的过程和机理可以很好地帮助我们理解、程序,正如当我知道了宏定义是在预处理阶段展开时的恍然大悟。宏定义的这一特征意味着宏函数不像普通函数有对函数栈空间的压栈操作和内存需求,效率上有一定提升。

总之,了解 C++ 编译过程必然是一次充满收获的探险。本文仅概括了 C++ 编译生成可执行文件的过程,并未涉及静态库生成,如您有类似需求请移步:C++静态库与动态库 - 吴秦 - 博客园https://www.cnblogs.com/skynet/p/3372855.html


参考

  1. Learn How to Compile a C++ Program | gamedevunboxedhttps://gamedevunboxed.com/learn-how-to-compile-a-c-program/
  2. How does the compilation/linking process work? | StackOverflowhttps://stackoverflow.com/questions/6264249/how-does-the-compilation-linking-process-work
  3. C语言编译过程详解 | 博客园C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程。https://www.cnblogs.com/CarpenterLee/p/5994681.html#top
  4. C++到底是怎么编译的? | 知乎g++编译可执行文件: g++ main.cpp -o main.exe 应该有不少人都好奇过:“g++编译器帮我们做了什么?怎么就让C++的代码变成一个可以完成对应代码指令的可执行二进制文件了?”,这里我们就稍微打破一点砂锅,问多…https://zhuanlan.zhihu.com/p/365820917
  5. C语言编译和链接详解我们平时所说的程序,是指双击后就可以直接运行的程序,这样的程序被称为 可执行程序(Executable Program) 。在 Windows 下,可执行程序的后缀有 .exe 和 .com (其中 .exe 比较常见);在类http://c.biancheng.net/view/1736.html

C++ 程序编译过程:从代码到程序相关推荐

  1. 第一章 PX4程序编译过程解析

    版权声明:本文为博主原创文章,未经博主允许不得转载. 第一章 PX4程序编译过程解析 PX4是一款软硬件开源的项目,目的在于学习和研究.其中也有比较好的编程习惯,大家不妨可以学习一下国外牛人的编程习惯 ...

  2. PX4程序编译过程解析

    第一章 PX4程序编译过程解析 PX4是一款软硬件开源的项目,目的在于学习和研究.其中也有比较好的编程习惯,大家不妨可以学习一下国外牛人的编程习惯.这个项目是苏黎世联邦理工大学的一个实验室搞出来的.该 ...

  3. C程序编译过程及常见选项--静态库和动态库

    C程序编译过程及常见选项--静态库和动态库 前言 一.gcc详讲 1.1 编译过程 1.2 预处理 1.3 编译(Compilation) 1.4 汇编(Assembly) 1.5 链接(Linkin ...

  4. Linux 程序编译过程

    前言 计算机程序设计语言通常分为机器语言,汇编语言和高级语言三类.而高级语言需要被翻译成机器语言才可以被执行,而翻译的方式也被分为两种,一种是编译型,另一种为解释型,根据这两种的不同,我们将其分为编译 ...

  5. C语言——C程序编译过程

    C语言目录: 1. 概述 2. 数据类型 3. 量 4. 运算符 5. 流程控制 6. 函数 7. C程序编译过程 8. 文件 9. 内存管理 #mermaid-svg-5eSYOEOTEbZDntT ...

  6. 编译html成qch,在应用程序编译过程中运行qcollectiongenerator

    我一直在研究一个名为RoboJournal的程序很长一段时间.下一版本包含完整的文档;每当用户按F1或单击RoboJournal程序中的帮助项目时,帮助文件将显示在Qt助手中(比简单地打开浏览器窗口以 ...

  7. C/C++程序编译过程详解

    C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接.编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程.链接是把目标文件.操作 ...

  8. Linux 程序编译过程的来龙去脉

    大家肯定都知道计算机程序设计语言通常分为机器语言.汇编语言和高级语言三类.高级语言需要通过翻译成机器语言才能执行,而翻译的方式分为两种,一种是编译型,另一种是解释型,因此我们基本上将高级语言分为两大类 ...

  9. C++ 程序编译过程

    前言 C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接.编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程.链接是把目标文件 ...

最新文章

  1. 磁盘与文件系统管理( 认识磁盘,了解磁盘,文件系统的建立与自动挂载)
  2. 【渝粤题库】国家开放大学2021春1703农村发展理论与实践题目
  3. nginx访问日志 logstash 配置文件实例2
  4. day27-python并发编程之多进程
  5. 12.16直播:藏在华为物联网操作系统里的“秘密”
  6. 【elasticsearch】block.ClusterBlockException: blocked by: SERVICE_UNAVAILA
  7. Quartz的使用案例
  8. android内存占用分析,Android内存优化————虚引用与弱引用的使用及内存分析工具...
  9. 西游记不单单讲的是故事(1) ------ 摘自 吴闲云的《煮酒探西游》
  10. 独家深挖!F1赛车协会“刹车表现”是如何进行数据分析的?
  11. 2.4 旋转曲面 (1)
  12. iOS中SDK的简单封装与使用
  13. C语言上学期整理(第6章)
  14. python 根据TIN查询点云坐标
  15. 蓝奏云网盘无法访问解决方法
  16. imx53 uboot tftp nfs启动, linux tftp,复制gdb, linux host 创建sd卡启动,ddr stress tester
  17. Spring Boot内置Tomcat设置超时时间
  18. pdk7105的I2C配置
  19. [BISTU校赛]12月12日校赛题解
  20. java 集合与泛型_java的集合和泛型的知识点归纳1

热门文章

  1. Apache Spark 怎么选择 JOIN 策略?
  2. 前端基础 CSS 第十一章 使用CSS样式表 ----暑假学习第七、八天
  3. html标签中before,css中before是什么意思?
  4. bc在计算机领域是什么意思,“BC”是“Before Computers”的缩写,意思是“在计算机之前”...
  5. 美国的工会制度——Google成立工会背后
  6. 关于微信微博等二维码问题
  7. 微信微博分享注意事项(sharesdk)
  8. 入门python爬虫
  9. LeetCode题解:矩阵中战斗力最弱的 K 行
  10. 洛谷P2486 lct做法