模块接口划分的核心是控制可见性:用抽象接口切断头文件依赖链,配合cmake的interface/private目录设置和pimpl惯用法,确保实现细节不泄露,从而降低编译耦合。

直接说结论:模块接口划分的核心不是“怎么分”,而是“让谁看不到什么”——用抽象接口切断头文件依赖链,配合 CMake 的 target_include_directories 严格控制可见范围,才能真正降低编译耦合。
为什么改一个 .h 文件会导致全量重编?
根本原因不是代码多,而是头文件包含关系失控。比如 network_module.h 里直接 #include "json_parser.h",而 json_parser.h 又依赖 memory_pool.h 和 logging.h……调用方哪怕只用了一个 send_request() 函数,也会被拖进整个依赖树。
- 所有实现细节(如内部类、私有成员、第三方库类型)不能出现在模块的 public 头文件里
- 用
pimpl惯用法把实现细节藏在.cpp文件中,public 头只暴露最小接口 - 避免在接口头里
#include其他模块的具体实现头,改用前向声明(class JsonParser;)+ 指针/引用传递
怎么定义一个真正可解耦的模块接口?
接口不是“把函数列出来”,而是要能独立编译、独立测试、不随实现变而变。关键看三点:是否纯虚、是否无状态、是否不泄露实现类型。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
- 用纯虚类定义接口,比如
class IDataSink { public: virtual ~IDataSink() = default; virtual void write(const std::string&) = 0; }; - 接口函数参数和返回值尽量用标准类型(
std::string,int,std::unique_ptr<t></t>),避免传入JsonParserImpl*这类具体类型 - 工厂函数(如
std::unique_ptr<idatasink> create_file_sink(const std::string&)</idatasink>)必须定义在实现模块的.cpp中,接口头里只声明,不实现
CMake 配置里哪些设置会悄悄破坏模块隔离?
很多项目写了 add_library(my_module),但没管 target_include_directories 的 PUBLIC/PRIVATE 语义,结果还是把实现路径暴露给了依赖方。
-
target_include_directories(my_module PUBLIC include/)→ 调用方会自动获得这个路径,危险 - 正确做法是:
target_include_directories(my_module INTERFACE include/)(只暴露头文件) +PRIVATE src/(实现路径仅本模块可见) - 对第三方依赖(如
boost或protobuf),务必用PRIVATE引入,否则下游模块会被迫链接或编译这些库 - 检查最终生成的
compile_commands.json,确认调用方的编译命令里没有出现不该有的头文件路径
什么时候该用动态库而不是静态库?
不是“功能多就该用 so/dll”,而是当模块变更频率远高于调用方,且你无法控制调用方重新编译节奏时,才需要运行时链接。
- 典型场景:日志模块、配置加载器、硬件抽象层(HAL)——这些模块可能由不同团队维护,或需热更新
- 静态库(
STATIC)适合稳定基础模块(如内存池、线程工具),但必须确保其头文件完全不暴露实现细节,否则仍会引发重编 - 动态库导出符号要显式控制,Linux 用
__attribute__((visibility("default"))),Windows 用__declspec(dllexport),避免把内部符号泄漏出去
最常被忽略的一点:模块接口的稳定性不取决于你写得多严谨,而取决于你有没有在 CI 中跑“头文件 ABI 兼容性检查”。一次不小心给纯虚类加了新函数,或者改了虚函数顺序,下游所有链接该模块的二进制都会崩溃——这种问题不会在编译时报错,只会在线上静默失败。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










