Hang Zhengyang

面向对象(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” 关系
    • 正确的继承语义通常可以读作“Derived is-a Base”。
    • 若不能自然地读出 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” 关系
    • 组合表示“某对象拥有另一个对象作为组成部分”,而不是“是一个”。
    • 例如:Car has-a Engine,Order has-a OrderLine 列表。
  • 组合优于继承(解耦、灵活性更高)
    • 继承强耦合:子类紧绑在父类结构和生命周期之上。
    • 组合弱耦合:只依赖另一个类型的接口,不依赖其继承层次和内部实现。
    • 常见建议:优先用组合表达复用,只有在真正存在 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 等)。
    • 静态类型在编译期提供更多检查与优化,动态信息则在必要时提供“自省/类型擦除”的能力。
  • RTTI(typeid / dynamic_cast)
    • 允许在运行时检查对象的真实类型、做安全的向下转型。
    • 过度依赖 RTTI 往往是设计味道,优先考虑通过虚函数/多态/访问者模式等来表达分派逻辑。

9. 设计模式(应用层)

  • 创建型(Factory, Singleton)
    • Factory:把对象创建逻辑集中到工厂中,调用方只关心接口。
    • Singleton:限制某个类型全局只存在一个实例(注意多线程与生命周期问题)。
  • 结构型(Adapter, Decorator)
    • Adapter:把一个接口转换为调用方期望的另一个接口(接口适配)。
    • Decorator:在不修改原类的前提下为其动态地添加职责(包装一层)。
  • 行为型(Strategy, Observer)
    • Strategy:把算法封装为对象,可在运行时自由替换策略实现。
    • Observer:发布-订阅模式,主体状态变化时通知一组观察者。

设计模式本质上是:在面向对象和类型系统约束下,对常见问题的一组命名解法。
理解封装/抽象/继承/多态这些基础,再看模式会更容易“看穿招式背后的工夫”。