多态的原理与实现机制
目录
一、多态的基本原理
二、虚函数表机制
三、多态的实现条件分析
1、为什么需要虚函数重写?
2、为什么必须使用父类指针或引用?
3、为什么直接使用父类对象无法实现多态?(重点!!!)
四、总结
五、综上的问题解惑
1、核心原因:对象的类型决定了它的虚表
2、Person p2 = Johnson; 背后发生了什么?
总结与对比
一句话总结:
一、多态的基本原理
多态是面向对象编程的核心特性之一,它允许不同类的对象对同一消息做出不同的响应。在C++中,多态主要通过虚函数机制实现。
以下面的代码为例,分析多态的工作原理:思考一下为什么当父类Person指针指向的是父类对象Mike时,调用的就是父类的BuyTicket,当父类Person指针指向的是子类对象Johnson时,调用的就是子类的BuyTicket?
#include <iostream>
using namespace std;// 父类
class Person
{
public:virtual void BuyTicket() // 虚函数声明{cout << "买票-全价" << endl;}int _p = 1; // 成员变量
};// 子类
class Student : public Person
{
public:virtual void BuyTicket() // 重写虚函数{cout << "买票-半价" << endl;}int _s = 2; // 子类特有成员变量
};int main()
{Person Mike;Student Johnson;Johnson._p = 3; // 修改父类成员变量值,便于观察切片效果Person* p1 = &Mike; // 父类指针指向父类对象Person* p2 = &Johnson; // 父类指针指向子类对象(切片)p1->BuyTicket(); // 输出:买票-全价p2->BuyTicket(); // 输出:买票-半价return 0;
}
二、虚函数表机制
通过调试分析可以发现:(之前好像讲解过,忘了,再写一遍也无妨)
-
对象
Mike
中包含一个成员变量_p
和一个虚表指针 -
对象
Johnson
中包含两个成员变量_p
和_s
以及一个虚表指针 -
这两个对象的虚表指针分别指向各自的虚函数表,而通过虚函数表我们能找到表里面保存的虚函数的地址
多态的实现原理如下:
-
父类指针
p1
指向Mike
对象,p1->BuyTicket()
在Mike
的虚表中找到的虚函数是Person::BuyTicket
-
父类指针
p2
指向Johnson
对象,p2->BuyTicket()
在Johnson
的虚表中找到的虚函数是Student::BuyTicket
这样就实现了不同对象执行同一行为时,展现出不同形态的多态特性。
三、多态的实现条件分析
多态的实现需要满足两个条件:
-
完成虚函数的重写(覆盖)
-
必须使用父类的指针或引用调用虚函数
1、为什么需要虚函数重写?
虚函数重写是为了实现子类虚表中虚函数地址的覆盖。当子类重写父类的虚函数时,子类的虚表会使用子类重写后的函数地址覆盖父类对应的虚函数地址。
2、为什么必须使用父类指针或引用?
使用父类指针或引用时,实际上发生的是切片行为:只会获取父类对象或子类对象中属于父类的那一部分。这样,通过父类指针或引用调用虚函数时,能够根据实际指向的对象类型找到正确的虚表,从而调用正确的虚函数。
Person* p1 = &Mike; // 切片:获取Mike中Person部分
Person* p2 = &Johnson; // 切片:获取Johnson中Person部分
因此,我们后序用p1和p2调用虚函数时,p1和p2通过虚表指针找到的虚表是不一样的,最终调用的函数也是不一样的。
3、为什么直接使用父类对象无法实现多态?(重点!!!)
Person p1 = Mike; // 调用拷贝构造函数
Person p2 = Johnson; // 切片后调用拷贝构造函数
使用父类对象时:
-
切片操作会获取子类对象中属于父类的部分成员变量
-
调用父类的拷贝构造函数进行拷贝构造
-
关键点:拷贝构造出来的父类对象
p1
和p2
中的虚表指针指向的都是父类的虚表(同类型的对象共享一张虚表)
因此,通过p1
和p2
调用虚函数时,都会通过父类的虚表找到父类的虚函数,无法实现多态效果。
四、总结
-
构成多态时:指向哪个类的对象就调用哪个类的虚函数(与对象实际类型相关)
-
不构成多态时:对象类型是什么就调用哪个类的虚函数(与指针/引用类型相关)
多态机制通过虚函数表和动态绑定实现了运行时的多态性,是面向对象编程中实现代码扩展性和灵活性的重要基础。
五、综上的问题解惑
在“Person p2 = Johnson; // 切片后调用拷贝构造函数”中调用拷贝构造函数,Johnson的父类成员变量被拷贝过去了,那为什么那个虚表指针却不是Johnson重写的虚函数表呢?而是Mike中的虚函数表。为什么?
回答:
这个问题是理解C++对象模型的一个关键点!Johnson
确实有它自己的虚表指针,但拷贝后p2
的虚表指针却变了。原因在于:拷贝构造函数的行为是“按类型构造”,而不是“按原样复制”。
1、核心原因:对象的类型决定了它的虚表
在C++中,每个对象的虚表指针是在构造函数中初始化的。一个Person
对象的虚表指针永远指向Person
的虚表,一个Student
对象的虚表指针永远指向Student
的虚表。这是铁律。
2、Person p2 = Johnson;
背后发生了什么?
这行代码的准确说法是:用Johnson
对象中属于Person
的部分作为参数,调用Person
类的拷贝构造函数,构造了一个全新的、完整的Person
类对象p2
。
这个过程可以分解为:(重点理解!!!)
-
切片(Slice):编译器从
Johnson
对象中提取出属于Person
类的成员(也就是_p
和虚表指针__vptr
)。 -
构造新对象:调用
Person
类的拷贝构造函数Person(const Person&)
。 -
关键步骤:在
Person
的拷贝构造函数内部(无论是编译器生成的还是自定义的),它要做的就是构造一个Person
对象。因此,它会按照构造Person
对象的规则来:-
初始化
p2
的成员变量_p
:将切片得到的值(Johnson._p = 3
)拷贝过来。 -
初始化
p2
的虚表指针__vptr
:绝不会拷贝Johnson
的虚表指针,而是将其设置为Person
类的虚表地址。因为现在是在构建一个Person
对象,它的虚表理所当然应该是Person
的虚表。
-
总结与对比
操作 | 代码 | 结果对象类型 | 虚表指针来源 | 是否多态 |
---|---|---|---|---|
指针/引用 | Person* p2 = &Johnson; | Student (实际) | Johnson 对象的 (Student 虚表) | 是 |
对象切片 | Person p2 = Johnson; | Person (编译时确定) | Person 类的拷贝构造函数 (Person 虚表) | 否 |
一句话总结:
对象切片 Person p2 = Johnson;
创建的是一个全新的、纯粹的Person
对象,它继承的是Johnson
的数据,但它的身份(虚表指针) 由它自己的类型(Person
)决定。而指针/引用是直接指向原有对象,不会创建新对象,因此身份保持不变。