我们在这里介绍编译型语言程序(特别是我们自己编写的C/C++程序)的生命周期。

预期学习产出

本章内容

广义的编译指程序源代码文件到最终二进制程序/库的全过程。狭义的编译指上述过程中的一个特定阶段:从预处理结束的文本到汇编代码的过程。

众所周知,计算机无法识别源代码,在被保存为文本文件(不论后缀是.c/.h/.hpp/.cc/….)时,在计算机看来这只是一串ASCII/UTF-8/xxx编码,更进一步,则是一个二进制序列。

编译型语言需要经过预处理-编译-汇编-链接的过程才能转化为CPU可识别的机器代码。

image.png

编译型语言的一般编译过程;此处以GCC工具链为例

CPU能够执行的特殊二进制机器码的集合被称作指令集,一条特定的指令对应着固化在硬件中的操作,比如加载特定内存地址的内容到寄存器,求两个寄存器中的数据之和等。

指令集高度依赖于CPU平台(x86/amd64/ia32/arm)以及特定的芯片设计。同一段C源代码在x86-64架构下的windows系统编译的结果和arm64架构下的macos系统是几乎不同的。

我们以下面的C代码为例,一一揭晓编译的全过程:

#include <stdio.h>
#define TEST 1
// a simple test program
int main()
{
    int num = 0;
    printf("input your num:\\n");
    scanf("%d", &num);
    for (int i = 0; i < num; ++i)
    {
        printf("hello world!\\n");
    }
    return num - TEST;
}

预处理

这是“编译”的第一个阶段,由C/C++预处理器执行。在这里,以#开头的预处理指令会被识别,所有宏#define XXX会被展开为对应的替换字符串。预处理器还会识别注释以及多余的空格和回车,将它们删除。最后,给每一行代码行首添加行号。

预处理器实际上是一个字符串分析工具,他找到特定的字符序列然后进行替换和删除。常见的预处理指令可以查看: 常用宏

<aside> 💡 在C++中,已经不再推荐使用宏来定义常量,constexpr是更好的选择;而宏函数是C语法特性的限制导致的特定时期产物,在C++里应该减少甚至避免使用,using关键字是一个更好的替代。

</aside>

C/C++还提供了一些预定义的宏,你可以利用它们进行调试或者记录编译日志,但你不可以通过#define重新修改它们的值: