【C++ 基础入门】C++ 多态(上):虚函数、重写、override/final 与抽象类

👋 欢迎阅读

🔥愿旖旎 · 个人主页

📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》

🌄 钱塘江上潮信来,今日方知我是我

✨当前学习内容: 《C++学习》

📑 目录


一、前置讲解

在进入正文前,先花 10 秒了解本篇会反复用到的核心概念,带着印象去读正文,学习效率更高。

🧠 前置知识一:多态(Polymorphism)

同一行为,不同对象去完成会产生不同结果------买票:普通人全价、学生半价。

🎯 前置知识二:虚函数(virtual)

被 virtual 修饰的类成员函数 ,是多态能成立的前提;和虚继承的 virtual 不是一回事。

🔁 前置知识三:重写(覆盖)

派生类写一个与基类虚函数返回值、函数名、参数列表完全相同的函数,把它"盖"掉。

🚫 前置知识四:override / final

override 检查是否真的重写 ,final 禁止再被重写 或禁止被继承。

🧱 前置知识五:抽象类

带纯虚函数 (= 0)的类,不能实例化 ;派生类必须重写所有纯虚函数才能创建对象。


二、多态的概念

多态,通俗来说就是多种形态。具体点就是:去完成某个行为,当不同的对象去完成时会产生出不同的状态。

比如买票这个行为:

谁买票 结果
普通人 全价买票
学生 半价买票
军人 优先买票

三、多态的定义及实现

3.1 多态的构成条件

多态是:不同继承关系的类对象,去调用同一函数,产生了不同的行为。 比如 Student 继承了 Person:Person 对象买票全价,Student 对象买票半价。

在继承中要构成多态还有两个条件:

序号 条件
1 必须通过基类的指针或者引用调用虚函数
2 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

💡 两条缺一不可:只用指针/引用不行 (函数不是虚函数,编译期就绑死了);只写虚函数也不行(用对象调用,依然是静态绑定)。

3.2 虚函数的定义

虚函数:被 virtual 修饰的类成员函数称为虚函数。

⚠️ 要和虚继承 做区分:虚继承是修饰继承方式 的,虚函数是修饰成员函数的。

格式:virtual 返回值类型 函数名(形参)

cpp 复制代码
class Person
{
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

3.3 虚函数的重写

虚函数的重写(覆盖) :派生类中有一个跟基类完全相同的虚函数 (即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同 ),就称子类的虚函数重写了基类的虚函数。

cpp 复制代码
class Person
{
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
​
class Student : public Person
{
public:
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
​
// 重写基类虚函数时,派生类的虚函数不加 virtual 关键字时,虽然也可以构成重写
//(因为继承后基类的虚函数被继承下来了,派生类依旧保持虚函数属性)
// 但这种写法不规范,不建议这样使用:
// void BuyTicket() { cout << "买票-半价" << endl; }
​
void Func(Person& p)
{
    p.BuyTicket();
}
​
int main()
{
    Person ps;
    Student st;
    Func(ps);
    Func(st);
    return 0;
}

运行结果:

复制代码
买票-全价
买票-半价

💡 同样的 Func(st),传进去的是 Student 对象、形参却是 Person&,最终却调到了 Student::BuyTicket------这就是多态:调用哪份实现,由运行时对象的真实类型决定,而不是由形参的静态类型决定。

虚函数重写的两个例外:

例外 1:协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数的返回值类型不同 ------即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。(了解即可)

cpp 复制代码
class Person
{
public:
    virtual Person* f() { return new Person; }
};
​
class Student : public Person
{
public:
    virtual Student* f() { return new Student; }
};

例外 2:析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写 ------虽然基类与派生类析构函数名字不同 。看起来这违背了重写规则,其实不然:编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor。

cpp 复制代码
class Person
{
public:
    virtual ~Person() { cout << "~Person()" << endl; }
};
​
class Student : public Person
{
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};
​
// 只有派生类 Student 的析构函数重写了 Person 的析构函数,
// 下面的 delete 对象调用析构函数才能构成多态,
// 才能保证 p1 和 p2 指向的对象正确调用析构函数。
​
int main()
{
    Person* p1 = new Person;
    Person* p2 = new Student;
    delete p1;
    delete p2;
    return 0;
}

运行结果:

复制代码
~Person()
~Student()
~Person()
若基类析构不是虚函数 若基类析构是虚函数
delete p2 只调 ~Person(),派生类部分没被清理 → 资源泄漏 delete p2 先调 ~Student()、再自动调 ~Person(),清理完整

💡 结论:只要一个类可能被继承,并且会用基类指针 delete 派生类对象,基类析构就必须是 virtual。 这是面试高频"送命题"。

3.4 C++11 override 和 final

C++ 对函数重写的要求比较严格,但有些情况下由于疏忽,函数名字母次序写反 就导致无法构成重写------而这种错误编译期间不会报出 ,只有运行时发现结果不对再 debug 就得不偿失了。所以 C++11 提供了 override 和 final 两个关键字,帮助检测是否重写。

关键字 作用
final 修饰虚函数 :表示该虚函数不能再被重写 ;也可修饰类 :让类不能被继承
override 检查派生类虚函数是否重写了基类某个虚函数 ,如果没有重写则编译报错

final 示例(派生类试图重写 → 编译报错):

cpp 复制代码
class Car
{
public:
    virtual void Drive() final {}
};
​
class Benz : public Car
{
public:
    virtual void Drive() { cout << "Benz-舒适" << endl; }   // 编译报错:无法重写 final 函数
};

运行结果:

复制代码
编译报错:无法重写 "final" 函数(Car::Drive)

override 示例(正确重写,编译通过):

cpp 复制代码
class Car
{
public:
    virtual void Drive() {}
};

class Benz : public Car
{
public:
    virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

运行结果:

复制代码
编译通过(若函数名写错,override 会直接报"未重写任何基类函数")

3.5 重载、重写(覆盖)、重定义(隐藏)对比


四、抽象类

4.1 概念与纯虚函数

在虚函数的后面写上 = 0,则这个函数为纯虚函数。

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。 纯虚函数规范了派生类必须重写 ,另外纯虚函数更体现出了接口继承。

cpp 复制代码
class Car
{
public:
    virtual void Drive() = 0;   // 纯虚函数
};

class Benz : public Car
{
public:
    virtual void Drive()        // 重写了纯虚函数,可以实例化
    {
        cout << "Benz-舒适" << endl;
    }
};

class BMW : public Car
{};   // 没有重写 Drive,依然是抽象类,不能实例化

void Test()
{
    Car* pBenz = new Benz;
    pBenz->Drive();

    Car* pBMW = new BMW;   // 错误:任何派生类想被实例化,必须重写所有纯虚函数
}

运行结果:

复制代码
Benz-舒适

Car* pBMW = new BMW;   编译报错:无法实例化抽象类(BMW 未重写 Drive)

抽象类要点:

要点 说明
= 0 只是"声明" 纯虚函数可以只有声明、没有实现(也可以给实现,但派生类仍必须重写)
抽象类不能实例化 含纯虚函数的类不能创建对象,但可以定义指针/引用
派生类要"补全" 派生类必须重写所有纯虚函数,否则它自己也是抽象类
用途 强制派生类实现接口,表达"这是一类能力",常用于接口设计

4.2 接口继承和实现继承

普通函数的继承是一种实现继承 :派生类继承了基类函数,可以直接使用 这个函数,继承的是函数的实现。

虚函数的继承是一种接口继承 :派生类继承的是基类虚函数的接口 ,目的是为了重写 ,达成多态 ,继承的是接口。

继承类型 继承什么 目的
实现继承(普通函数) 函数的实现 直接复用代码
接口继承(虚函数) 函数的接口(签名) 重写它,实现多态

所以如果不实现多态,不要把函数定义成虚函数。

💡 这句话很关键:虚函数是有代价的 (多一次间接寻址、对象里多一个虚表指针)。如果一个函数根本不打算被重写、也不参与多态,就写成普通函数 ,别乱加 virtual。


五、复盘(附答案)

💡 思考题

  1. 构成多态的两个条件是什么? 只满足其中一个会怎样?

  2. virtual 修饰虚函数和 virtual 修饰继承方式,分别是什么? 两者有什么本质区别?

  3. 重载、重写、重定义三者怎么区分?各自发生在什么作用域?

  4. 虚函数重写的两个例外是什么? 为什么析构函数的名字不同也能构成重写?

  5. 基类的析构函数为什么建议定义为虚函数? 如果不这样做会发生什么?

  6. override 和 final 分别解决什么问题?

  7. 抽象类为什么不能实例化? 派生类要满足什么条件才能实例化?

  8. 什么是接口继承和实现继承? 为什么说不实现多态就不要把函数定义成虚函数?

📝 答案

  1. ① 必须通过基类的指针或者引用调用虚函数;② 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。 只满足一个都不行:只有虚函数但用对象 调用,是编译期静态绑定 ,调不到派生类版本;只用指针/引用但函数不是虚函数,同样静态绑定,结果只取决于指针的静态类型。

  2. virtual 修饰函数 ,得到的是虚函数 ,用于实现多态 (运行期动态绑定);virtual 修饰继承方式 (如 class Student : virtual public Person),得到的是虚继承 ,用于解决菱形继承的数据冗余与二义性 。两者修饰的对象完全不同 :一个修饰成员函数 ,一个修饰继承方式。

  3. 重载 :同一作用域 内,函数名相同、参数列表不同 ;重写(覆盖) :基类和派生类 之间,函数名、参数列表、返回值都相同 ,且都是虚函数 ;重定义(隐藏) :基类和派生类 之间,函数名相同 即构成(参数可同可不同),本质是内层作用域屏蔽外层同名名字。

  4. 两个例外是协变 和析构函数的重写 。协变 :基类虚函数返回基类对象的指针/引用、派生类虚函数返回派生类对象的指针/引用,返回值类型不同也能构成重写。析构函数 :基类析构为虚函数时,派生类析构不管加不加 virtual 都构成重写------因为编译器对析构函数名做了特殊处理,编译后统一成 destructor,所以名字看起来不同、实际是同一个。

  5. 因为通过基类指针 delete 派生类对象 时,若基类析构不是虚函数 ,只会调用基类的析构函数,派生类那部分资源不会被释放 (资源泄漏、行为未定义)。把基类析构设为 virtual 后,delete 才能多态地先调派生类析构、再调基类析构,保证清理完整。

  6. override 用来检查派生类虚函数是否真的重写了基类虚函数 :写错函数名/参数时编译期直接报错 ,避免"以为重写了、其实只是隐藏"的隐蔽 bug。final 用来禁止重写 (修饰虚函数)或禁止继承(修饰类),保护不希望被改动/被扩展的实现。

  7. 抽象类包含纯虚函数(= 0),纯虚函数没有可供调用的实现,所以抽象类不能实例化出对象 (但可以定义指针和引用 )。派生类必须重写所有纯虚函数 才能实例化;只要还有一个纯虚函数没重写,派生类自己仍然是抽象类,同样不能实例化。

  8. 实现继承 是普通函数的继承 ,派生类直接拿到函数的实现 并复用;接口继承 是虚函数的继承 ,派生类继承的是接口/签名 ,目的是重写 以实现多态。因为虚函数是为多态服务的 ,会带来虚表指针、间接寻址等开销;如果不打算重写、也不打算通过基类指针调用,就不该定义成虚函数,否则只是白白付出代价。


结语

如果本文对你有帮助,欢迎:

👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充

你的每一次互动都是我继续更新的动力,我们下一篇见!🚀

相关推荐
starzy199032 分钟前
深入理解Scala嵌套函数:作用域封装之道
开发语言·scala
Sakura_j_h_41 分钟前
从零写一个 MCP stdio Server:用 Python 校验博客文章格式
开发语言·python·自动化·ai工具·mcp·json-rpc
Oll Correct2 小时前
Adobe illustrator 案例十二:视频播放(Media & Video)图标绘制
笔记·ui·adobe·illustrator
青少儿编程课堂2 小时前
蓝桥杯青少组图形化 STEMA 省考(报名倒计时·11/22开考)+ 全国青少年信息素养大赛 2026—2027赛季 + 电子学会图形化考级:校园华容道大闯关
c++·python·算法·bfs·信息学竞赛
白杨尚青2 小时前
C++进阶篇(五):红黑树——四条规则、三种插入情况与旋转变色
开发语言·数据结构·c++·stl·红黑树·平衡二叉树
橙橙笔记2 小时前
QT的学习第四部分
linux·qt·学习·音视频
.道阻且长.2 小时前
Linux基本开发工具:gdb和cgdb的使用
linux·运维·服务器·开发语言
叼烟扛炮3 小时前
Linux 学习笔记(五十四):内网穿透实战——用 frp 让外网访问你的私人电脑
linux·服务器·网络·笔记·学习
在猴站学算法3 小时前
(《统计学习方法》(第2版) 李航)第2章 感知机算法
学习·算法·机器学习·统计学习