面向对象(OOP)总览
1. 封装(Encapsulation)
- 数据 + 行为绑定
- 将数据成员和对这些数据的操作(成员函数)放在同一个类里,让“能做什么”和“对谁做”靠近。
- 这样使用者不需要直接操作裸数据,而是通过类提供的接口安全地修改状态。
- 访问控制:
public/protected/private- 用访问控制符隐藏不应该被外部随意访问的细节,只暴露稳定的 API。
- 典型做法:数据成员设为
private,只通过public方法读写。
- 不变量维护(class invariant)
- 封装的一个核心目的就是:把维持不变量的责任锁在类内部。
- 例如:
size_与data_的关系、某些字段必须成对出现等,都由构造/成员函数保证。
- 接口 vs 实现分离
- 头文件只暴露“能做什么”(函数签名、类型),实现细节藏在
.cpp中。 - 方便重构内部实现而不破坏调用方(binary compatibility / source compatibility)。
- 头文件只暴露“能做什么”(函数签名、类型),实现细节藏在
2. 抽象(Abstraction)
- 提供“做什么”,隐藏“怎么做”
- 抽象强调的是对外只暴露“能力/语义”,不暴露具体算法/数据结构。
- 比如:
Logger只承诺log(string),至于是写文件、发网络还是写内存缓冲,调用方不关心。
- 接口设计(API / abstract class)
- 抽象往往通过“接口”来表达:函数、纯虚基类、concept 等。
- 好的抽象应该清晰、最小、稳定,避免把实现细节泄露到接口上。
- 关注语义,而不是实现细节
- 思考“这个类型在领域里的含义是什么”,而不是先纠结于具体容器/算法选择。
- 有意识地区分“公共语义层”(interface)和“具体实现层”(implementation)。
3. 继承(Inheritance)
- 代码复用 + 结构层级
- 继承允许把公共行为和状态提取到基类中,派生类共享实现。
- 同时为类型系统引入层级结构(
Base/Derived)方便建模。
- “is-a” 关系
- 正确的继承语义通常可以读作“
Derivedis-aBase”。 - 若不能自然地读出 is-a 关系,往往说明不应该用继承(应考虑组合)。
- 正确的继承语义通常可以读作“
- 单继承 / 多继承
- C++ 支持多继承,但实际工程中多继承容易带来复杂性(菱形继承、歧义)。
- 常见实践:接口型多继承(多个纯虚基类) + 实现只从一个具体基类继承。
- 接口继承 vs 实现继承
- 接口继承:只继承“函数签名/契约”,不继承实现(纯虚基类)。
- 实现继承:子类直接使用并依赖基类实现,耦合更强。
- 一般推荐:接口继承多于实现继承,避免把过多实现细节固化在基类里。
4. 多态(Polymorphism)
- 同一接口,不同行为
- 调用方只知道“这个对象支持某个接口”,具体行为由对象的实际类型决定。
- 例如:
shape.draw()可以画圆也可以画矩形,取决于shape的实际类型。
- 静态多态(模板/重载)
- 编译期根据模板实参/重载决议选择具体实现,调用在编译期就定死。
- 典型:模板函数、函数重载、CRTP、
std::variant。 - 优点:零运行时开销、易于优化;缺点:类型集合固定、接口约定隐式。
- 动态多态(virtual / override)
- 通过基类指针/引用 + 虚函数实现,运行期根据 vptr/vtable 选择具体实现。
- 优点:接口稳定、支持运行期扩展;缺点:每次调用有间接跳转,优化空间有限。
多态是“抽象 + 封装 + 继承”的自然结果:
抽象定义接口,封装内部状态,继承复用/扩展行为,多态决定“在什么时候、如何选择具体实现”。
5. 对象生命周期(Object Lifecycle)
- 构造 / 析构
- 构造函数负责把对象初始化到一个“有效状态”,析构函数负责释放资源。
- 在 C++ 里,构造/析构和异常/跳出作用域有严格的语义,是 RAII 的基础。
- 拷贝 / 移动语义
- 拷贝语义:
T(const T&)/T& operator=(const T&),表示“按值复制”的行为。 - 移动语义:
T(T&&)/T& operator=(T&&),优化资源转移,避免不必要的拷贝。 - 好的类应明确自己的拷贝/移动策略(可拷贝、仅移动、不可拷贝等)。
- 拷贝语义:
- RAII(资源绑定生命周期)
- Resource Acquisition Is Initialization:资源在对象构造时获取,在析构时自动释放。
- 典型例子:
std::unique_ptr、std::lock_guard、文件句柄包装类等。 - RAII 把“资源释放”这件事从逻辑流程里剥离出去,避免忘记释放/异常漏释放。
6. 组合 vs 继承(Composition)
- “has-a” 关系
- 组合表示“某对象拥有另一个对象作为组成部分”,而不是“是一个”。
- 例如:
Carhas-aEngine,Orderhas-aOrderLine列表。
- 组合优于继承(解耦、灵活性更高)
- 继承强耦合:子类紧绑在父类结构和生命周期之上。
- 组合弱耦合:只依赖另一个类型的接口,不依赖其继承层次和内部实现。
- 常见建议:优先用组合表达复用,只有在真正存在 is-a 关系时才用继承。
- 依赖注入(DI)
- 通过构造函数/Setter 把依赖对象注入到类中,而不是在类内部自己 new。
- 好处:容易替换实现(测试 double、mock)、解耦组件、提高可测试性。
7. 接口与设计原则
- SOLID:
- 单一职责(SRP):一个类只负责一组单一、紧密相关的功能。
- 开闭原则(OCP):对扩展开放,对修改关闭;通过新增子类/组合扩展行为,而尽量不改老代码。
- 里氏替换(LSP):任何使用基类的地方都应可以替换为子类,而不破坏语义。
- 接口隔离(ISP):倾向于多个小接口,而不是一个大而全的接口,让实现类只依赖自己真正需要的部分。
- 依赖倒置(DIP):高层模块不依赖低层细节,二者都依赖抽象;抽象不依赖细节,细节依赖抽象。
- 面向接口编程
- 调用方只依赖抽象接口,而非具体实现类。
- 这样可以方便地在不影响调用代码的前提下替换具体实现。
8. 类型系统相关
- 向上转型 / 向下转型(cast)
- 向上转型:
Derived*→Base*,通常是安全的(is-a 关系)。 - 向下转型:
Base*→Derived*,需要小心(可用dynamic_cast检查)。 - 尽量减少强制的向下转型,过多的向下转型往往说明抽象设计有问题。
- 向上转型:
- 动态类型 vs 静态类型
- C++ 静态类型为主,但也有动态信息(RTTI、
std::any、std::variant等)。 - 静态类型在编译期提供更多检查与优化,动态信息则在必要时提供“自省/类型擦除”的能力。
- C++ 静态类型为主,但也有动态信息(RTTI、
- RTTI(
typeid/dynamic_cast)- 允许在运行时检查对象的真实类型、做安全的向下转型。
- 过度依赖 RTTI 往往是设计味道,优先考虑通过虚函数/多态/访问者模式等来表达分派逻辑。
9. 设计模式(应用层)
- 创建型(Factory, Singleton)
- Factory:把对象创建逻辑集中到工厂中,调用方只关心接口。
- Singleton:限制某个类型全局只存在一个实例(注意多线程与生命周期问题)。
- 结构型(Adapter, Decorator)
- Adapter:把一个接口转换为调用方期望的另一个接口(接口适配)。
- Decorator:在不修改原类的前提下为其动态地添加职责(包装一层)。
- 行为型(Strategy, Observer)
- Strategy:把算法封装为对象,可在运行时自由替换策略实现。
- Observer:发布-订阅模式,主体状态变化时通知一组观察者。
设计模式本质上是:在面向对象和类型系统约束下,对常见问题的一组命名解法。
理解封装/抽象/继承/多态这些基础,再看模式会更容易“看穿招式背后的工夫”。