简述 C++ 应用是如何构建的

C++ 应用的构建一般分为四个步骤:

  1. 预处理(Preprocessing)
  2. 编译(Compiling)
  3. 汇编(Assembling)
  4. 链接(Linking)

预处理

预处理源文件,为编译做准备,文件内容的本质没有变,还都是源文件。这些处理包括:删除注释、处理宏定义、处理编译指令(如条件编译),处理 #include 指令等。

// 将 xxx.h 里面的内容复制到当前位置
#include "xxx.h"

// 宏定义,预处理时将本文件中的所有 INTEGER 替换为 int
#define INTEGER int

// 条件处理,1 为真,add 函数将会被保留
#if 1
int add(int a, int b) {
    return a + b;
}
#endif

// 条件处理,0 为假,mul 函数将会被移除
#if 0
int mul(int a, int b) {
    return a * b;
}
#endif

预处理命令:cpp source.cpp -o source.i

预处理后的文件以 .i 结尾。

编译

将源代码翻译成汇编代码,期间检查语法错误和类型错误。

编译命令:g++ -S -masm=intel source.i-masm 指定目标架构。

也可以直接指定 source.cpp 文件,完成预处理 + 编译。

编译后的文件以 .s 结尾。

汇编

将汇编代码翻译成机器语言。

汇编命令:g++ -c source.s

也可以直接指定 source.cpp 文件,完成预处理 + 编译 + 汇编。

汇编后的文件以 .o 结尾,被称为目标文件。

链接

将目标文件与库文件链接,形成可执行文件。

链接器的一个重要工作是寻找函数的定义(和函数声明相同的函数定义)。

链接错误 1:没找到该函数定义

log.cpp

#include <iostream>

// Log 函数定义
void Log(const char* msg) {
    std:cout << msg << std::endl;
}

main.cpp

// Log 函数声明
void Log(const char* msg);

int main() {
    // 函数调用
    Log("Hello world");
}

main.cpp 调用了 log.cpp 的 log 函数,只要 main.cpp 中有 log 函数的声明,那么编译阶段就不会报错。但是链接阶段会去找 log 函数的定义,因为它在 main.cpp 中被调用了,如果找不到,则会报链接错误(g++ 中会报错 undefined reference to Log(char const*),vs 中会报错 unresolved external symbol.)。如果把 main.cpp 中的 log 函数调用注释掉,那么就能通过链接,因为 main.cpp 并没有调用 log 函数。

但如果是间接引用呢?

main.cpp

void Log(const char* msg);

int mul(int a, int b) {
    Log("Hello");
    return a * b;
}

int main() {
    // 注释掉
    // mul(1, 2);
}

链接会报错,因为 mul 虽然没在当前文件使用,但是有可能会在其他文件使用,因此链接器还是需要将 Log 和 mul 链接在一起,如果找不到 Log 函数的定义,则会报错。

链接错误 2:函数定义重复

如果一个文件中有两个相同的函数定义,那么编译阶段就会报错。如果将其拆到不同的文件中就不会报错,编译完美通过,但是链接阶段会报错,因为有重复的定义,链接器不知道该链接哪个函数。

所以你可能会想,将 Log 函数定义放到头文件不就行了,其他文件 include 这个头文件。这样也会报重复定义的错误,这涉及到 include 的原理,include 会将引用的文件原封不动的复制到当前文件,这样多个文件都有一个 Log 的函数定义,当然还会报错。

有两种解决方法:

#include <iostream>

// 重复定义
// void Log(const char* msg) {

// 静态,表示这个定义只在当前文件使用,即便被复制了多份,也是一个文件一份,各用各的
// static void Log(const char* msg) {

// 内联,表示用代码定义的函数体替换掉函数调用,也就是说消除了函数,没函数什么事了
inline void Log(const char* msg) {
    std::cout << msg << std::endl;
}

还有一个更好的解决办法,那就是头文件只声明,不定义,然后把定义放在它该在的地方。如 log.h 中只声明 Log 函数,log.cpp 中才定义 Log 函数。main.cpp 只需 #include "log.h" 通过编译,链接时去 log.cpp 找函数声明。这样 Log 函数全项目独一份。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注