“is a”用public继承,“has a”用组合;误用会导致接口污染或丢失多态;private成员虽不可访问但仍占用内存。

“is a” 和 “has a” 的语义边界必须从设计意图出发
继承(public)表达的是“is a”关系,即子类在逻辑上是父类的一种特化;包含(成员变量)表达的是“has a”,即一个类内部持有另一个类的实例。这不是语法选择问题,而是建模决策——一旦选错,后续扩展和维护会持续踩坑。
比如 Dog 是一种 Animal,用 class Dog : public Animal;但 Car 有一个 Engine,就该写 class Car { Engine engine; };,而不是继承 Engine。
- 误用继承实现“has a”,会导致接口污染:外部代码可能误调
car.start()(如果Engine有start),而实际语义上车才该启动,引擎只是部件 - 误用包含模拟“is a”,会丢失多态能力:无法把
Dog*和Cat*统一传给接受Animal*的函数 - private 继承虽技术上能模拟“has a”,但非常规、难理解、破坏 LSP 原则,应直接用组合
public 继承时,基类 private 成员不可见但依然存在
很多人以为 private 成员“没被继承”,其实不然:private 成员仍占用派生类对象内存布局,只是编译器禁止任何访问语法(包括派生类内部)。这直接影响 sizeof 和内存对齐,但你写不出合法代码去读它。
示例中,Base 有 private int c;,Derived : public Base 的对象大小通常包含 c 占用的字节,但 Derived::show() 里写 cout 会报错 <code>error: 'int Base::c' is private within this context。
立即学习“C++免费学习笔记(深入)”;
- 调试时可在监视窗口看到该字段(如 VS 的“自动”或“局部”窗口),但它标为“不可访问”
- 序列化/ memcpy 等底层操作会复制它,但语义上你不该依赖这个行为
- 若真需要“隐藏但可复用”的数据,应改用
protected+ 文档说明,而非靠private暗示“不参与继承契约”
struct 默认 public 继承,class 默认 private 继承
这是 C++ 容易忽略的隐式规则:用 class 定义派生类时省略继承方式,等价于 private;用 struct 则默认 public。不显式写出继承方式,极易导致预期外的访问控制。
例如 class D : B { }; 中,即使 B 的 public 成员,在 D 外也无法通过 D 对象访问——因为 D 私有继承了 B,所有 B 的成员在 D 中都变成 private。
- 永远显式写
public,除非你明确需要protected或private继承(极少见) - 不要依赖
struct的默认行为来“偷懒”,团队协作中语义模糊比多敲几个字母代价高得多 - Clang-Tidy 等工具能检测隐式继承方式,建议开启
cppcoreguidelines-special-member-functions类检查项
组合比继承更灵活,且避免虚函数开销和菱形继承问题
当两个类之间没有清晰的“is a”语义,优先用组合。它天然支持运行时替换(如注入不同 Logger 实现),不引入虚表、不触发构造/析构顺序复杂性,也不卷入多继承的二义性。
比如日志模块,class Service { std::unique_ptr<logger> logger; };</logger> 比 class Service : public Logger 更合理——Service 不是一种 Logger,它只是用到了日志能力。
- 继承强制编译期绑定,组合允许运行期多态(配合指针/引用+虚函数,或模板)
- private 继承看似“安全”,实则让
dynamic_cast失效、破坏static_cast的可读性,不如直接组合+私有成员清晰 - 现代 C++ 中,
std::variant、std::any、策略类等进一步削弱了继承的必要性,组合+泛型才是主流解法
真正难的不是语法,而是每次写 : 之前,得停下来问一句:这个“关系”在业务模型里是否经得起推敲?


















