C++程序的完整编译过程并不是由编译器单独完成的,函数预处理器是编译器执行正式编译工作前的重要前置环节,二者共同协作将我们编写的源码转换为可运行的可执行文件。
预处理器的工作内容
预处理器是C++编译流程的第一个处理阶段,它本质上是一个文本处理工具,不会对代码的语法进行检查,只会对源码进行文本层面的替换、裁剪等操作。针对函数相关的预处理操作主要有以下几类:
- 宏定义展开:将代码中所有的宏名替换为对应的宏体内容,比如函数式的宏会被直接展开为对应的代码片段
- 头文件包含:处理
#include指令,将对应的头文件内容直接插入到当前源码的对应位置,函数声明通常就存放在这些头文件中 - 条件编译:根据
#if、#ifdef等指令判断条件,保留符合条件的代码段,剔除不符合条件的代码,实现不同场景下的函数编译控制
我们可以通过一段简单的代码观察预处理器的处理效果,假设我们有如下源码:
// 定义函数式宏
#define ADD(a,b) ((a) + (b))
#include <iostream>
int main() {
int x = 10, y = 20;
// 调用宏,预处理器会直接展开为 ((x) + (y))
int sum = ADD(x, y);
std::cout << sum << std::endl;
return 0;
}
预处理器处理后会把ADD(x,y)直接替换为((x) + (y)),同时把<iostream>头文件的内容插入到对应位置,生成一份全新的纯C++代码文本交给后续的编译器处理。
编译器的工作内容
编译器接收的是预处理器处理完成后的代码,此时代码中已经没有预处理指令,所有的宏、头文件包含都已经被处理完毕。编译器会对这份代码进行多阶段的处理:
- 词法分析:将代码拆分为一个个的 token,比如关键字、标识符、运算符、常量等,识别出函数名、函数参数、函数体等语法单元
- 语法分析:根据C++语法规则检查代码的结构是否合法,比如函数定义是否符合语法、函数调用是否匹配声明等,如果有语法错误会在这个阶段报错
- 语义分析:检查代码的语义是否合理,比如函数参数的类型是否匹配、函数返回值是否符合定义等
- 优化与目标代码生成:对合法的代码进行优化,最终生成对应平台的目标代码,也就是二进制格式的目标文件
还是以上面的代码为例,编译器处理时会先识别出main是函数名,sum是变量,std::cout是输出流对象,然后检查语法的合法性,确认没有问题后生成对应的汇编代码,再转换为目标文件。
二者的协作关系
预处理器和编译器的关系是前后衔接、分工明确的:
- 预处理器是编译器的前置依赖,编译器不会处理任何预处理指令,所有预处理相关的工作都必须在编译前由预处理器完成,如果预处理器处理出错,编译器接收到的代码就是有问题的,后续编译也会失败
- 预处理器只做文本处理,不检查语法,即使代码有语法错误,只要不涉及预处理指令的错误,预处理器也会正常输出结果,语法错误会交给编译器来发现
- 二者的处理结果是传递的,预处理器输出的代码就是编译器的输入,没有预处理器的处理,编译器无法正确处理包含宏、头文件包含等内容的源码
我们可以通过完整的编译命令来观察这个过程,在Linux环境下,我们可以使用如下命令单独执行预处理和编译步骤:
# 只执行预处理,输出预处理后的代码到test.i文件 g++ -E test.cpp -o test.i # 对预处理后的代码执行编译,生成目标文件test.o g++ -c test.i -o test.o
上面的-E参数就是告诉编译器只运行预处理器,不做后续的编译工作,-c参数则是告诉编译器只编译到目标文件阶段,不做链接。从这个过程可以很清晰地看到预处理器和编译器的先后执行顺序和协作关系。
常见误区说明
很多开发者会误以为宏定义的函数和普通的C++函数是一样的,实际上二者有本质区别:宏函数是预处理器处理的,只是简单的文本替换,没有函数的调用开销,也不会做类型检查;而普通函数是编译器处理的,有类型检查、函数调用栈等完整的函数机制。这个区别本质上就是预处理器和编译器功能差异的体现。
另外,头文件中通常存放函数的声明,很多人以为头文件是编译器处理的,实际上头文件的包含动作是预处理器完成的,编译器处理的时候已经看不到#include指令,只能看到被插入进来的函数声明内容。