C++ 基础5 继承 多态
想象你在写一个程序,就像是在安排一个送外卖的流程。正常流程 (try:外卖小哥接单 -> 取餐 -> 送达。出现意外 (throw:突然,外卖小哥发现“电动车坏了”或者“餐洒了”。这时候他不能假装没看见继续送,他必须**抛出(throw)**这个问题,停止送货,向上级汇报。处理问题 (catch:客服(上级)**捕获(catch)**到了这个报告,根据不同的情况做处理(退款、补发、或者换个骑手)。
多继承:
多继承与同名成员处理:
如果子类重定义了父类的同名函数,不管参数是否相同,子类都将屏蔽父类的同名函数。如果非要想访问同名函数,需要加作用域(无论是数据成员还是成员函数都沿用此办法)
继承-菱继承:

继承-虚继承:
#include <iostream>
// 基类 Base
class Base {
public:
int baseValue;
Base(int val = 0) : baseValue(val) {
std::cout << "Base 构造函数, 地址: " << this << ", baseValue: " << baseValue << std::endl;
}
virtual ~Base() {
std::cout << "Base 析构函数, 地址: " << this << std::endl;
}
void printBaseValue() const {
std::cout << "Base::baseValue = " << baseValue << std::endl;
}
};
// 派生类 Derived1 虚继承 Base
class Derived1 : virtual public Base {
public:
int d1Value;
Derived1(int d1Val = 0, int baseVal = 0)
: Base(baseVal), d1Value(d1Val) { // 注意:Base构造函数在这里被调用,但实际由MostDerived负责
std::cout << "Derived1 构造函数, 地址: " << this << ", d1Value: " << d1Value << std::endl;
}
};
// 派生类 Derived2 虚继承 Base
class Derived2 : virtual public Base {
public:
int d2Value;
Derived2(int d2Val = 0, int baseVal = 0)
: Base(baseVal), d2Value(d2Val) { // 注意:Base构造函数在这里被调用,但实际由MostDerived负责
std::cout << "Derived2 构造函数, 地址: " << this << ", d2Value: " << d2Value << std::endl;
}
};
// 最终派生类 MostDerived 多重继承 Derived1 和 Derived2
// 由于 Base 是虚基类,MostDerived 负责构造它,并确保只有一份实例。
class MostDerived : public Derived1, public Derived2 {
public:
int mdValue;
MostDerived(int mdVal = 0, int d1Val = 0, int d2Val = 0, int baseVal = 0)
: Base(baseVal), // 最终派生类负责初始化虚基类
Derived1(d1Val, baseVal), // Derived1中的Base构造函数调用会被忽略
Derived2(d2Val, baseVal), // Derived2中的Base构造函数调用会被忽略
mdValue(mdVal) {
std::cout << "MostDerived 构造函数, 地址: " << this << ", mdValue: " << mdValue << std::endl;
}
};
int main() {
MostDerived obj(100, 10, 20, 5); // 构造一个MostDerived对象
std::cout << "--- 访问共享基类成员 ---" << std::endl;
obj.printBaseValue(); // 直接访问 Base 的成员
std::cout << "通过 obj.baseValue 访问: " << obj.baseValue << std::endl;
std::cout << "--- 验证 Base 子对象地址一致性 ---" << std::endl;
// 获取不同路径下的 Base 指针
Base* basePtrFromMostDerived = &obj; // 从 MostDerived 对象直接转换为 Base*
Derived1* d1Ptr = &obj;
Base* basePtrFromDerived1 = d1Ptr; // 从 Derived1* 转换为 Base*
Derived2* d2Ptr = &obj;
Base* basePtrFromDerived2 = d2Ptr; // 从 Derived2* 转换为 Base*
std::cout << "通过 MostDerived 获取 Base 地址: " << basePtrFromMostDerived << std::endl;
std::cout << "通过 Derived1 获取 Base 地址: " << basePtrFromDerived1 << std::endl;
std::cout << "通过 Derived2 获取 Base 地址: " << basePtrFromDerived2 << std::endl;
// 输出将显示所有 Base 地址都相同,证明 Base 子对象只存在一份。
return 0;
}
多态:
向上类型转换:用一个父类的 指针/引用 来保存一个子类空间的地址
形参是一个父类的指针或者引用
实参是子类的对象

这里需要引入一个绑定概念 向上类型转换就是早绑定决定的 早绑定:把函数体与函数调用相联系称为绑定(捆绑,binding) 当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding)
早绑定会造成无法调用子类,只能输出父类的值,所以要将早绑定修改为晚绑定(迟绑定)[迟绑定又称为运行时绑定才能确定具体要调用哪一个对象的函数]
实现迟绑定用虚函数:将父类中的函数写成虚函数 在函数前加一个关键字:virtual
如果父类中的函数是虚函数,那么子类重写虚函数以后,子类中的成员函数默认也就是虚函数
所以子类加不加关键字(virtual )都行
多态-抽象基类&纯虚函数:
纯虚函数就像一个接口,被称为抽象基类
当一个类包含至少一个纯虚函数时,它就被称为抽象基类(Abstract Base Class, ABC)。抽象基类不能被直接实例化(即不能创建其对象)。它的主要目的是为了定义一个接口规范,强制所有继承它的子类必须实现这些纯虚函数,从而确保子类拥有某些特定的行为
纯虚函数: 很多时候在继承关系中,父类中的成员函数一般不需要写实际的实现代码,只需要作为向上类型转换来使用。只想让父类作为一个接口使用,不希望去执行父类中的函数体的内容基于这种情况,我们就可以让父类的虚函数作为纯虚函数使用,就是只有声明,不需要函数体.
如何定义纯虚函数? 函数要使用virtual关键字 还要在函数后使用=0,表示这个函数是没有函数体的,纯虚函数没有函数体
//virtual void driver() = 0; // 这就是一个纯虚函数
如果一个类中存在至少一个纯虚函数,那么这个类就被叫做抽象类
抽象类不能被实例化
如果一个类继承了一个抽象类,那么子类中必须重写所有的纯虚函数,否则子类就也是一个抽象类


在一些编程语言中,我们也会将抽象类叫做 接口
建立公共接口(抽象类)目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来 操纵一组类,且这个公共接口不需要事先(或者不需要完全实现)。可以创建一个公共类.
模板方法模式:抽象类
例子中子类请记住这句话:如果一个类继承了一个抽象类,那么子类中必须重写所有的纯虚函数,否则子类就也是一个抽象类
class AbstractDrinking{
public:
//烧水
virtual void Boil() = 0;
//放入咖啡/茶叶
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//规定流程,这个函数是来决定上面4个纯虚函数执行顺序的
void MakeDrink(){
this->Boil(); // 烧水
Brew(); // 放入咖啡/茶叶
PourInCup(); // 倒入杯中
PutSomething(); // 加辅料
}
};
//制作咖啡
class Coffee : public AbstractDrinking{
public:
//烧水
virtual void Boil(){
cout << "煮农夫山泉!" << endl;
}
//冲泡
virtual void Brew(){
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup(){
cout << "将咖啡倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething(){
cout << "加入牛奶!" << endl;
}
};
//制作茶水
class Tea : public AbstractDrinking{
public:
//烧水
virtual void Boil(){
cout << "煮自来水!" << endl;
}
//冲泡
virtual void Brew(){
cout << "冲泡茶叶!" << endl;
}
//倒入杯中
virtual void PourInCup(){
cout << "将茶水倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething(){
cout << "加入食盐!" << endl;
}
};
//业务函数
void DoBussiness(AbstractDrinking* drink){
drink->MakeDrink();
delete drink;
}
void test(){
// 如果传入coffee类,那么就会执行coffee类从父类中所继承的MakeDrink(),那么就将执行Coffee中的重写的4个纯虚函数
DoBussiness(new Coffee);
cout << "--------------" << endl;
DoBussiness(new Tea);
}
多态-虚析构函数:

虚析构函数 跟虚构函数差不多
纯虚析构函数:使用virutal修饰,函数头后面加一个=0,析构函数是必须要有函数体
它们两个唯一的区别,如果一个类中存在纯虚析构函数,那么这个类就是一个抽象类
virtual ~Car() = 0
ps:一个面试题
C++异常
什么是 C++ 异常?(通俗解释)
想象你在写一个程序,就像是在安排一个送外卖的流程。
-
正常流程 (
try):外卖小哥接单 -> 取餐 -> 送达。 -
出现意外 (
throw):突然,外卖小哥发现“电动车坏了”或者“餐洒了”。这时候他不能假装没看见继续送,他必须**抛出(throw)**这个问题,停止送货,向上级汇报。 -
处理问题 (
catch):客服(上级)**捕获(catch)**到了这个报告,根据不同的情况做处理(退款、补发、或者换个骑手)。
核心语法三剑客:
-
try(尝试): 把可能出问题的代码包起来。 -
throw(抛出): 当代码运行不下去时,扔出一个错误(可以是数字、字符、字符串等)。 -
catch(捕获): 专门用来接住throw扔出来的错误,并处理它。
- 异常严格类型匹配:
这是 C++ 异常最特别的地方,也是新手最容易晕的地方。
在普通函数调用中,如果你传一个整数 10 给一个需要浮点数 double 的函数,C++ 会自动帮你转成 10.0。
但在异常处理中,C++ 是“直男”性格,非常死板:
-
你扔出一个 整数 (
int),我就只用catch(int)来接。 -
你扔出一个 字符 (
char),我就只用catch(char)来接。 -
绝对不会发生隐式类型转换! 即使
int可以转成double,异常机制也不会理会。
一个简单的案例代码
#include <iostream>
using namespace std;
// 这是一个模拟做饭的函数
// 参数 choice 代表选择做什么
void Cook(int choice) {
if (choice == 1) {
// 情况1:发现没盐了
// 抛出一个【整数】类型的异常,代表错误代码
throw 404;
}
else if (choice == 2) {
// 情况2:锅烧糊了
// 抛出一个【字符】类型的异常,代表错误等级
throw 'C';
}
else if (choice == 3) {
// 情况3:食材坏了
// 抛出一个【浮点数】类型的异常
// 注意:虽然 3.14 是数字,但它不是 int,类型不同!
throw 3.14;
}
cout << "饭做好了,真香!" << endl;
}
int main() {
// 我们尝试做饭,这里填入 1, 2, 或 3 来测试不同的错误
// 你可以修改这个变量来观察不同的 catch 结果
int myChoice = 3;
try {
// 1. 监控区域:这里面的代码可能会“炸”
cout << "开始做饭..." << endl;
Cook(myChoice);
cout << "流程结束(如果没有异常,这行才会显示)" << endl;
}
catch (int e) {
// 2. 专门捕获【整数】类型的异常
cout << "【捕获整数异常】: 错误代码是 " << e << " (没盐了!)" << endl;
}
catch (char e) {
// 3. 专门捕获【字符】类型的异常
cout << "【捕获字符异常】: 错误等级是 " << e << " (锅烧糊了!)" << endl;
}
catch (double e) {
// 4. 专门捕获【浮点数】类型的异常
// 重点:严格匹配!如果 Cook 抛出的是 int,这里绝对不会执行!
cout << "【捕获浮点异常】: 数据是 " << e << " (食材坏了!)" << endl;
}
catch (...) {
// 5. 兜底捕获:如果上面都没接住,这个能接住所有异常
cout << "【捕获未知异常】: 发生了什么我也不知道!" << endl;
}
cout << "------- 异常处理完毕,程序继续运行 -------" << endl;
return 0;
}
代码深度解析(为什么叫严格匹配?)
场景 A:如果 throw 404 (int)
-
程序运行到
Cook里面,抛出了整数404。 -
程序立马跳出
Cook函数,回到main的catch列表。 -
它看到第一个
catch (int e)。 -
匹配成功! 因为类型完全一样。执行里面的代码,打印“没盐了”。
场景 B:如果 throw 3.14 (double)
-
程序抛出
3.14。 -
它看到第一个
catch (int e)。-
普通函数:
3.14会变成3传进去。 -
异常机制: 匹配失败! C++ 说:“你是 double,他是 int,不约。”
-
-
它看到第二个
catch (char e)。匹配失败! -
它看到第三个
catch (double e)。匹配成功! 执行打印“食材坏了”。
场景 C:如果我把 catch (double e) 删掉?
如果你抛出了 3.14,但是代码里没有写 catch(double),且没有写兜底的 catch(...):
-
程序会直接崩溃(Crash/Terminate)!
-
因为它找不到处理这个异常的人,就像外卖出事了没人管,系统就崩了
异常的多态使用:
// 定义一个基本的父类异常类
class BaseException
{
public:
virtual void printExceptionMsg() {};
};
// 定义一个空指针异常,继承于BaseException
class NullPointerException :public BaseException
{
public:
void printExceptionMsg()
{
cout << "null pointer exception!" << endl;
}
};
// 定义一个数组下标越界异常,继承于BaseException
class IndexOutOfException :public BaseException
{
public:
void printExceptionMsg()
{
cout << " Index out of range!" << endl;
}
};
void fun1()
{
try
{
throw IndexOutOfException();
}
// 如果要处理多种异常,没必要在这个地方写N多个catch
// 让它们继承于同一个父类,然后在这个地方用父类引用接收不同子类对象就可以
// 这就是之前讲过的多态
catch (BaseException &e)
{
e.printExceptionMsg();
}
}
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)