从源码到进程:01.程序的预处理
前言
在这个系列中,我们准备从 源码到进程 这个角度去分析,在 Linux 平台下,一个项目的文件,究竟是怎么一步步变成可执行文件,然后最终变成一个进程的。
开篇我们从最简单、大家最熟悉的一个阶段说起:预处理。
从编译四步走说起
一个广为认知的理论是:
预处理 → 编译 → 汇编 → 链接
这是 C++ 程序变成可执行文件的四个主要阶段。
其中,预处理(Preprocessing)是 C++ 编译的第一个阶段。编译器(如 gcc/clang)会先将整个项目组织起来,进行文本级的处理,生成一份“纯净”的中间代码(.i 文件)。
在预处理中,主要进行了三件事:
- 宏替换:简单的文本替换。
- 文件包含:头文件的展开。
- 条件宏处理:条件编译,避免头文件循环包含,支持跨平台代码。
通过预处理,我们可以实现:
- 跨平台的项目开发(条件编译)
- 避免头文件重复包含
- 简单的符号替换和调试增强
GCC 官方文档对预处理器的完整说明在这里:
https://gcc.gnu.org/onlinedocs/cpp/
你可以查阅其中关于宏、条件编译、头文件包含的更详细介绍。
一个简单的例子
先来看一个演示预处理作用的例子。
preprocess_demo.h
#ifndef PREPROCESS_DEMO_H
#define PREPROCESS_DEMO_H
#include <cstdio>
// 条件编译:根据平台选择不同实现
#ifdef _WIN32
#define PLATFORM "Windows"
#else
#define PLATFORM "Linux/Unix"
#endif
// 宏:自动带上源码位置的日志
#define LOG(fmt, ...) \
std::fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#endif // PREPROCESS_DEMO_H
preprocess_demo.cpp
#include "preprocess_demo.h"
int main() {
LOG("Hello from %s", PLATFORM);
LOG("Value test: %d", 42);
return 0;
}
实验:运行预处理
使用 gcc 只做预处理,不进行编译:
g++ -E preprocess_demo.cpp -o preprocess_demo.i
然后我们打开 preprocess_demo.i 文件,就能看到:
#include <cstdio>已经被完整展开(几千行内容)。
![preprocess_1]()
PLATFORM宏被替换成"Linux/Unix"。
![preprocess_2]()
LOG宏展开成了fprintf调用,自动带上了__FILE__和__LINE__。
![preprocess_3]()
这就是预处理的实际效果:把工程组织好,变成一份没有预处理指令的纯 C++ 源码。
在实际工程中的应用:spdlog
在实际开发中,预处理发挥着重要作用。我们以开源日志库 spdlog 为例。
条件编译
在 os-inl.h 中定义了大量的时间与路径的操作函数,为了跨平台,可以看到大量针对不同操作系统的条件编译:
#ifdef _WIN32
#include <spdlog/details/windows_include.h>
#include <fileapi.h> // for FlushFileBuffers
#include <io.h> // for _get_osfhandle, _isatty, _fileno
#include <process.h> // for _get_pid
#ifdef __MINGW32__
#include <share.h>
#endif
#if defined(SPDLOG_WCHAR_TO_UTF8_SUPPORT) || defined(SPDLOG_WCHAR_FILENAMES)
#include <cassert>
#include <limits>
#endif
#include <direct.h> // for _mkdir/_wmkdir
#else // unix
#include <fcntl.h>
#include <unistd.h>
#ifdef __linux__
#include <sys/syscall.h> //Use gettid() syscall under linux to get thread id
#elif defined(_AIX)
#include <pthread.h> // for pthread_getthrds_np
#elif defined(__DragonFly__) || defined(__FreeBSD__)
#include <pthread_np.h> // for pthread_getthreadid_np
#elif defined(__NetBSD__)
#include <lwp.h> // for _lwp_self
#elif defined(__sun)
#include <thread.h> // for thr_self
#endif
#endif // unix
这保证了同一份源码可以在 Windows 和 Linux 平台下编译运行。
宏控制符号
在 common.h 中,spdlog 定义了一个 SPDLOG_INLINE 宏:
......
#ifdef SPDLOG_COMPILED_LIB
#undef SPDLOG_HEADER_ONLY
#if defined(SPDLOG_SHARED_LIB)
#if defined(_WIN32)
#ifdef spdlog_EXPORTS
#define SPDLOG_API __declspec(dllexport)
#else // !spdlog_EXPORTS
#define SPDLOG_API __declspec(dllimport)
#endif
#else // !defined(_WIN32)
#define SPDLOG_API __attribute__((visibility("default")))
#endif
#else // !defined(SPDLOG_SHARED_LIB)
#define SPDLOG_API
#endif
#define SPDLOG_INLINE
#else // !defined(SPDLOG_COMPILED_LIB)
#define SPDLOG_API
#define SPDLOG_HEADER_ONLY
#define SPDLOG_INLINE inline
#endif // #ifdef SPDLOG_COMPILED_LIB
.....
- 如果是 头文件模式(header-only),那么
SPDLOG_INLINE就是inline,避免多重定义。 - 如果是 非头文件模式,
SPDLOG_INLINE就为空,由链接器处理符号。
这是预处理在工程实践中的典型用法:用宏来控制编译模式,解决符号重复定义的问题。
总结
-
预处理是 C++ 编译的第一步,主要负责宏替换、文件展开、条件裁剪。
-
在实验中我们看到,它能把
#include展开、把宏替换成实际代码,生成一份可编译的源码。 -
在工程实践中,它常被用于:
- 头文件保护(避免重复定义)
- 跨平台支持(条件编译)
- 符号控制(导出/内联)
-
在大型项目(如 spdlog、Linux 内核)中,预处理器是支撑跨平台和可配置性的关键机制。




浙公网安备 33010602011771号