C++ 面向对象特征

封装 · 继承 · 多态 · 抽象

类与对象基础

类是 C++ 中实现面向对象编程的核心机制,它是用户自定义的数据类型,用于描述对象的属性(数据成员)和行为(成员函数)。对象是类的实例。

类的定义

类通常包含数据成员和成员函数,使用关键字 class 定义。访问修饰符(如 `public`, `private`)控制成员的可见性。

// 定义一个 Person 类
class Person {
private: // 私有成员,只能在类内部访问
    std::string name;
    int age;

public: // 公有成员,可以在类外部访问
    // 构造函数:用于初始化对象
    Person(std::string n, int a) : name(n), age(a) {
        std::cout << "Person 对象被创建: " << name << std::endl;
    }

    // 析构函数:在对象销毁时调用
    ~Person() {
        std::cout << "Person 对象被销毁: " << name << std::endl;
    }

    // 成员函数(方法)
    void sayHello() {
        std::cout << "你好,我是 " << name << ",今年 " << age << " 岁。" << std::endl;
    }

    // 获取年龄的方法 (Getter)
    int getAge() const { // const 表示此方法不修改对象状态
        return age;
    }
};

内存中的对象:

Person p1 ("张三", 25);
p1: { name: "张三", age: 25 }
p1.sayHello(); 👉 输出问候语

封装与访问控制

封装是将数据(属性)和操作数据的方法(函数)捆绑在一起,并对数据的访问进行限制。C++通过访问修饰符 `public`, `protected`, `private` 实现封装。

public: (公共)

任何地方都可以访问(类内部、派生类、类外部)。通常用于类的接口。

void setAge(int a); int getAge() const;

protected: (受保护)

只能被该类的成员函数和其派生类的成员函数访问。

std::string internalID;

private: (私有)

只能被该类的成员函数访问。提供最高级别的数据保护。

double salary; void calculateBonus();

封装的优点

  • 数据隐藏:保护内部状态不被随意修改。
  • 接口清晰:使用者只需关心 `public` 接口,无需了解内部实现。
  • 代码模块化:提高代码的可维护性、可重用性和灵活性。
  • 安全性增强:控制对敏感数据的访问。

继承与派生

继承允许一个类(派生类/子类)获得另一个类(基类/父类)的属性和方法。这促进了代码重用,并建立了类之间的 "is-a" 关系(例如,`Student` is a `Person`)。

基类 (Person)
public: string name; public: void sayHello(); protected: int age;
派生类 (Student)
// 继承自 Person
public: string studentID; public: void study(); // 可以访问 Person 的 public 和 protected 成员

继承的类型

// 1. 公有继承 (public inheritance): is-a 关系
// 基类的 public -> 派生类的 public
// 基类的 protected -> 派生类的 protected
// 基类的 private -> 派生类不可访问
class Student : public Person { /* ... */ };

// 2. 保护继承 (protected inheritance):
// 基类的 public, protected -> 派生类的 protected
// 基类的 private -> 派生类不可访问
class SpecialStudent : protected Person { /* ... */ };

// 3. 私有继承 (private inheritance): has-a / implemented-in-terms-of
// 基类的 public, protected -> 派生类的 private
// 基类的 private -> 派生类不可访问
class Worker : private Person { /* ... */ };

// 4. 多重继承 (multiple inheritance):
// 一个类可以从多个基类继承
class TeachingAssistant : public Student, public Worker { /* ... */ };
// 注意:可能导致菱形继承问题 (Dreaded Diamond Problem)

// 5. 虚继承 (virtual inheritance):
// 用于解决菱形继承问题,确保共享基类只有一个实例
class A { public: int val; };
class B : virtual public A { /* ... */ };
class C : virtual public A { /* ... */ };
class D : public B, public C { /* D 中只有一份 A::val */ };

多态与虚函数

多态(Polymorphism)意为“多种形态”,允许我们使用基类的指针或引用来调用派生类中重写的方法。C++ 主要通过 **虚函数 (virtual functions)** 和 **动态绑定 (dynamic binding)** 来实现运行时多态。

Shape (基类) 虚函数表 (vtable)
virtual draw() Shape::draw
virtual area() Shape::area
~Shape() Shape::~Shape
Circle (派生类) 虚函数表 (vtable)
virtual draw() Circle::draw
virtual area() Circle::area
~Shape() Circle::~Shape
Shape* ptr;
ptr = new Circle(); // 基类指针指向派生类对象
ptr->draw(); // 调用哪个 draw() ?

点击按钮查看效果

虚函数与动态绑定

  • 在基类中用 `virtual` 关键字声明的函数称为虚函数。
  • 派生类可以**重写 (override)** 基类的虚函数(函数签名需完全一致)。建议使用 `override` 关键字明确意图并进行编译时检查。
  • 当通过基类指针或引用调用虚函数时,程序会在**运行时**根据指针实际指向的对象类型来决定调用哪个版本的函数(基类或派生类的版本)。这就是**动态绑定**或**晚期绑定**。
  • 实现原理:编译器为每个包含虚函数的类创建一个**虚函数表 (vtable)**,存储虚函数的地址。每个对象实例包含一个**虚函数指针 (vptr)**,指向其所属类的 vtable。调用时通过 vptr 查找 vtable 来确定正确的函数地址。
  • **虚析构函数**:如果基类指针可能指向派生类对象,并且需要删除该对象,基类的析构函数**必须**声明为 `virtual`,以确保派生类的析构函数能被正确调用,防止内存泄漏。

抽象与接口

抽象是关注对象的关键特征而忽略次要细节的过程。在 C++ 中,抽象通常通过**抽象类 (Abstract Classes)** 和**纯虚函数 (Pure Virtual Functions)** 来实现,用于定义接口规范。

抽象基类 (接口) - IShape

包含至少一个纯虚函数的类是抽象类。抽象类**不能被实例化**,它定义了一个契约,要求派生类必须实现这些纯虚函数。

// 定义图形接口
class IShape {
public:
    // 纯虚函数 (Pure Virtual Function)
    // 没有函数体,赋值为 0
    virtual void draw() const = 0;
    virtual double area() const = 0;

    // 虚析构函数(好习惯,即使是空的)
    // 确保通过基类指针删除派生类对象时,派生类的析构函数被调用
    virtual ~IShape() { std::cout << "IShape destroyed\n"; }
};
具体实现类 - Circle

派生类必须实现基类中所有的纯虚函数,才能成为具体类并被实例化。

#include  // For M_PI

class Circle : public IShape {
private:
    double radius;
    std::string color;

public:
    Circle(double r, std::string c) : radius(r), color(c) {}

    // 实现纯虚函数 draw()
    void draw() const override {
        std::cout << "Drawing a " << color << " circle with radius " << radius << std::endl;
    }

    // 实现纯虚函数 area()
    double area() const override {
        return M_PI * radius * radius;
    }

    ~Circle() override { std::cout << "Circle destroyed\n"; }
};

练习时间:选择题

正在加载题目...

练习时间:编程练习

尝试根据描述思考如何实现,然后点击查看参考答案。

练习 1: Student 继承与多态

假设已有 `Person` 类(包含 `name`, `age` 和虚函数 `virtual void printInfo()`)。请定义一个 `Student` 类,继承自 `Person`,添加 `studentID` 成员。重写 `printInfo()` 方法,使其能打印学生特有的信息(包括学号)。编写测试代码,使用 `Person*` 指针指向 `Student` 对象,并调用 `printInfo()` 来验证多态。

练习 2: Logger 抽象类

设计一个抽象基类 `Logger`,它包含一个纯虚函数 `virtual void log(const std::string& message) = 0;`。然后,实现两个具体的日志记录器类:`ConsoleLogger`(将消息打印到控制台)和 `FileLogger`(将消息写入指定文件)。编写测试代码,演示如何使用 `Logger*` 指针来调用不同日志记录器的 `log` 方法。

注意事项与陷阱

在使用 C++ 面向对象特性时,需要留意一些常见问题:

对象切片 (Object Slicing)

当把派生类对象**按值**赋给基类对象或按值传递给期望基类对象的函数时,派生类特有的部分(成员变量和方法)会被“切掉”,只保留基类部分。这会丢失多态行为。

class Base { public: virtual void func() { /* ... */ } };
class Derived : public Base { public: int extra; void func() override { /* ... */ } };

Derived d;
Base b = d; // 对象切片发生!b 只包含 Base 部分,d.extra 被丢失
b.func();   // 调用的是 Base::func(),而不是 Derived::func()

// 正确做法:使用指针或引用
Base* b_ptr = &d;
Base& b_ref = d;
b_ptr->func(); // 正确调用 Derived::func()
b_ref.func(); // 正确调用 Derived::func()

避免方法: 始终通过基类的指针或引用来操作派生类对象,以保持多态性。

访问控制与继承

继承类型 (`public`, `protected`, `private`) 会影响基类成员在派生类中的访问权限。`private` 成员永远不能被派生类直接访问。选择合适的继承类型很重要。

使用 `using` 声明可以在 `protected` 或 `private` 继承时,有选择地将基类的某些 `public` 或 `protected` 成员提升到派生类的 `public` 或 `protected` 域中。

菱形继承与虚继承

多重继承可能导致“菱形继承”问题:一个类通过两条或多条路径继承自同一个间接基类,导致该基类的成员在最终派生类中出现多份副本,引发二义性。

解决方案: 在中间基类继承共同的间接基类时使用 `virtual` 关键字进行**虚继承**。这确保最终派生类中只有一份共享基类的实例。

class A { /* ... */ };
class B : virtual public A { /* ... */ }; // B 虚继承 A
class C : virtual public A { /* ... */ }; // C 虚继承 A
class D : public B, public C { /* ... */ }; // D 中只有一份 A 的子对象

虚继承会带来一些额外的运行时开销(通常通过虚基类表指针实现),应在确实需要解决菱形继承问题时使用。

知识延伸

掌握了 OOP 的基础特征后,可以进一步探索以下相关领域:

SOLID 设计原则

一套面向对象设计的指导原则,旨在使软件设计更易于理解、灵活和维护:

  • Single Responsibility Principle (单一职责原则)
  • Open/Closed Principle (开闭原则)
  • Liskov Substitution Principle (里氏替换原则)
  • Interface Segregation Principle (接口隔离原则)
  • Dependency Inversion Principle (依赖倒置原则)

遵循 SOLID 原则有助于编写高质量的面向对象代码。

设计模式 (Design Patterns)

在特定情境下解决常见软件设计问题的可复用解决方案。许多设计模式都基于 OOP 特征实现:

  • 工厂模式 (Factory Pattern): 利用封装和继承/多态来创建对象,隐藏实例化逻辑。
  • 策略模式 (Strategy Pattern): 定义一系列算法(封装),让它们可以互换(多态),使算法独立于使用它的客户端。
  • 观察者模式 (Observer Pattern): 定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖者(观察者)都会收到通知并自动更新(利用接口/抽象类)。
  • 模板方法模式 (Template Method Pattern): 在基类中定义一个算法的骨架(抽象),将某些步骤延迟到子类中实现(继承/多态)。

UML 类图 (Class Diagrams)

统一建模语言 (UML) 中的一种图形表示法,用于描述系统的静态结构,包括类、接口、它们的属性、方法以及它们之间的关系(如继承、关联、聚合、组合)。学习阅读和绘制类图有助于理解和设计复杂的面向对象系统。

类图能清晰地展示封装、继承、关联等关系,是 OOP 设计沟通的重要工具。

总结:C++ 面向对象四大特征

本次学习我们回顾了 C++ 面向对象编程的核心概念:

📦 封装 (Encapsulation)

将数据和操作数据的方法捆绑在一起,并通过访问修饰符 (`public`, `protected`, `private`) 控制访问权限,实现数据隐藏和接口分离。

🔗 继承 (Inheritance)

允许一个类(子类)继承另一个类(父类)的属性和方法,实现代码重用和类层次结构。"is-a" 关系。注意继承类型和菱形继承问题。

🎭 多态 (Polymorphism)

“多种形态”,允许使用基类指针/引用调用派生类重写的方法。主要通过虚函数和动态绑定实现运行时多态。注意对象切片和虚析构函数。

📐 抽象 (Abstraction)

关注对象的关键特征,忽略次要细节。通过抽象类和纯虚函数定义接口规范,强制派生类实现特定功能,实现 “接口与实现分离”。