📝前言:
上篇文章我们讲了继承,这篇文章我们来讲讲C++面向对象三大特性之一——多态
🎬个人简介:努力学习ing
📋个人专栏:C++学习笔记
🎀CSDN主页 愚润求学
🌄其他专栏:C语言入门基础,python入门基础,python刷题专栏,Linux
文章目录
一,什么是多态
多态允许不同的对象对同一消息做出不同的响应,从代码实现角度来看,多态通常通过继承和方法重写来实现。是同⼀个继承关系的下的不同类对象,去调用同⼀函数,产⽣了不同的行为。
多态分为编译时多态(静态多态)和运⾏时多态(动态多态)
- 编译时多态,如:函数重载和函数模板,通过参数不同达到多种形态。之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时就确定状态的⼀般归为静态
- 运⾏时多态,就是在运行时才找到对应的匹配。例如,子类和父类构成的多态,传不同的对象就会完成不同的行为,且行为是在具体的运行过程才确定。
二,多态
1 虚函数
虚函数:类成员函数前加virtual
关键字,注意非成员函数不可加virtual
修饰
示例:
class Animal
{
virtual void yell()
{
cout << "Animal::yell()" << endl;
}
};
2 多态的构成条件
构成多态的条件:
- 子类要完成对基类的继承
- 子类要重写基类中的虚函数
2.1 虚函数的重写/覆盖
虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数
下面定义一个Aminal
子类:Dog
和一个Cat
,并对虚函数yell
进行重写:
class Animal
{
virtual void yell()
{
cout << "Animal::yell()" << endl;
}
};
class Dog: public Animal // 继承
{
virtual void yell() // 重写
{
cout << "汪汪" << endl;
}
};
class Cat: public Animal
{
virtual void yell()
{
cout << "喵喵" << endl;
}
};
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual
关键字时,也可以构成重写(因为基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性)。
重写的只是实现,虚函数的声明方式被继承下来了。也就是说,我们在多态的情况下调用虚函数时,是:父类虚的函数声明 + 子类的函数实现
2.2 override 和 final关键字
override
:帮助用户检查是否重写final
:禁止派⽣类重写这个虚函数
示例:
用final
修饰虚拟函数:
class Animal
{
virtual void yell() final
{
cout << "Animal::yell()" << endl;
}
};
override
:帮助用户检查是否重写
示例:
// 书写正确不会报错
class Dog: public Animal
{
virtual void yell() override
{
cout << "汪汪" << endl;
}
};
// 如下,书写错误会报错
class Cat: public Animal
{
virtual void yelt() override
{
cout << "喵喵" << endl;
}
};
2.3 重载/重写/隐藏 对比
2.4 协变
协变:基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引用
3 多态的使用
3.1 简单使用
使用多态的条件:
- 必须是基类的指针或者引⽤调用虚函数,因为只有基类的指针或引⽤才能既指向基类对象⼜指向派⽣类对象(当派生类指针传进去就指向派生类对象了,切片)
- 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖
示例:
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void yell()
{
cout << "Animal::yell()" << endl;
}
};
class Dog: public Animal
{
public:
virtual void yell() override
{
cout << "汪汪" << endl;
}
};
class Cat: public Animal
{
public:
virtual void yell() override
{
cout << "喵喵" << endl;
}
};
void Func(Animal* ptr) // 接收一个基类的指针
{
ptr->yell(); // 必须是基类的指针 调用虚函数yell
}
int main()
{
Animal animal;
Dog dog;
Cat cat;
传入三个不同类的指针
Func(&animal);
Func(&dog); // 子类指针传递给基类指针,“向上传型”(切片)
Func(&cat);
return 0;
}
运行结果:
可见:这里ptr->yell()
的结果,和ptr
是什么类型没关系,而是由ptr
指向的对象决定的。
3.2 经典考题
程序输出结果?
答案:
原因:
p
调用父类的成员函数test()
test()
里面调用func()
- 因为指针是
p
指向的是B
类对象,所以调用的应该是B
类的func()
- 但是
B
类的func()
== 父类A
的func()
的实现 +B
类的实现
这也说明了,当父类虚函数有缺省值的时候,子类虚函数不建议再重新赋值。
3.3 析构函数的重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual
关键字,都与基类的析构函数构成重写。(因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理destructor
)
为什么要这样设计呢?
这就得谈到delete
:会自动调用析构函数和 operator delete
先假设子类析构没有与父类析构形成多态:
如果有一个父类类型的指针ptr
指向子类对象,这时候使用delete ptr
,调用的是父类的析构函数,但是无法将这个子类对象的特有成员给释放掉。
但是如果析构函数的名称统一为destructor
,那么这时候子类和父类的析构函数构成多态,使用delete ptr
时,调用的就是子类函数的析构函数,就可以正确释放。
4 纯虚函数和抽象类
在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现,因为这个函数是要派生类被重写的,只需要“声明”即可。
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。如果派生类继承抽象类后,不重写纯虚函数,则这个派生类也是抽象类,不能实例化出对象。
示例:
class Animal // 抽象类
{
public:
virtual void yell() = 0; // 纯虚函数
};
class Dog: public Animal
{
public:
virtual void yell()
{
cout << "汪汪" << endl;
}
};
int main()
{
// Animal animal; // 报错:“Animal”: 无法实例化抽象类
Dog dog;
dog.yell();
return 0;
}
三,多态的原理
1 虚函数表指针与虚函数表
1.1 问题引入
程序的输出是什么?
答案(运行环境为64位系统,vs2022):16
解析:
很明显,这里占用内存的不只有 _b
和_ch
,实际上还有一个_vfptr
占8个字节(虚函数表指针)
什么是虚函数表?
在一个类中,不同的虚构函数的函数指针会被存在虚函数表里面(虚函数表实际上是一个函数指针数组),不同的类有不同的虚函数表。
那什么是虚函数表指针?
顾名思义就是虚函数表的指针,每个类对象里面并不会直接存放虚函数表,而是只会多存放一个虚函数表的指针。
重点知识:
基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各自有独立的虚表。
也就是说:假如通过Animal
类实例化了3个对象,这些对象中有独自的虚函数表指针,但是共享的是一张虚函数表派生类由两部分构成,继承下来的基类和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,自己就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址
派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分
虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
比如,Animal
类里有3个虚函数,则Animal
类对应的虚函数表中就有3个函数指针。
如果,Animal
的派生类Dog
只重写了一个Animal
的虚函数,并且有一个自己虚函数,则Dog
的虚函数表中就有4个虚函数指针(其中 3 个来自 Animal
类(其中一个被覆盖),1 个是 Dog
类自己新增的)。Animal
的虚表和Dog
的虚表是不同的,独立的表(尽管里面可能有重复部分)。
选择题
假设D类先继承B1,然后继承B2,B1和B2基类均包含虚函数,D类对B1和B2基类的虚函数重写了,并且D类增加了新的虚函数,则:()
- A.D类对象模型中包含了3个虚表指针
- B.D类对象有两个虚表,D类新增加的虚函数放在第一张虚表最后
- C.D类对象有两个虚表,D类新增加的虚函数放在第二张虚表最后
答案:选B。
D类有几个父类,如果父类有虚函数,则就会有几张虚表,自身子类不会产生多余的虚表,所以只有2张虚表。子类自己的虚函数只会放到第一个父类的虚表后面。
2 多态执行过程分析
比如我们看下面的代码,思考:这个ptr
是怎么找到其他派生类的重写后的虚函数的呢?
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
实际上,满足多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
底层汇编生成的都是同一段代码:根据指针指向的类的虚拟表指针,去虚拟表指针里面找函数地址。又因为这些函数地址分别代表了不同类的不同虚函数,所以就找到了。
动态绑定与静态绑定
- 对不满足多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定
- 满足多态条件的函数调⽤是在运行时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!