【C++】多态详解——虚函数、重写、原理、抽象类(超详细)

一、多态开篇
前面我们把继承讲完了,今天我们来学习面向对象最后一个、也是最难、最重要的特性——多态。
多态是面试必考、底层原理最难、但写代码最实用的知识点。
如果还没学继承的小伙伴,一定要先看继承:
【C++】继承详解——基类/派生类、作用域、默认函数、菱形继承
今天我会把多态概念、构成条件、虚函数、重写、override/final、抽象类、多态原理、虚表全部讲透,全程高能,希望大家最后能有收获
二、多态的概念
什么是多态?通俗来说:多种形态。
同一件事,不同的对象去做,会表现出不同的结果。
比如:
- 普通人买票:全价
- 学生买票:半价
- 军人买票:优先
比如:
- 猫叫:喵~
- 狗叫:汪汪
这就是多态
多态分为两种:
- 静态多态(编译时多态)
函数重载、模板——编译时就确定调用哪个函数(之前讲过的内容,在我的主页可以查看) - 动态多态(运行时多态)
运行时才确定调用哪个函数——我们今天重点讲这个
三、多态的定义及实现
1. 多态的两个必须条件(背下来)
要实现多态,必须同时满足两个条件:
- 必须通过基类的指针 或者 引用 调用虚函数
- 被调用的函数必须是虚函数,且子类必须对父类虚函数完成重写
两个条件缺一不可!
2. 虚函数
成员函数前面加 virtual 关键字,这个函数就是虚函数。
注意:只有类的成员函数才能加 virtual,普通函数不行!
class Person
{
public:
// 虚函数
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
3. 虚函数的重写(覆盖)
重写(覆盖)的要求:
- 函数名相同
- 参数相同
- 返回值相同
- 两个函数必须都是虚函数
满足这些,子类就重写了父类的虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
// 重写父类虚函数
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
特别注意
子类重写时,不加 virtual 也能构成重写,因为虚函数特性会被继承。
但规范写法必须加 virtual!
4. 多态使用示例
void Func(Person* ptr)
{
// 多态调用:看指向的对象,不看指针类型
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps); // 全价
Func(&st); // 半价
return 0;
}
结果:
买票-全价
买票-半价
这就是多态:同一个函数调用,指向不同对象,执行不同逻辑。
5. 动物叫声示例(经典)
class Animal
{
public:
virtual void talk() const
{}
};
class Dog : public Animal
{
public:
virtual void talk() const
{
cout << "汪汪" << endl;
}
};
class Cat : public Animal
{
public:
virtual void talk() const
{
cout << "喵~" << endl;
}
};
void letsHear(const Animal& animal)
{
animal.talk();
}
int main()
{
Cat cat;
Dog dog;
letsHear(cat); // 喵
letsHear(dog); // 汪汪
return 0;
}
四、多态中的重点坑点
1. 析构函数的重写(面试必考,超重要)
父类析构函数写成虚函数,子类析构函数就会自动构成重写。
为什么?
编译器会把所有析构函数名都处理成 destructor(),所以名字统一了。
为什么要这样做?
避免内存泄漏!如果父类析构不是虚函数,那么当使用父类Person的指针ps指向子类Student对象st后,如果delete释放ps,此时编译器只知道ps是父类,不知道有多态,就会只调用父类析构,那么子类如果有堆上的资源就会造成内存泄漏,只有将父类写成虚函数,子类析构才能自动构成重写,此时编译器就会按照正常顺序析构子类后析构父类!!!
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
~B()
{
cout << "~B()" << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
使用:
int main()
{
A* p2 = new B;
delete p2;
// 如果父类析构不是虚函数,只会调用~A(),造成泄漏!
// 是虚函数,就会先调用~B() 再 ~A()
return 0;
}
结论:基类析构函数尽量写成虚函数!
2. 协变(了解)
重写时返回值可以不同:
- 父类返回:父类指针/引用
- 子类返回:子类指针/引用
这叫协变,实际很少用,了解即可。
3. override / final 关键字(C++11)
(1)override
检查子类函数是否成功重写,写错直接编译报错。
class Car
{
public:
virtual void Drive()
{}
};
class Benz : public Car
{
public:
// 写错会直接报错!
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
(2)final
- 修饰类:该类不能被继承
- 修饰虚函数:该函数不能被重写
class Car
{
public:
virtual void Drive() final
{}
};
// 报错:不能重写final函数
class Benz : public Car
{
virtual void Drive()
{}
};
4. 重载 / 重写 / 隐藏 对比(面试必考)
我给你总结成最清晰表格:
| 概念 | 作用域 | 函数名 | 参数 | 返回值 | 关键字 |
|---|---|---|---|---|---|
| 重载 | 同一类 | 相同 | 不同 | 随意 | 无 |
| 重写 | 父子类 | 相同 | 相同 | 相同 | virtual |
| 隐藏 | 父子类 | 相同 | 随意 | 随意 | 无 |
口诀:
- 同一作用域、参数不同 → 重载
- 父子类、都是虚函数、完全相同 → 重写
- 父子类、同名、不是重写 → 隐藏
五、纯虚函数 和 抽象类
1. 纯虚函数
虚函数后面写 =0,就是纯虚函数。
virtual void Drive() = 0;
2. 抽象类
包含纯虚函数的类,叫抽象类。
抽象类不能实例化对象!
子类必须重写纯虚函数,否则子类也是抽象类。
class Car
{
public:
// 纯虚函数 → 抽象类
virtual void Drive() = 0;
};
class Benz : public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
使用:
// Car car; 报错!
Car* pBenz = new Benz;
pBenz->Drive();
意义:强制子类重写,规范接口。
六、多态的原理(最难!)
1. 虚函数表指针(_vfptr)
只要类里有虚函数,对象中就会多一个指针:_vfptr 虚表指针。
这个指针指向一张表:虚函数表(vftable)。
我们看大小:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
// 32位平台:4+1+3(对齐)+4(虚表指针) = 12字节
cout << sizeof(Base) << endl; // 输出12
2. 多态底层原理
多态调用时:
- 去对象中取 _vfptr
- 通过虚表指针找到 虚函数表
- 在表中找到对应 虚函数地址
- 调用函数
这就是运行时确定,也就是动态多态。
- 父类对象 → 用父类虚表 → 调用父类虚函数
- 子类对象 → 用子类虚表 → 调用子类虚函数
一句话:多态是靠虚表指针 + 虚函数表 + 重写实现的!
3. 静态绑定 vs 动态绑定
- 静态绑定:编译时确定地址(普通函数)
- 动态绑定:运行时查虚表确定(多态)
满足多态条件就是动态绑定,否则静态绑定。
4. 虚表存放位置
- 虚函数:代码段
- 虚表:代码段(常量区)
- 虚表指针:在对象内部(栈/堆)
七、多态知识点总结
- 多态是父类指针指向不同对象,同样函数调用产生不同行为
- 多态条件:虚函数 + 重写 + 指针/引用
- 虚函数:virtual + 成员函数声明
- 重写:父子类关系、父类是虚函数、函数声明完全相同
- 析构函数建议全部写成虚函数,防止造成内存泄漏
- override 的作用是检查重写,final 的作用是禁止重写/继承
- 纯虚函数就是虚函数声明后面加上 = 0 → 有纯虚函数的类是抽象类,不能实例化
- 多态底层:虚表指针 + 虚函数表
- 动态绑定:运行时查虚表;静态绑定:编译时确定
那么今天关于C++ 多态就全部讲完了,内容非常多、非常细,大家一定要多画图、多敲代码。
有什么不懂欢迎私信问我,我会及时做出解答!
下一篇我们开始学习二叉搜索树,敬请期待吧!
bye~
更多推荐

所有评论(0)