ABI兼容性需确保二进制层面不变:锁死public类大小与成员布局,避免非POD类型、慎改虚函数表,精确控制符号导出,模板/内联/模块需严格约束。

只要公开头文件里的声明(函数签名、类布局、枚举值、宏定义)不改,用户代码就能照常编译链接——但 ABI 兼容性不是靠“能编译”保证的,得看二进制层面是否真没动。
别碰 public 类的内存布局
这是最常踩的坑:加个 int 成员、换成员顺序、改基类继承方式,都会让 sizeof 或成员偏移变化,调用方读写地址就错乱。
- 用
static_assert(sizeof(MyClass) == 16, "ABI break")在头文件里锁死大小 - 所有 public 成员必须保持原有顺序和类型;新增字段一律塞到私有实现里(比如
PImpl的impl对象内部) - 避免把
std::string、std::vector这类非 POD 类型直接放在 public 类里——它们的 ABI 在不同标准库版本间不保证一致
虚函数表不能删、不能挪、不能重载覆盖
哪怕只是把 virtual void f() final 改成 virtual void f() = 0,vtable 布局就变了,老二进制调用会跳到错误地址。
- 已有虚函数签名(含 const/volatile/override/final)绝不可修改
- 新增虚函数只能追加在最后;若需扩展接口,优先用非虚函数 +
std::function回调或策略对象 - 慎用
final:它虽能阻止派生,但一旦去掉,子类 vtable 就可能错位
导出符号必须稳定,连 name mangling 都要可控
Linux/macOS 默认导出所有全局符号,Windows 用 __declspec(dllexport),但模板、内联函数、静态成员一旦被头文件暴露,就可能意外导出并绑定到具体实现。
立即学习“C++免费学习笔记(深入)”;
- 显式控制导出:用
extern "C"封装 C 接口,或用__attribute__((visibility("default")))/__declspec(dllexport)精确标记 - 禁止在头文件里定义模板函数(除非你打算导出所有实例化体);模板实现全放 .cpp 里,或用显式实例化声明
- 内联函数必须确保逻辑不变——哪怕只改一行注释,某些编译器也可能生成新符号
C++27 模块下更得盯紧 export 边界
模块接口单元(.ixx)不是头文件替代品,它是 ABI 契约的正式载体。export 出去的东西,编译器会生成带指纹的 .mxi 文件,任何变更都会触发链接时校验失败。
-
export只能修饰声明,不能修饰定义;函数体必须在 module implementation unit 里 - 模块内联函数(
export inline int f() { return 42; })一旦修改,所有导入该模块的 TU 都得重编译——这不是 ABI 问题,是构建一致性问题 - 跨模块类型别名(如
export using handle_t = std::shared_ptr<impl>;</impl>)要小心:如果Impl是非导出类型,这个别名实际无法安全使用
真正难的不是“怎么改实现”,而是“怎么证明没改坏 ABI”。每次发布前跑 abi-dumper + abi-compliance-checker 对比前后符号表,比靠经验靠谱得多。


















