【C++】揭开C++多态的神秘面纱

【C++】揭开C++多态的神秘面纱

码农世界 2024-05-29 前端 112 次浏览 0个评论

目录

初识多态

多态的条件

接口继承和实现继承

override 和 final

多态原理

继承与虚函数表

析构函数与多态

抽象类


本篇内容关联知识的链接

【C++】详解C++的继承-CSDN博客

【C++】详解C++的模板-CSDN博客

【C++】C++的内存管理-CSDN博客

初识多态

父类被不同子类继承后,父类呈现出不同的状态

假设有人这个类,这个类的状态是工作。当人被学生类继承后,人这个类的状态就变成了学习。当人被程序员类继承后,人这个类的状态就变成了写代码。当人被厨师类继承后,人这个类的状态就变成了做饭。

多态的条件

 必须通过父类的指针或者引用调用虚函数  被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写
虚函数 被 virtual 修饰的类 成员函数 称为虚函数
class Person {
public:
 virtual void BuyTicket() { cout << "工作" << endl;}
};
虚函数重写 子类中有一个跟父类完全相同的虚函数 ( 即子类虚函数与父类虚函数的 返回值类型、函数名字、参数列表完全相同 ) ,称子类的虚函数重写了父类的虚函数。 . 协变 ( 基类与派生类虚函数返回值类型不同 ) 派生类重写父类虚函数时,与父类虚函数 返回值类型 不同。即父类虚函数返回父类对象的指 针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。 如下是协变示例代码
class A{};
class B : public A {};
class Person {
public:
 virtual A* f() {return new A;}
};
class Student : public Person {
public:
 virtual B* f() {return new B;}
};
重写是用子类函数的实现把父类函数的实现覆盖掉 完成重写后,再去调用子类中父类的虚函数时,不再是父类原来的虚函数 如下是多态代码示例
#include
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "工作" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "学习" << endl; }
};
class programmer : public Person { 
public:
	virtual void BuyTicket() { cout << "写代码" << endl; }
};
class cook : public Person { 
public:
	virtual void BuyTicket() { cout << "做饭" << endl; }
};
void Func(Person& p) //父类的指针或引用
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	programmer pr;
	cook ck;
	Func(ps);
	Func(st); 
	Func(pr);
	Func(ck); 
	return 0;
}

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数

override 和 final

final : 修饰虚函数,表示该虚函数不能再被重写 如下是代码示例
class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};
override : 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错 如下是代码示例
class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

多态原理

类对象的顶部存有一个指针,该指针指向虚函数表,虚函数表中存放虚函数的指针。

既然该指针存放在对象的顶部,就会有个很容易混淆的问题:

如果有个指针指向对象,那么不同类型的指针会有不同的含义。对象指针:指向该对象。二级指针:指向——>指向虚函数表的指针

虚函数表是一个函数指针数组,虚函数表的最后一般以空结尾,和字符串以\0结尾类似,都是为了控制结束条件。

虚函数表中只存虚函数的函数指针

那么便容易理解为什么要调用父类对象的指针或引用了

从语言层理解:多态是父类在不同的子类中呈现不同状态,既然如此,用父类的指针或引用调用一样的函数,却能呈现不同的结果,才能对比出多态的意义。

从底层理解:指向虚函数表的指针是存给父类的。因为在子类的内存模型中,父类的数据在上,子类的数据在下。

用父类类型指针去调用子类中父类虚函数发生了切片

理解多态原理:

因为每个父类中都有一张自己的虚函数表(有虚函数才有虚函数表,要形成多态的的话父类一定会有虚函数),而子类中父类的虚函数表中的虚函数被子类重写了。

即使用相同类型的指针或引用去调用,即使被调用函数的函数名,参数,返回值也相同。

但重写之后,函数的具体实现不同了,这就实现了多态。

继承与虚函数表

不同的继承 虚函数表是怎么存虚函数指针呢

单继承

代码示例如下

class Base { 
public :
 virtual void func1() { cout<<"Base::func1" < 

模型如下

多继承

代码示例

class Base1 {
public:
 virtual void func1() {cout << "Base1::func1" << endl;}
 virtual void func2() {cout << "Base1::func2" << endl;}
private:
 int b1;
};
class Base2 {
public:
 virtual void func1() {cout << "Base2::func1" << endl;}
 virtual void func2() {cout << "Base2::func2" << endl;}
private:
 int b2;
};
class Derive : public Base1, public Base2 {
public:
 virtual void func1() {cout << "Derive::func1" << endl;}
 virtual void func3() {cout << "Derive::func3" << endl;}
private:
 int d1;
};

模型如下

子类中有多个父类都有虚函数,那么会建多张虚函数表,第一个父类建的虚函数表是第一张虚函数表。

如果是多继承,子类自己的虚函数会存于第一张虚函数表中。

上述模型中,父类1和父类2的func1都被子类重写,但第一张虚函数表中存的才是func1的函数指针。第二张表中存的是经过封装后的函数指针,该指针最终会去第一张虚函数表中找func1的函数指针,而func2没有被子类重写,两张表中存的是各自的函数指针,两个func2也没有关系,因为在不同的类域中(了解即可)

析构函数与多态

父类的析构函数只要加上关键字virtual就能和子类构成多态关系,虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。

父类的析构为什么要和子类的析构构成重载关系呢

请看如下场景

class Person {
public:
  ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
  ~Student() { cout << "~Student()" << endl; }
};
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;
delete p1;
 delete p2;
 return 0;
}

那么上述场景中,资源会被清理干净吗

显然不会,因为只调用了两次Person类的析构,并没有调用Student类的析构。为什么呢

这是因为p1,p2的指针类型是父类的,delete只会调用父类的析构函数。

这时应该让Student类的析构与Person类的析构构成多态关系,让Student类的析构函数重写Person类的析构函数。

此时delete再通过父类类型的指针调用父类的析构时,实际上调用的是子类的析构函数,子类的析构函数。子类的析构函数在清理子类数据时,也会调用父类的析构函数清理父类数据。这样就完成了资源的清理。

如下图

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。 代码示例如下
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 virtual void Drive()
 {
 cout << "Benz-舒适" << endl;
 }
};
class BMW :public Car
{
public:
 virtual void Drive()
 {
 cout << "BMW-操控" << endl;
 }
};
void Test()
{
Car* pBenz = new Benz;
 pBenz->Drive();
 Car* pBMW = new BMW;
 pBMW->Drive();
}

转载请注明来自码农世界,本文标题:《【C++】揭开C++多态的神秘面纱》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,112人围观)参与讨论

还没有评论,来说两句吧...

Top