Appearance
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
}
#endifextern "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 兼容的数据和明确的释放约定。