面向对象程序设计

掌握C++面向对象编程——基类/派生类、虚函数与动态绑定、抽象基类、访问控制与继承——使用多态构建可扩展的类体系

学习目标

  • 能解释 C++ 多态的三大核心机制——继承建立类型层次、虚函数提供动态绑定接口、动态绑定在运行时自动选择正确版本——并说明三者如何协同工作
  • 能独立设计一个包含抽象基类和至少两个派生类的类体系,写出完整的虚函数声明(含 override/final)、虚析构、访问控制,并让代码通过编译
  • 能回答:下面代码通过基类指针 delete 派生类对象时有什么隐患?写出正确的版本。
    class Base {
    public:
        virtual void f() { /* ... */ }
    };
    class Derived : public Base {
    public:
        void f() override { /* ... */ }
    private:
        std::string *data;
    };
    int main() {
        Base *p = new Derived;
        p->f();
        delete p;
    }

机制总览

面向对象程序设计:机制路径

  1. 1

    直觉:一模多型,从一张图纸到一整条产品线

    你已经学会了设计一个类——封装数据、定义接口、管理资源。但现在遇到一个新问题:你的书店系统里有普通书籍,还有批量折扣书、限量版精装书——它们都是"书",有很多共性,但计算价格的方式各不相同。你不想写三个几乎一模一样的类,然后把 if (type == BULK) ...

  2. 2

    官方 Chapter 15 的完整继承契约

    继承体系不仅是“virtual 调到派生版本”。访问控制决定向上转换是否可见,类作用域决定候选名字能否进入重载集,构造与拷贝控制维护每层子对象,容器则必须保存多态所有者而不是发生对象切片。

  3. 3

    继承:用已有的类定义新类

    这行代码声明了三件事:① Bulk quote 是一种 Quote (IS-A 关系);② Bulk quote 自动获得了 Quote 的全部成员——数据成员和函数成员都"继承"下来了;③ public 继承意味着基类的 public 成员在派生类中仍是 public——用户代码可以通过 Bulk quote 对象直接调用继承来的 isbn() 等接口。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

面向对象程序设计:机制与证据

切换《面向对象程序设计》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 直觉:一模多型,从一张图纸到一整条产品线

你已经学会了设计一个类——封装数据、定义接口、管理资源。但现在遇到一个新问题:你的书店系统里有普通书籍,还有批量折扣书、限量版精装书——它们都是"书",有很多共性,但计算价格的方式各不相同。你不想写三个几乎一模一样的类,然后把 if (type == BULK) ...

可核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:一模多型,从一张图纸到一整条产品线」的契约。

学完《面向对象程序设计》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

面向对象程序设计:失效与核验

直觉:一模多型,从一张图纸到一整条产品线

典型失效

若把「直觉:一模多型,从一张图纸到一整条产品线」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。

核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「直觉:一模多型,从一张图纸到一整条产品线」的契约。

官方 Chapter 15 的完整继承契约

典型失效

若把「官方 Chapter 15 的完整继承契约」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。

核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「官方 Chapter 15 的完整继承契约」的契约。

继承:用已有的类定义新类

典型失效

若把「继承:用已有的类定义新类」当作孤立语法点,忽略类型约束、对象生命周期或库契约,代码即使通过编译也可能破坏不变量。

核验证据

保留编译诊断,运行本节最小示例,并用边界断言、对象计数或 sanitizer 复核「继承:用已有的类定义新类」的契约。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

直觉:一模多型,从一张图纸到一整条产品线

你已经学会了设计一个类——封装数据、定义接口、管理资源。但现在遇到一个新问题:你的书店系统里有普通书籍,还有批量折扣书、限量版精装书——它们都是"书",有很多共性,但计算价格的方式各不相同。你不想写三个几乎一模一样的类,然后把 if (type == BULK) {...} 的判断洒满整个代码库。

想象工厂的模具系统——有一张"书"的基础模具,压出来只带编号和单价。批量折扣书是这张模具加了一个"买够多少本打折"的附件。限量版再加一个"编号签名"的附件。三种产品共用同一个基础模具,各自在上面的附件不同。你对外只说"这是书",但送进计价口时,每本书按自己的附件自动算出不同的价格。

这一章解决什么问题? 教你用 C++ 的三个核心机制——基类/派生类建立"是什么"的关系、虚函数在运行时自动选择正确版本、抽象基类作为体系蓝图——来构建一个可扩展的类体系。没有它们,你就得在所有用到多态的地方手动写类型判断和分支——每新增一种书就要改遍所有地方、漏一处就是一个 bug。

官方 Chapter 15 的完整继承契约

继承体系不仅是“virtual 调到派生版本”。访问控制决定向上转换是否可见,类作用域决定候选名字能否进入重载集,构造与拷贝控制维护每层子对象,容器则必须保存多态所有者而不是发生对象切片。

访问控制与继承

public 继承表达可替换的 IS-A 关系,派生对象可在可访问语境中隐式转换为基类指针或引用。protected/private 继承会限制这种转换对用户代码的可见性,更接近实现复用而非公共子类型;它们并非“一律错误”,但组合通常让依赖关系更显式。

protected 还有对象限制:派生类成员可通过派生类对象访问继承来的 protected 成员,却不能借任意 Base 对象访问 Base 的 protected。这样能防止某个派生类修改与自己无关的其他基类子对象。

继承中的类作用域

解释了为什么签名不同也会隐藏:

struct Base { virtual void f(int); void f(double); };
struct Derived : Base {
    using Base::f;       // 把基类重载重新引入当前作用域
    void f(int) override;
    void f(const char*); // 新重载
};

名字查找先于参数匹配;没有 using Base::f 时,只要 Derived 找到任意 f,就不会继续把 Base 的其他 f 加入候选。显式限定调用 obj.Base::f(1) 会绕过虚分派,只有确实要调用基类实现时才这样写。

构造函数、拷贝控制与虚析构

派生构造函数只直接初始化其直接基类,基类再负责更上层。用户自定义派生拷贝/移动构造时应在初始化列表调用对应基类操作;若完全使用合成派生拷贝控制,编译器会自动对基类子对象执行相应操作。不能把“手写时要显式调用”误解成所有派生拷贝都要手写。

是所有权接口的一部分。只要类可能通过基类指针被删除,析构就必须 virtual;若基类从不负责多态删除,可采用 protected 非虚析构阻止外部误删,而不是机械地给所有基类增加虚函数。

构造与析构期间,虚调用只分派到当前正在构造或析构的层级,因为更派生部分尚未建立或已经销毁。这是语言语义;实现常用逐层调整虚表指针,但标准不规定必须存在某种 vptr 布局。

容器与继承:Basket 和 clone

不能直接存 Quote 值,否则派生部分被切片。只接受 shared_ptr 会把所有权决策推给调用方;增加 clone 可提供值式入口:

class Quote {
public:
    virtual std::shared_ptr<Quote> clone() const {
        return std::shared_ptr<Quote>(new Quote(*this));
    }
    virtual ~Quote() {}
};
 
class Bulk_quote : public Quote {
public:
    std::shared_ptr<Quote> clone() const override {
        return std::shared_ptr<Quote>(new Bulk_quote(*this));
    }
};

Basket 的 multiset<shared_ptr<Quote>, Compare> 按 ISBN 组织项目,add_item(const Quote&) 调用 clone,结算时通过基类引用调用 net_price。容器持有对象,调用点无需知道具体派生类型,也不会发生切片。

文本查询再探:表达式也是类体系

Chapter 12 的 WordQuery 可扩展成 Query_base 抽象接口:eval 执行查询,rep 生成表达式文本;WordQuery 处理单词,NotQuery 包装一元操作,AndQuery/OrQuery 派生自保存左右 Query 的 BinaryQuery。对外的 Query 句柄持有 shared_ptr<Query_base>,并通过重载 ~&| 组合表达式树。

这个设计把所有权和多态分开:Query 是小型值式句柄,Query_base 层次负责动态行为,shared_ptr 负责共享表达式节点。用户写 fiery & bird | wind 时得到组合对象,不需要 dynamic_cast 或类型枚举分支。

继承:用已有的类定义新类

的语法非常简单:

class Bulk_quote : public Quote { /* ... */ };
//                 ↑ 继承方式   ↑ 基类名

这行代码声明了三件事:① Bulk_quote 是一种 Quote(IS-A 关系);② Bulk_quote 自动获得了 Quote 的全部成员——数据成员和函数成员都"继承"下来了;③ public 继承意味着基类的 public 成员在派生类中仍是 public——用户代码可以通过 Bulk_quote 对象直接调用继承来的 isbn() 等接口。

但在"自动获得"的背后有一条关键限制:虽然在每个派生类对象中都存在——它们被"继承"了——但派生类的成员函数不能直接访问它们。比如 Bulk_quote::net_price() 不能直接写 bookNoprice——它们是 Quote 的 private 成员,只有 Quote 自己的成员函数能碰。

如果你的基类需要让派生类访问某些成员但又不暴露给用户——用 。基类把 price 声明为 protected 而不是 private——用户代码不能直接读 quote.price,但派生类 Bulk_quote 的成员函数可以:

继承层次:基类与派生类的成员关系protected 成员可被派生类访问但对用户不可见;virtual 函数支持覆盖(override)class Quote(基类)private:std::string bookNo;double price = 0.0;protected: (派生类可访问,用户不可访问)public:virtual double net_price(size_t) const;: public Quoteclass Bulk_quote : public Quote(派生类)private:std::size_t min_qty = 0;double discount = 0.0;public:double net_price(size_t) const override;overrides the base virtual function// isbn() and other non-virtual members inherited as-isInherited from base: bookNo (not directly accessible, private)Inherited from base: isbn() (public, directly usable) / net_price (overridden)Visibilitypublicprotectedprivatebase privatenot visibleto derivedCore rule: base class private members exist in derived objects but are not directly accessible
Base class Quote defines bookNo/price (private) and virtual net_price (public). Derived Bulk_quote inherits via public all base members and overrides net_price. Base private members exist in the derived object but cannot be directly accessed.

上图展示了 Quote(基类)和 Bulk_quote(派生类)的完整层次结构。注意三个颜色区域:public 成员(绿区)继承后仍是 public——用户可见;protected 成员(黄区)只有派生类能访问——用户不可见;private 成员(灰区)派生类不可直接访问——必须通过基类接口间接操作。

虚函数:同一个接口,不同的实现

继承解决了"自动获得基类成员"的问题,但还不够。Bulk_quote 计算价格的方式与普通 Quote 不同——它需要根据购买数量打折。你需要在派生类里"重新定义" net_price 的行为,同时保证通过基类引用调用的代码能自动找到正确版本。

这就是 。基类在函数声明前加 virtual,派生类用相同签名覆盖——并在声明末尾加 关键字。override 不是必须的,但它是编译器的第二双眼睛——如果签名不匹配,编译器直接报错,不会让你无意中定义了一个新函数,在程序跑了三个月后才发现覆盖没生效:

class Quote {
public:
    virtual double net_price(std::size_t n) const
    { return n * price; }
    // ↑ virtual——告诉编译器"这个函数可以被派生类覆盖"
};
 
class Bulk_quote : public Quote {
public:
    double net_price(std::size_t n) const override
    //                                   ↑ override——编译器帮我检查签名
    {
        if (n >= min_qty) return n * (1 - discount) * price;
        else              return n * price;
    }
};

virtual 只需要在基类声明——派生类自动也是虚函数(即使不加 virtual 关键字)。但 override 是派生类显式表明意图的最佳方式——读到这行就知道"这是在覆盖基类的虚函数"。如果某个派生类想阻止更下一层再覆盖——在该函数声明末尾加

虚函数常见实现模型:vptr → vtable → 函数地址

下面用主流 ABI 的 vptr/vtable 模型解释一种常见实现。C++ 语言只保证最终覆写函数被调用,不规定对象必须含 vptr、指针位置与大小、表项顺序或是否去虚拟化;图不能当作可移植内存布局。

猜一猜:Quote *p = new Bulk_quote; p->net_price(10);——编译器只知道 pQuote*,那它到底执行的是 Quote::net_price 还是 Bulk_quote::net_price?怎么做到的?用 Stepper 一步步验证。

分步1 / 3

① 对象内存布局:vptr 隐藏指针

vptr → vtable → function address : How virtual dispatch worksStep 1: Each object with virtual functions has a hidden vptr pointing to its vtable123Quote object (base)vptr → Quote vtablestring bookNo (data)double price (data)Non-virtual: not in vtable8 bytes (64-bit) vptr at obj startBulk_quote object (derived)vptr → Bulk_quote vtablestring bookNo (base part)double price (base part)size_t min_qty (derived new)double discount(derived new)Key: each object's vptr points to a different vtablevptr is set during construction - layer by layer
Each object with virtual functions has a <strong>vptr</strong> (virtual pointer) at its start. Base and derived objects have different vptrs pointing to different vtables.

主流实现会在多态对象中保存 ,让不同动态类型定位到不同的虚函数表。图把它画在对象头部只是示意;32/64 位、虚继承、多重继承和编译器 ABI 都可能改变布局。

抽象基类:为整个体系画一张蓝图

有时候你不想让用户直接创建基类对象——基类只应该作为"接口蓝图"而存在,具体实例只能是它的派生类。比如 Quote——在现实业务中,从不存在一个"普通书"——每本书要么是批量折扣书、要么是限量版、要么是电子版。Quote 应该只定义"什么是书"的接口,而永远不被直接构造。

通过声明末尾的 = 0 指定:

class Quote {
public:
    virtual ~Quote() = default;            // 虚析构(铁律)
    virtual double net_price(std::size_t) const = 0;  // 纯虚——"你必须定义"
    std::string isbn() const { return bookNo; }
protected:
    double price = 0.0;
private:
    std::string bookNo;
};

= 0 声明了一个"纯虚"——派生类必须覆盖它,否则派生类自己也变成抽象类、无法实例化。包含至少一个纯虚函数的类就是 ——不能写 Quote q;,但可以写 Quote *p = new Bulk_quote;Quote &r = someBulk;。抽象基类的价值在于——给整个类体系定义统一的接口契约。所有需要"书"的地方都用 const Quote & 接收——实际参数承载的是某个具体派生类对象。函数内部通过纯虚接口操作,运行时自动找到正确实现。

继承与友元 / static 成员

两个容易遗漏的点,一句话就能说清,但考试和查 bug 时经常遇到。

友元关系不继承。基类友元可访问派生对象中的基类子对象私有成员,因为那仍是 Base 的部分,但不能因此访问 Derived 自己新增的 private/protected 成员;派生类也不会自动成为基类友元的友元。

基类的 static 成员在整个继承体系中只存在唯一实例class Base { public: static int count; };——Base::countDerived::countMoreDerived::count 访问的都是同一个 count。不管有多少层派生、多少个派生类——static 成员只有一份、大家共享。

影响成员可见性,也影响用户代码能否执行派生到基类转换:

上面这张表总结了三种继承方式下成员访问级别的变化。核心记住一条:基类的 private 成员永远不对派生类直接可见——无论继承方式是什么。public 继承保持 public→public 和 protected→protected——最常见。protectedprivate 继承把访问级别"降级"——极少使用。struct 默认 public 继承,class 默认 private 继承。

派生类对象的完整生命周期

派生类对象的构造和析构有一条不可妥协的铁序:

Derived Object Lifecycle: Strict 4-Stage OrderConstruction: base first, then derived (bottom-up) | Destruction: derived first, then base (top-down)Time →12341. Base ctorQuote::Quote()Init base part first:bookNo, price membersvptr → Quote vtable2. Derived ctorBulk_quote::Bulk_quote()Then init derived part:min_qty, discountvptr → Bulk_quote vtable3. Derived dtor~Bulk_quote()Destroy derived part first:min_qty, discount (reverse)vptr falls back to Quote vtable4. Base dtor~Quote()Finally destroy base part:price, bookNo (reverse)object lifetime endsObject Memory Layout Changes During LifecycleBase: initializedvptr → Quote vtableDerived: uninitializedFully constructedvptr → Bulk vtableReady to useDerived destroyedvptr → Quote vtableOnly base part leftAll destroyedMemory releasedRule: construct foundation first, then the building; destruct building first, then the foundationDestruction order is strict reverse of construction | vptr switches layer by layer | Base destructor MUST be virtual
Construction: base → derived. Destruction: derived → base (reverse).vptr updates layer by layer during construction and destruction. Base destructor must be virtual for correct cleanup through base pointer.

构造顺序:虚基类(由最派生类负责)→直接基类→数据成员按声明顺序→派生构造函数体。析构顺序严格反向:派生析构函数体→成员逆序→直接基类→虚基类。初始化列表的书写顺序不会改变这套次序。

构造或析构期间调用虚函数,最终覆写函数只考虑当前正在构造/析构的层级,不会进入尚未构造或已经销毁的更派生部分。主流实现常通过逐层调整分派信息实现,但应记语言后置条件而不是依赖 vptr 细节。

拷贝控制也要精心处理:当你自定义派生拷贝/移动构造或赋值时,要显式处理基类子对象;若使用合成版本,编译器会自动调用基类对应操作:

class Bulk_quote : public Quote {
public:
    // 拷贝构造——「初始化列表里先调基类拷贝」
    Bulk_quote(const Bulk_quote &rhs)
        : Quote(rhs), min_qty(rhs.min_qty), discount(rhs.discount) {}
    // 移动构造
    Bulk_quote(Bulk_quote &&rhs) noexcept
        : Quote(std::move(rhs)), min_qty(rhs.min_qty), discount(rhs.discount) {
        rhs.min_qty = 0; rhs.discount = 0;
    }
    // 拷贝赋值——「函数体里显式调基类拷贝赋值」
    Bulk_quote& operator=(const Bulk_quote &rhs) {
        Quote::operator=(rhs);  // 不写这一行 = 基类部分没被赋值!!!
        min_qty = rhs.min_qty;
        discount = rhs.discount;
        return *this;
    }
};

在用户自定义派生拷贝构造中漏掉 Quote(rhs) 会默认初始化基类部分;在用户自定义赋值中漏掉 Quote::operator=(rhs) 会保留目标原有基类状态。合成派生操作没有这个遗漏,它按成员与基类逐项处理。

代码逐段拆解:Quote / Bulk_quote 体系完整实现

以下是覆盖本章五个核心知识点的完整实现——每段对应一个知识点,有独立的设计决策解说。

基类骨架 + 虚函数接口

#include <string>
#include <iostream>
 
class Quote {
public:
    Quote() = default;
    Quote(const std::string &book, double p)
        : bookNo(book), price(p) {}
 
    std::string isbn() const { return bookNo; }
 
    // 虚函数——派生类可以覆盖
    virtual double net_price(std::size_t n) const
    { return n * price; }
 
    virtual ~Quote() = default;  // 虚析构铁律

isbn() 不是虚函数——所有书获取 ISBN 的方式都一样,不需要派生类覆盖。net_price() 是虚函数——不同种类的书有不同的计价逻辑。~Quote() 是虚析构——确保通过 Quote* 删除派生类对象时正确析构。bookNo 是 private——派生类通过 isbn() 访问,不直接碰数据。

派生类定义 + 虚函数覆盖

class Bulk_quote : public Quote {
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string &book, double p,
               std::size_t qty, double disc)
        : Quote(book, p), min_qty(qty), discount(disc) {}
 
    // 覆盖虚函数——签名完全一致 + override
    double net_price(std::size_t n) const override {
        if (n >= min_qty)
            return n * (1 - discount) * price;
        else
            return n * price;
    }

派生类构造时必须调用基类构造函数——Quote(book, p) 初始化基类部分的 bookNoprice。派生类只负责自己的 min_qtydiscountnet_priceoverride——编译器帮你检查签名是否匹配。price 必须是 Quote 的 protected 成员——派生类需要读它来算折扣价,但用户代码不应该碰它。

拷贝与移动控制

    // 拷贝控制——必须显式处理基类部分
    Bulk_quote(const Bulk_quote &rhs)
        : Quote(rhs), min_qty(rhs.min_qty), discount(rhs.discount) {}
 
    Bulk_quote(Bulk_quote &&rhs) noexcept
        : Quote(std::move(rhs)), min_qty(rhs.min_qty), discount(rhs.discount) {
        rhs.min_qty = 0; rhs.discount = 0;
    }
 
    Bulk_quote& operator=(const Bulk_quote &rhs) {
        Quote::operator=(rhs);
        min_qty = rhs.min_qty;
        discount = rhs.discount;
        return *this;
    }

每个拷贝/移动控制成员都显式地处理了基类部分——Quote(rhs)Quote(std::move(rhs))Quote::operator=(rhs)。忘了这行 = 派生类对象里继承来的 bookNoprice 没有被正确拷贝/移动。移动构造中基类资源由 Quote(std::move(rhs)) 接管,派生类自己的 min_qtydiscount 是标量——对它们置 0(rhs.min_qty = 0; rhs.discount = 0;)只是良好习惯,并非必需。

类体系外部接口:print_total

// 通过基类引用操作——运行时自动选择正确版本
double print_total(std::ostream &os, const Quote &item, std::size_t n) {
    double ret = item.net_price(n);   // 虚调用——动态绑定
    os << "ISBN: " << item.isbn()     // 非虚——静态绑定
       << " # sold: " << n << " total due: " << ret << std::endl;
    return ret;
}

参数用 const Quote& 而不是值传递。值传递会构造独立 Quote 并切除派生部分;引用仍指向原始动态对象,item.net_price(n) 按动态类型调用最终覆写函数。item.isbn() 非虚,按静态类型绑定到 Quote::isbn。

容器存储多态对象

#include <vector>
#include <memory>
 
// 存指针(不能存对象——对象切片)
std::vector<std::unique_ptr<Quote>> basket;
 
basket.push_back(std::unique_ptr<Quote>(
    new Bulk_quote("0-201-1", 50, 10, 0.25)));
basket.push_back(std::unique_ptr<Quote>(new Quote("0-201-2", 30)));
 
double total = 0;
for (const auto &item : basket)
    total += item->net_price(10);  // 虚调用——各用自己的版本

容器必须存指针(智能指针),不能存对象——vector<Quote> 会发生对象切片,把每个存进去的 Bulk_quote 切成只剩 Quote 部分。存 unique_ptr<Quote> 保持多态性——通过基类指针访问虚函数时动态绑定到正确版本。for 循环中 item->net_price(10)Bulk_quote 对象走折扣版本、对普通 Quote 对象走原价版本——同一行代码、自动适配。

容易踩的坑

小结

  • 继承建立 IS-A 层次("Bulk_quote 是一种 Quote")——派生类自动获得基类成员,可扩展新成员、覆盖虚函数行为。基类 private 成员派生类不可直接访问——用 protected 为派生类开放窗口
  • 虚函数通过基类引用/指针按动态类型选择最终覆写函数;vptr/vtable 只是常见 ABI 模型,override 检查覆盖签名,final 阻止继续覆盖
  • 抽象基类包含纯虚函数(= 0)——不能实例化但定义统一接口。派生类必须覆盖纯虚函数,覆盖了才能实例化——这是编译器强制执行的"契约"
  • 构造和析构按虚基类、直接基类、成员、派生层逐步建立并反向销毁;期间虚调用不进入更派生层,用户自定义派生拷贝控制要显式处理基类子对象
  • 静态成员在体系中共享;友元关系不继承,但基类友元仍可访问派生对象里的基类子对象私有部分

练习

问题 1(改代码型) 下面的代码有至少三个与面向对象设计相关的问题——找出并修正。

class Base {
public:
    void f() { std::cout << "Base"; }
    ~Base() {}
};
 
class Derived : public Base {
public:
    void f() { std::cout << "Derived"; }
private:
    std::string *data;
public:
    Derived() : data(new std::string("hello")) {}
};
 
int main() {
    Base *p = new Derived;
    p->f();           // 期望输出 Derived
    delete p;          // 正确释放资源?
    std::vector<Base> v;
    v.push_back(Derived());  // 能存吗?
}

问题 2(独立实现题) 设计并实现一个 Employee 抽象类体系:

  • Employee 包含:name(string)、id(int)—— protected;纯虚函数 double salary() const = 0;;非虚函数 string getName() const { return name; };虚析构
  • HourlyEmployee 继承 EmployeehourlyRate + hoursWorked——salary() 返回两者乘积
  • SalariedEmployee 继承 EmployeeannualSalary——salary() 返回月薪(年/12)
  • main 测试:创建一个包含两种雇员的 unique_ptr 容器,遍历调用 getName()salary() 验证动态绑定

问题 3(问答型) class C : public A, public B {};——如果 AB 都有一个 virtual void f()C 必须覆盖 f() 吗?如果 C 不覆盖,通过 C 对象调 f() 会怎样?通过 A*B* 分别指向 C 对象调用 f() 呢?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

继承(inheritance)

C++ OOP 三大基石之一。通过已有类(基类)定义新类(派生类)。派生类自动获得基类的全部成员,可在此基础上扩展新成员或覆盖已有行为。表达 IS-A 关系——派生类是基类的一种,凡是需要基类的地方都能用派生类。public 继承是最常见形式。类比:基础模具做出通用产品,派生模具在上面加特殊附件。

基类的 private 成员

private 声明——只有本类的成员函数和友元可以访问。派生类虽然"拥有"这些成员(它们存在于每个派生类对象中)——但不能直接读写它们。必须通过基类的 publicprotected 接口间接操作。这是封装性的体现——派生类不能随意篡改基类的内部实现细节。

protected 访问控制

介于 publicprivate 之间——对类的用户(外部代码)不可见,但对派生类的成员和友元可见。基类用 protected 声明那些"派生类需要读写、但外界用户不该碰"的成员。设计原则:能用 private 就不用 protected——protected 也是一种暴露,会影响所有派生类。

虚函数(virtual function)

virtual 关键字声明的成员函数。通过基类的指针或引用调用时,实际执行的是指针/引用所指对象的真实类型的版本——不是基类版本。这是 C++ 实现运行时多态的核心机制。构造函数不能是虚函数(对象还没构造完);析构函数通常应该是虚函数(确保通过基类指针 delete 时正确释放派生类资源)。

vptr(virtual pointer)

主流 C++ ABI 用于定位虚函数表的隐藏实现指针,常随构造与析构阶段调整。C++ 标准不要求它存在,也不规定位置、数量或大小;图中对象头布局只是教学模型。

虚函数表(vtable)

主流 ABI 为多态类生成的分派表,通常保存函数地址或调整入口。表的存在、布局、槽位顺序都不是标准接口,编译器还可在已知动态类型时去虚拟化直接调用。

纯虚函数(pure virtual function)

声明中带 pure-specifier 的虚函数,使类成为抽象类。纯虚函数仍可在类外提供定义;纯虚析构必须有定义,因为派生对象析构链会调用它。派生类需提供最终覆写才能成为具体类。

抽象基类(abstract base class)

包含至少一个纯虚函数的类。不能直接创建实例(Quote q; 编译错误)——但可以定义指针或引用指向派生类对象。作为类体系的"接口蓝图"——定义统一的接口规范,各派生类提供自己的具体实现。让调用方通过抽象接口操作而无需关心具体是哪种派生类。

override

C++11 关键字——放在派生类虚函数声明末尾。告诉编译器"这个函数意图覆盖基类的虚函数"。如果签名与基类虚函数不匹配——编译器立即报错,不会悄悄定义一个新的无关函数。是编译器的"第二双眼睛"——最佳实践:每次覆盖虚函数都加 override

final

C++11 关键字。放在虚函数声明后——禁止派生类再覆盖此函数。放在类名后——禁止此类再被继承。用于明确"这个函数/类是这个体系中的最终版本——到此为止、不可再变"。

继承的访问控制

public/protected/private 继承对基类 public/protected 成员可见性和派生到基类转换可访问性的控制。public 表达公开子类型,非 public 继承用于受限实现复用;基类 private 成员始终不可由派生类直接访问。

继承中的类作用域

派生类作用域嵌套在基类作用域内。名字查找先在最近层停止,再做重载决议,所以派生同名成员会隐藏基类整组重载;using 声明可把基类重载重新引入。

虚析构函数(virtual destructor)

声明为 virtual 的基类析构。通过基类指针删除派生对象时,保证派生到基类逐层析构;承担多态删除的基类必须提供它。

多态容器(containers and inheritance)

容器通过基类智能指针拥有不同派生对象,并借虚函数保持动态行为。直接保存基类值会切片;clone 接口可为左值对象复制真实动态类型。

资料与写作方式声明

本章以C++ Primer, Fifth Edition, Chapter 15: Object-Oriented Programming权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…