👋 欢迎阅读

🔥愿旖旎 · 个人主页
📘学习专栏: 《算法专栏》《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。
五、复盘(附答案)
💡 思考题
-
构成多态的两个条件是什么? 只满足其中一个会怎样?
-
virtual修饰虚函数和virtual修饰继承方式,分别是什么? 两者有什么本质区别? -
重载、重写、重定义三者怎么区分?各自发生在什么作用域?
-
虚函数重写的两个例外是什么? 为什么析构函数的名字不同也能构成重写?
-
基类的析构函数为什么建议定义为虚函数? 如果不这样做会发生什么?
-
override和final分别解决什么问题? -
抽象类为什么不能实例化? 派生类要满足什么条件才能实例化?
-
什么是接口继承和实现继承? 为什么说不实现多态就不要把函数定义成虚函数?
📝 答案
-
① 必须通过基类的指针或者引用调用虚函数;② 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。 只满足一个都不行:只有虚函数但用对象 调用,是编译期静态绑定 ,调不到派生类版本;只用指针/引用但函数不是虚函数,同样静态绑定,结果只取决于指针的静态类型。
-
virtual修饰函数 ,得到的是虚函数 ,用于实现多态 (运行期动态绑定);virtual修饰继承方式 (如class Student : virtual public Person),得到的是虚继承 ,用于解决菱形继承的数据冗余与二义性 。两者修饰的对象完全不同 :一个修饰成员函数 ,一个修饰继承方式。 -
重载 :同一作用域 内,函数名相同、参数列表不同 ;重写(覆盖) :基类和派生类 之间,函数名、参数列表、返回值都相同 ,且都是虚函数 ;重定义(隐藏) :基类和派生类 之间,函数名相同 即构成(参数可同可不同),本质是内层作用域屏蔽外层同名名字。
-
两个例外是协变 和析构函数的重写 。协变 :基类虚函数返回基类对象的指针/引用、派生类虚函数返回派生类对象的指针/引用,返回值类型不同也能构成重写。析构函数 :基类析构为虚函数时,派生类析构不管加不加
virtual都构成重写------因为编译器对析构函数名做了特殊处理,编译后统一成destructor,所以名字看起来不同、实际是同一个。 -
因为通过基类指针
delete派生类对象 时,若基类析构不是虚函数 ,只会调用基类的析构函数,派生类那部分资源不会被释放 (资源泄漏、行为未定义)。把基类析构设为virtual后,delete才能多态地先调派生类析构、再调基类析构,保证清理完整。 -
override用来检查派生类虚函数是否真的重写了基类虚函数 :写错函数名/参数时编译期直接报错 ,避免"以为重写了、其实只是隐藏"的隐蔽 bug。final用来禁止重写 (修饰虚函数)或禁止继承(修饰类),保护不希望被改动/被扩展的实现。 -
抽象类包含纯虚函数(
= 0),纯虚函数没有可供调用的实现,所以抽象类不能实例化出对象 (但可以定义指针和引用 )。派生类必须重写所有纯虚函数 才能实例化;只要还有一个纯虚函数没重写,派生类自己仍然是抽象类,同样不能实例化。 -
实现继承 是普通函数的继承 ,派生类直接拿到函数的实现 并复用;接口继承 是虚函数的继承 ,派生类继承的是接口/签名 ,目的是重写 以实现多态。因为虚函数是为多态服务的 ,会带来虚表指针、间接寻址等开销;如果不打算重写、也不打算通过基类指针调用,就不该定义成虚函数,否则只是白白付出代价。
结语
如果本文对你有帮助,欢迎:
👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充
你的每一次互动都是我继续更新的动力,我们下一篇见!🚀