从源码到进程:01.程序的预处理

前言

在这个系列中,我们准备从 源码到进程 这个角度去分析,在 Linux 平台下,一个项目的文件,究竟是怎么一步步变成可执行文件,然后最终变成一个进程的。

开篇我们从最简单、大家最熟悉的一个阶段说起:预处理

从编译四步走说起

一个广为认知的理论是:

预处理 → 编译 → 汇编 → 链接

这是 C++ 程序变成可执行文件的四个主要阶段。

其中,预处理(Preprocessing)是 C++ 编译的第一个阶段。编译器(如 gcc/clang)会先将整个项目组织起来,进行文本级的处理,生成一份“纯净”的中间代码(.i 文件)。

在预处理中,主要进行了三件事:

  1. 宏替换:简单的文本替换。
  2. 文件包含:头文件的展开。
  3. 条件宏处理:条件编译,避免头文件循环包含,支持跨平台代码。

通过预处理,我们可以实现:

  • 跨平台的项目开发(条件编译)
  • 避免头文件重复包含
  • 简单的符号替换和调试增强

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 展开、把宏替换成实际代码,生成一份可编译的源码。

  • 在工程实践中,它常被用于:

    1. 头文件保护(避免重复定义)
    2. 跨平台支持(条件编译)
    3. 符号控制(导出/内联)
  • 在大型项目(如 spdlog、Linux 内核)中,预处理器是支撑跨平台和可配置性的关键机制。

posted @ 2025-09-19 07:43  ToBrightmoon  阅读(16)  评论(0)    收藏  举报

© ToBrightmoon. All Rights Reserved.

Powered by Cnblogs & Designed with ❤️ by Gemini.

湘ICP备XXXXXXXX号-X