枫の个人主页
你不能改变过去,但你可以改变未来
算法/C++/数据结构/C
Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记】
话不多说,让我们一起进入今天的学习吧~~~
一、多态的概念
多态(polymorphism)字面意思是“多种形态”,在C++中分为两类:编译时多态(静态多态)和运行时多态(动态多态)。
1. 编译时多态(静态多态)
核心是“编译期确定调用哪个函数”,主要通过函数重载和函数模板实现。
原理:通过不同的参数类型/个数,编译器在编译阶段就确定要调用的函数版本,无需运行时判断。
// 函数重载示例(编译时多态)
#include <iostream>
using namespace std;// 加法函数:int类型
int add(int a, int b) {return a + b;
}// 加法函数:double类型(重载)
double add(double a, double b) {return a + b;
}int main() {cout << add(1, 2) << endl; // 编译时确定调用int版本cout << add(1.5, 2.5) << endl;// 编译时确定调用double版本return 0;
}
2. 运行时多态(动态多态)
核心是“运行期确定调用哪个函数”,即“同一个行为,传入不同对象,产生不同结果”。
生活案例:
- 买票行为:普通人全价、学生打折、军人优先
- 动物叫行为:猫“喵”、狗“汪汪”
// 动物叫示例(运行时多态)
#include <iostream>
using namespace std;class Animal {
public:// 虚函数:关键标志virtual void talk() const {cout << "动物叫" << endl;}
};class Cat : public Animal {
public:// 重写虚函数virtual void talk() const override {cout << "(>^ω^<)喵" << endl;}
};class Dog : public Animal {
public:virtual void talk() const override {cout << "汪汪" << endl;}
};// 统一接口:接收基类引用
void letsHear(const Animal& animal) {animal.talk(); // 运行时确定调用哪个版本
}int main() {Cat cat;Dog dog;letsHear(cat); // 输出:(>^ω^<)喵letsHear(dog); // 输出:汪汪return 0;
}
注意:
本文重点讲解运行时多态,因为它是C++面向对象的核心,也是面试高频考点;编译时多态相对简单,日常开发中使用频率也较低。
二、多态的定义及实现
2.1 多态的构成条件
要实现运行时多态,必须同时满足以下两个核心条件:
- 条件1:必须通过基类的指针或引用调用虚函数
- 条件2:被调用的函数必须是虚函数,且派生类必须对基类的虚函数完成重写(覆盖)
为什么必须用基类指针/引用?
因为只有基类的指针或引用才能“兼容”指向基类对象和派生类对象,普通基类对象无法做到这一点(会发生切片,丢失派生类特性)。
2.1.1 虚函数
虚函数是多态的“开关”,定义方式:在类成员函数前加virtual关键字。
// 虚函数定义示例
class Person {
public:// 虚函数:加virtual关键字virtual void BuyTicket() {cout << "买票-全价" << endl;}// 注意:非成员函数不能加virtual(编译报错)// virtual void func() {} // 错误:全局函数不能是虚函数
};class Student : public Person {
public:// 派生类虚函数:建议显式加virtual(规范)virtual void BuyTicket() {cout << "买票-打折" << endl;}
};
注意:
1.virtual只能修饰类成员函数,不能修饰全局函数、静态成员函数、构造函数;
2. 派生类的虚函数可以省略virtual(因为继承后基类虚函数的“虚属性”会保留),但不建议这样写,会降低代码可读性。
2.1.2 虚函数的重写(覆盖)
重写(覆盖)是指:派生类中有一个与基类完全相同的虚函数(返回值类型、函数名、参数列表必须完全一致),此时派生类的虚函数会“覆盖”基类的虚函数。
// 虚函数重写示例
#include <iostream>
using namespace std;class Person {
public:// 基类虚函数virtual void BuyTicket() {cout << "Person: 买票-全价" << endl;}
};class Student : public Person {
public:// 派生类重写虚函数(返回值、函数名、参数列表完全一致)virtual void BuyTicket() override { // override关键字:检测重写是否正确cout << "Student: 买票-半价" << endl;}
};class Soldier : public Person {
public:// 派生类重写虚函数virtual void BuyTicket() override {cout << "Soldier: 买票-优先" << endl;}
};// 统一接口:基类指针
void Func(Person* ptr) {ptr->BuyTicket(); // 运行时确定调用哪个版本
}int main() {Person ps;Student st;Soldier sr;Func(&ps); // 输出:Person: 买票-全价Func(&st); // 输出:Student: 买票-半价Func(&sr); // 输出:Soldier: 买票-优先return 0;
}
2.1.3 析构函数的重写(面试重点)
析构函数的重写是一个特殊场景:基类析构函数为虚函数时,派生类析构函数无论是否加virtual,都与基类析构函数构成重写。
原因:编译器会将所有析构函数的名称统一处理为destructor,因此即使派生类析构函数名与基类不同,也能构成重写。
// 析构函数重写的重要性(避免内存泄漏)
#include <iostream>
using namespace std;class A {
public:// 基类析构函数:加virtualvirtual ~A() {cout << "~A()" << endl;}
};class B : public A {
public:B() {_p = new int[10]; // 动态申请内存}// 派生类析构函数:无需显式加virtual(但建议加)~B() override {delete[] _p; // 释放内存cout << "~B(): 释放了int数组" << endl;}private:int* _p;
};int main() {A* p1 = new A;A* p2 = new B;delete p1; // 输出:~A()(正确)delete p2; // 输出:~B(): 释放了int数组 → ~A()(正确,无内存泄漏)return 0;
}
面试必问:为什么基类析构函数建议设计为虚函数?
如果基类析构函数不是虚函数,当用基类指针指向派生类对象并删除时,只会调用基类析构函数,不会调用派生类析构函数,导致派生类中动态申请的内存无法释放,造成内存泄漏。
2.2 易混淆概念对比
C++中重载、重写(覆盖)、重定义(隐藏)是三个容易混淆的概念,这里用表格清晰对比:
概念 | 定义 | 作用范围 | 函数名 | 参数列表 | 返回值 | virtual关键字 |
---|---|---|---|---|---|---|
重载 | 同一作用域内的同名函数 | 同一类 | 相同 | 不同 | 可以不同 | 无关 |
重写(覆盖) | 派生类重写基类的虚函数 | 基类与派生类 | 相同 | 相同 | 相同(协变除外) | 基类必须有,派生类可省略 |
重定义(隐藏) | 派生类与基类同名函数(非重写) | 基类与派生类 | 相同 | 可以相同/不同 | 可以不同 | 基类无virtual或参数不同 |
// 重载、重写、重定义对比示例
#include <iostream>
using namespace std;class Base {
public:// 重载:同一类中,函数名相同,参数不同void func() {cout << "Base::func()" << endl;}void func(int x) {cout << "Base::func(int x)" << endl;}// 虚函数:可被重写virtual void virtualFunc() {cout << "Base::virtualFunc()" << endl;}// 非虚函数:会被派生类重定义(隐藏)void nonVirtualFunc() {cout << "Base::nonVirtualFunc()" << endl;}
};class Derived : public Base {
public:// 重写(覆盖):重写基类虚函数virtual void virtualFunc() override {cout << "Derived::virtualFunc()" << endl;}// 重定义(隐藏):与基类nonVirtualFunc同名,参数相同但基类无virtualvoid nonVirtualFunc() {cout << "Derived::nonVirtualFunc()" << endl;}// 重定义(隐藏):与基类func同名但参数不同void func(double x) {cout << "Derived::func(double x)" << endl;}
};int main() {Derived d;Base* b = &d;b->func(); // 调用Base::func()b->func(10); // 调用Base::func(int x)b->virtualFunc(); // 调用Derived::virtualFunc()(重写,多态)b->nonVirtualFunc(); // 调用Base::nonVirtualFunc()(非虚函数,不构成多态)d.func(3.14); // 调用Derived::func(double x)d.Base::func(); // 显式调用基类被隐藏的函数return 0;
}
三、纯虚函数与抽象类
3.1 纯虚函数
纯虚函数是一种特殊的虚函数:在声明时初始化为0,且没有函数体。它的作用是强制派生类必须重写该函数。
// 纯虚函数定义
class Shape {
public:// 纯虚函数:=0表示没有函数体virtual double area() const = 0; // 普通虚函数:可以有函数体virtual void printInfo() const {cout << "这是一个图形" << endl;}
};
3.2 抽象类
含有纯虚函数的类称为抽象类(也叫接口类)。抽象类有以下特性:
- 抽象类不能实例化对象(无法创建具体实例)
- 抽象类的派生类必须重写所有纯虚函数,否则该派生类仍为抽象类
- 抽象类可以定义普通成员函数和成员变量
- 可以声明抽象或引用(这是多态的基础)
// 抽象类示例
#include <iostream>
using namespace std;// 抽象类:含有纯虚函数
class Shape {
public:// 纯虚函数:计算面积virtual double area() const = 0;// 纯虚函数:计算周长virtual double perimeter() const = 0;// 普通成员函数void print() const {cout << "面积: " << area() << ", 周长: " << perimeter() << endl;}
};// 派生类:圆
class Circle : public Shape {
public:Circle(double r) : _radius(r) {}// 必须重写所有纯虚函数double area() const override {return 3.14 * _radius * _radius;}double perimeter() const override {return 2 * 3.14 * _radius;}private:double _radius; // 半径
};// 派生类:矩形
class Rectangle : public Shape {
public:Rectangle(double w, double h) : _width(w), _height(h) {}// 必须重写所有纯虚函数double area() const override {return _width * _height;}double perimeter() const override {return 2 * (_width + _height);}private:double _width; // 宽double _height; // 高
};// 多态应用:统一接口操作不同图形
void showShapeInfo(const Shape& shape) {shape.print();
}int main() {// 错误:抽象类不能实例化对象// Shape shape; // 正确:可以声明抽象类的指针/引用Circle circle(5.0);Rectangle rect(3.0, 4.0);showShapeInfo(circle); // 输出:面积: 78.5, 周长: 31.4showShapeInfo(rect); // 输出:面积: 12, 周长: 14return 0;
}
抽象类的应用场景:
当我们需要定义一个基类,但不希望它被实例化,只作为派生类的接口规范时,就可以使用抽象类。例如:
- 图形类(Shape):派生类可以是圆形、矩形、三角形等
- 动物类(Animal):派生类可以是猫、狗、鸟等
- 设备类(Device):派生类可以是打印机、扫描仪、投影仪等
四、多态的底层原理
C++多态的底层是通过虚函数表(Virtual Table,简称vtable)和虚表指针(vpointer,简称vptr)实现的。理解这一机制,能帮你更深入地掌握多态的本质。
4.1 虚函数表(vtable)
当一个类中含有虚函数时,编译器会为该类创建一个虚函数表:
- 虚函数表是一个函数指针数组,存储该类所有虚函数的地址
- 每个含有虚函数的类只有一个虚函数表(所有对象共享)
- 派生类会继承基类的虚函数表,如果重写了基类的虚函数,会用派生类自己的函数地址覆盖虚表中对应的位置
- 如果派生类有新的虚函数,会被添加到虚函数表的末尾
4.2 虚表指针(vptr)
每个含有虚函数的类的对象,都会有一个虚表指针(vptr):
- 虚表指针是对象的第一个成员(存储在对象内存的最前面)
- 虚表指针指向该类的虚函数表
- 对象创建时,编译器会自动初始化vptr,使其指向相应的虚函数表
4.3 多态的实现过程
当通过基类指针/引用调用虚函数时,多态的实现过程如下:
- 通过基类指针/引用访问对象的虚表指针(vptr)
- 通过vptr找到该对象所属类的虚函数表(vtable)
- 在虚函数表中找到对应虚函数的地址
- 调用该地址指向的函数(即派生类重写后的函数)
// 多态底层原理示例
#include <iostream>
using namespace std;class Base {
public:virtual void func1() { cout << "Base::func1()" << endl; }virtual void func2() { cout << "Base::func2()" << endl; }void func3() { cout << "Base::func3()" << endl; } // 非虚函数
private:int _b = 1;
};class Derived : public Base {
public:virtual void func1() override { cout << "Derived::func1()" << endl; }virtual void func3() { cout << "Derived::func3()" << endl; } // 新的虚函数
private:int _d = 2;
};int main() {Base b;Derived d;// 注意:以下输出结果可能因编译器不同而略有差异cout << "Base对象大小: " << sizeof(b) << endl; // 输出:8(4字节_b + 4字节vptr)cout << "Derived对象大小: " << sizeof(d) << endl;// 输出:12(4字节_b + 4字节_d + 4字节vptr)return 0;
}
上述代码的虚函数表结构如下:
Base类的虚函数表
- vtable[0] → &Base::func1
- vtable[1] → &Base::func2
Derived类的虚函数表
- vtable[0] → &Derived::func1(覆盖基类的func1)
- vtable[1] → &Base::func2(继承基类的func2)
- vtable[2] → &Derived::func3(新增的虚函数)
注意:
1. 虚函数表是编译器在编译期生成的,存储在只读数据段(.rodata);
2. 虚表指针是在对象构造时初始化的,指向所属类的虚函数表;
3. 多态会带来轻微的性能开销(多一次指针间接访问),但通常可以忽略不计。
五、常见问题与面试题
Q1:静态成员函数可以是虚函数吗?
A:不可以。因为静态成员函数属于类,不属于某个具体对象,没有this指针,而虚函数的调用需要通过对象的vptr找到vtable,因此静态成员函数不能是虚函数。
Q2:构造函数可以是虚函数吗?
A:不可以。因为对象的vptr是在构造函数执行时初始化的,在构造函数还未执行时,vptr尚未指向正确的虚函数表,因此构造函数不能是虚函数。
Q3:析构函数为什么要设为虚函数?
A:如前文所述,当用基类指针指向派生类对象并删除时,如果基类析构函数是虚函数,会先调用派生类析构函数,再调用基类析构函数,确保资源正确释放;否则只会调用基类析构函数,导致派生类资源泄漏。
Q4:多态有什么优缺点?
A:优点:
1. 提高代码的复用性和可维护性;
2. 提高代码的扩展性,新增派生类不影响原有代码;
3. 接口统一,使用者无需关心具体实现。
缺点:
1. 增加了系统复杂度;
2. 带来轻微的性能开销(虚函数调用需要查表);
3. 可能隐藏错误,调试难度增加。
Q5:什么情况下会发生隐藏(重定义)?
A:派生类中的函数与基类中的函数同名,且不构成重写时,会发生隐藏:
1. 基类函数不是虚函数,派生类函数与基类函数同名(无论参数是否相同);
2. 基类函数是虚函数,但派生类函数与基类函数参数不同。
Q6:如何判断一段代码是否构成多态?
A:同时满足以下条件:
1. 存在继承关系;
2. 基类中存在虚函数,派生类重写了该虚函数;
3. 通过基类的指针或引用调用该虚函数。
六、总结
- 多态分为编译时多态(函数重载、模板)和运行时多态(虚函数);
- 运行时多态的实现条件:基类指针/引用 + 虚函数重写;
- 虚函数重写要求:函数名、参数列表、返回值完全相同(协变除外);
override
关键字用于检查重写是否正确,final
关键字用于禁止重写或继承;- 含有纯虚函数的类是抽象类,不能实例化,派生类必须重写所有纯虚函数;
- 多态的底层是通过虚函数表(vtable)和虚表指针(vptr)实现的;
- 基类析构函数建议设为虚函数,避免派生类对象销毁时的内存泄漏;
七、结语
今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~