Skip to content

C++ 翻译、链接与 ABI ​

C++ 源码并不是直接变成可执行文件。理解翻译单元、符号、链接与 ABI,能解释头文件重复定义、extern、静态库/动态库和 C/C++ 互调等高频问题。

从源文件到程序 ​

  • 预处理只做文本层面的包含、宏替换和条件选择;它不理解 C++ 类型、作用域或重载。
  • 编译将一个翻译单元独立翻译为目标代码;此时可能保留“本文件引用、定义在别处”的未解析外部符号。
  • 链接把多个目标文件和库组合起来,匹配符号定义与引用,并完成地址重定位。
  • “编译失败”与“链接失败”要区分:前者常是语法、类型或模板问题;后者常是漏链接、定义缺失、重复定义或 ABI 不兼容。

头文件、声明、定义与 ODR ​

头文件通常放声明、模板定义、inline 函数定义和可被多处包含的类型定义;一个普通的非 inline 函数或变量定义若被多个翻译单元包含,链接阶段会出现重复定义。

推荐模式:

cpp
// config.hpp
#pragma once
extern int global_count;                 // 声明,不分配对象定义
void reload_config();                    // 声明
inline int square(int x) { return x * x; } // inline 定义可位于头文件

// config.cpp
#include "config.hpp"
int global_count = 0;                    // 唯一定义
  • #pragma once 很常用;传统 include guard(#ifndef / #define / #endif)也可防止同一翻译单元重复包含,但不能解决跨翻译单元的多重定义。
  • C++17 的 inline 变量可在头文件中定义:inline std::string app_name = "demo";;它们在整个程序中表示同一个实体。
  • 模板通常必须在使用点可见,因此常把完整定义放进头文件;更多细节见模板、Concepts 与 C++20。
  • #include <...> 与 #include "..." 的搜索顺序依赖编译器、构建参数和工具链配置;前者惯例用于系统/第三方头,后者惯例用于项目头,不能把某个固定搜索路径当作语言规则。

符号、名字改编与 extern "C" ​

C++ 为支持命名空间、类和函数重载,通常会对符号名进行名字改编(name mangling)。C 编译器通常不采用同一规则;因此 C++ 调用 C 库时需要指定 C 语言链接方式:

cpp
// c_api.h:既能被 C 也能被 C++ 包含
#ifdef __cplusplus
extern "C" {
#endif

int compress_buffer(const void* input, unsigned long size);

#ifdef __cplusplus
}
#endif

extern "C" 主要影响语言链接(linkage),而不是“把函数变成 C 语言函数”:

  • 它不允许 C++ 重载同名的 C 链接函数;
  • 它不会让异常、std::string、模板、类布局自动具有 C ABI;跨 C 边界应使用 C 兼容参数、错误码和清晰的所有权约定;
  • 它不等同于 extern 变量声明。extern 常用于声明外部链接实体;extern "C" 是链接规范。

静态库、共享库与运行期装载 ​

形式常见后缀何时组合代码优点代价 / 边界
静态库Linux .a、Windows .lib链接可执行文件时抽取需要的目标代码部署相对简单,不依赖运行期库搜索可执行文件更大;更新库通常需重新链接
共享库 / 动态库Linux .so、macOS .dylib、Windows .dll可执行文件保留导入信息,装载器在运行期解析可由进程共享只读代码页,可独立升级路径、版本、符号与 ABI 兼容性更复杂

共享库通常由装载器映射进进程虚拟地址空间;多个进程可以共享同一份只读代码页,但可写数据、重定位和延迟绑定细节依操作系统与 ABI 而定。相关虚拟内存基础见虚拟内存与页面置换。

ABI:二进制接口的边界 ​

API 是源码层面的调用约定;ABI(Application Binary Interface) 是已编译二进制之间的约定,通常涉及:

  • 符号名与名称改编规则;
  • 函数调用约定、参数/返回值传递;
  • 类布局、对齐、异常处理和 RTTI;
  • 标准库实现、编译器版本、编译选项与运行库。

因此,不要随意跨共享库边界传递依赖特定 C++ ABI 的对象(如 std::string、std::vector、含虚函数的类),更不要让一侧 new 而另一侧以不兼容运行库 delete。稳定插件接口常使用 C ABI、PImpl、纯抽象接口配套统一工厂/销毁函数,或序列化数据。

面试速答 ​

C++ 编译流程:源码先经预处理形成翻译单元,编译和汇编生成 .o/.obj,链接器再解析各目标文件和库之间的符号引用、执行重定位,得到可执行文件或共享库。头文件被多个源文件包含会进入多个翻译单元,因此普通函数和全局变量通常“头文件声明、一个 .cpp 定义”;模板和 inline 实体则有专门的 ODR 规则。

extern "C" 的作用:指定 C 语言链接方式,解决 C++ 名字改编与 C 库符号不匹配的问题;它不负责异常安全、对象布局或内存所有权,跨语言接口仍应使用 C 兼容的数据和明确的释放约定。

使用 Markdown 与 VitePress 构建