面向对象程序设计
掌握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
直觉:一模多型,从一张图纸到一整条产品线
你已经学会了设计一个类——封装数据、定义接口、管理资源。但现在遇到一个新问题:你的书店系统里有普通书籍,还有批量折扣书、限量版精装书——它们都是"书",有很多共性,但计算价格的方式各不相同。你不想写三个几乎一模一样的类,然后把 if (type == BULK) ...
- 2
官方 Chapter 15 的完整继承契约
继承体系不仅是“virtual 调到派生版本”。访问控制决定向上转换是否可见,类作用域决定候选名字能否进入重载集,构造与拷贝控制维护每层子对象,容器则必须保存多态所有者而不是发生对象切片。
- 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 的基类析构函数。通过基类指针删除派生对象时,它保证先执行派生析构再逐层执行基类析构;面向多态删除的基类必须提供它。 是所有权接口的一部分。只要类可能通过基类指针被删除,析构就必须 virtual;若基类从不负责多态删除,可采用 protected 非虚析构阻止外部误删,而不是机械地给所有基类增加虚函数。
构造与析构期间,虚调用只分派到当前正在构造或析构的层级,因为更派生部分尚未建立或已经销毁。这是语言语义;实现常用逐层调整虚表指针,但标准不规定必须存在某种 vptr 布局。
容器与继承:Basket 和 clone
↡容器通过基类智能指针保存不同派生对象,并借虚函数保持动态行为的设计。若要把左值对象加入容器,通常提供虚 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 或类型枚举分支。
继承:用已有的类定义新类
↡面向对象编程的核心机制——通过已有类(基类)定义新类(派生类)。派生类自动获得基类的成员,可扩展新成员或覆盖已有行为。表达 IS-A 关系——派生类是基类的一种。public 继承是最常见形式。的语法非常简单:
class Bulk_quote : public Quote { /* ... */ };
// ↑ 继承方式 ↑ 基类名这行代码声明了三件事:① Bulk_quote 是一种 Quote(IS-A 关系);② Bulk_quote 自动获得了 Quote 的全部成员——数据成员和函数成员都"继承"下来了;③ public 继承意味着基类的 public 成员在派生类中仍是 public——用户代码可以通过 Bulk_quote 对象直接调用继承来的 isbn() 等接口。
但在"自动获得"的背后有一条关键限制:↡用 private 关键字声明的成员——只能被类自身的成员函数和友元访问,不能被派生类直接访问。基类的 private 成员虽然存在于每个派生类对象中,但派生类无法直接读写它们——必须通过基类的 public/protected 接口间接操作。虽然在每个派生类对象中都存在——它们被"继承"了——但派生类的成员函数不能直接访问它们。比如 Bulk_quote::net_price() 不能直接写 bookNo 或 price——它们是 Quote 的 private 成员,只有 Quote 自己的成员函数能碰。
如果你的基类需要让派生类访问某些成员但又不暴露给用户——用 ↡用 protected 关键字声明的成员——介于 public 和 private 之间:对本类的用户(外部代码)不可见,但对派生类的成员函数和友元可见。为派生类提供「允许访问但不对外公开」的中间地带。。基类把 price 声明为 protected 而不是 private——用户代码不能直接读 quote.price,但派生类 Bulk_quote 的成员函数可以:
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 关键字声明的成员函数。通过基类指针或引用调用虚函数时,实际执行的是指针/引用所指对象的真实类型(派生类)的版本——不是基类的版本。虚函数是实现运行时多态(动态绑定)的核心机制。构造函数不能是虚函数;析构函数通常应该是虚函数。。基类在函数声明前加 virtual,派生类用相同签名覆盖——并在声明末尾加 ↡C++11 关键字,放在派生类虚函数声明末尾,显式表明这个函数意图覆盖基类的虚函数。若签名与基类虚函数不匹配,编译器立即报错,不会悄悄定义一个新的无关函数——是编译器的第二双眼睛。 关键字。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 是派生类显式表明意图的最佳方式——读到这行就知道"这是在覆盖基类的虚函数"。如果某个派生类想阻止更下一层再覆盖——在该函数声明末尾加 ↡C++11 关键字。放在虚函数声明末尾——禁止派生类再覆盖此函数(void f() final;)。放在类名后——禁止此类再被继承(class Last final : Base {};)。。
虚函数常见实现模型:vptr → vtable → 函数地址
下面用主流 ABI 的 vptr/vtable 模型解释一种常见实现。C++ 语言只保证最终覆写函数被调用,不规定对象必须含 vptr、指针位置与大小、表项顺序或是否去虚拟化;图不能当作可移植内存布局。
猜一猜:
Quote *p = new Bulk_quote; p->net_price(10);——编译器只知道p是Quote*,那它到底执行的是Quote::net_price还是Bulk_quote::net_price?怎么做到的?用 Stepper 一步步验证。
① 对象内存布局:vptr 隐藏指针
主流实现会在多态对象中保存 ↡主流 C++ ABI 用于定位虚函数表的隐藏实现指针。它常随构造和析构阶段调整,但标准不要求其存在、位置或大小,程序不能直接依赖。,让不同动态类型定位到不同的虚函数表。图把它画在对象头部只是示意;32/64 位、虚继承、多重继承和编译器 ABI 都可能改变布局。
抽象基类:为整个体系画一张蓝图
有时候你不想让用户直接创建基类对象——基类只应该作为"接口蓝图"而存在,具体实例只能是它的派生类。比如 Quote——在现实业务中,从不存在一个"普通书"——每本书要么是批量折扣书、要么是限量版、要么是电子版。Quote 应该只定义"什么是书"的接口,而永远不被直接构造。
↡在虚函数声明中使用 pure-specifier 的函数,使包含它的类成为抽象类。纯虚函数仍可在类外提供定义;纯虚析构函数必须有定义,因为析构链会调用它。通过声明末尾的 = 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 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::count、Derived::count、MoreDerived::count 访问的都是同一个 count。不管有多少层派生、多少个派生类——static 成员只有一份、大家共享。
↡public、protected 或 private 继承对基类 public/protected 成员可见性与派生到基类转换可访问性的控制。public 表达公开子类型,非 public 继承主要用于受限实现复用。影响成员可见性,也影响用户代码能否执行派生到基类转换:
上面这张表总结了三种继承方式下成员访问级别的变化。核心记住一条:基类的 private 成员永远不对派生类直接可见——无论继承方式是什么。public 继承保持 public→public 和 protected→protected——最常见。protected 和 private 继承把访问级别"降级"——极少使用。struct 默认 public 继承,class 默认 private 继承。
派生类对象的完整生命周期
派生类对象的构造和析构有一条不可妥协的铁序:
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) 初始化基类部分的 bookNo 和 price。派生类只负责自己的 min_qty 和 discount。net_price 加 override——编译器帮你检查签名是否匹配。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)。忘了这行 = 派生类对象里继承来的 bookNo 和 price 没有被正确拷贝/移动。移动构造中基类资源由 Quote(std::move(rhs)) 接管,派生类自己的 min_qty 和 discount 是标量——对它们置 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继承Employee:hourlyRate+hoursWorked——salary()返回两者乘积SalariedEmployee继承Employee:annualSalary——salary()返回月薪(年/12)- 写
main测试:创建一个包含两种雇员的unique_ptr容器,遍历调用getName()和salary()验证动态绑定
问题 3(问答型) class C : public A, public B {};——如果 A 和 B 都有一个 virtual void f(),C 必须覆盖 f() 吗?如果 C 不覆盖,通过 C 对象调 f() 会怎样?通过 A* 和 B* 分别指向 C 对象调用 f() 呢?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 继承(inheritance)
C++ OOP 三大基石之一。通过已有类(基类)定义新类(派生类)。派生类自动获得基类的全部成员,可在此基础上扩展新成员或覆盖已有行为。表达 IS-A 关系——派生类是基类的一种,凡是需要基类的地方都能用派生类。public 继承是最常见形式。类比:基础模具做出通用产品,派生模具在上面加特殊附件。
- 基类的 private 成员
用
private声明——只有本类的成员函数和友元可以访问。派生类虽然"拥有"这些成员(它们存在于每个派生类对象中)——但不能直接读写它们。必须通过基类的public或protected接口间接操作。这是封装性的体现——派生类不能随意篡改基类的内部实现细节。- protected 访问控制
介于
public和private之间——对类的用户(外部代码)不可见,但对派生类的成员和友元可见。基类用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 接口可为左值对象复制真实动态类型。