文章目录
前言
我们知道C++语言的三大特性(封装,继承,多态)在之前的文章中我们已经学习了封装和继承,在这篇文章中我们将会学习到有关多态的相关问题。
一、多态的概念
多态:完成一件事情,不同的人会产生不同的行为,产生不同的状态。
例子:平时手机上买票,如果我们是大学生,就是半价票,如果是成年人,就是正常票价,如果是儿童,就是免票。
本质都是买票这个动作,但是不同的人买会有不同的价位,
二、多态的实现与虚函数
多态是在不同继承关系的类对象,去调用同一函数产生了不同行为,多态实在继承的基础上完成的
实现多态其实还有两个条件
必须通过父类的指针或者引用调用虚函数。
子类必须对父类的虚函数进行重写
这里出现了两个概念:重写,虚函数。我们依次来看一看。
虚函数
被关键字virtual修饰的类成员方法就是虚函数
class Person
{
public:
virtual void Buy()
{
cout << "Person::成人票" << endl;
}
};
这就是一个虚函数,注意virtual加的位置
重写
父类函数和子类函数拥有同样的名字,返回值,参数,必须是虚函数
class Person
{
public:
virtual void Buy(int a)
{
cout << "Person::成人票" << endl;
}
};
class Student :public Person
{
public:
virtual void Buy(int a)
{
cout << "Student :: 半价票" << endl;
}
};
我们看一下这段代码是否是重写
子类成员并没有加virtual,但是仍然构成重写,虽然可以这样写,但是我们不推荐,一般建议都加上virtual。
子类的虚函数重写了父类的虚函数
这就完成了重写工作,但是重写操作有两个特例
🌟🌟1.协变
协变要求,返回值可以不同,但是对返回值也有要求,必须返回父类和子类的指针或者引用,可以不是这两个父子类,只要满足父子类就可以。
🌟🌟2.析构函数
我们看一下这段代码有没有问题
class Person
{
public:
~Person()
{
cout << " ~Person()" << endl;
}
};
class Student :public Person
{
public:
~Student()
{
cout << " ~Student()" << endl;
}
};
int main()
{
Person p;
Student s;
return 0;
}
我们运行看一下
我们发现并没有问题,我们在继承中学习过,析构子类之后自动调用父类的析构函数。
我们在来看一下多态的条件,必须通过父类的指针或者引用去调用才可以。
我们用父类指针调用一下多态看一下
我们new了一个Student的对象,但是他并没有走析构函数,如果里面有资源没有清理,就会造成内存泄漏,就会出现问题。
我们需要也把析构函数定义成虚函数
但是他们两个名字不相同,怎末也构成重写了呢??
编译器对析构函数做了特殊处理,编译后析构函数的名称统一处理成destructor
三.final和override介绍
这两个关键字是c++11新增的
1.final
我们如果想要实现一个类不能被继承怎末实现呢??
🌟🌟我们可以把父类的构造函数设置为private,这样子类在继承之后就初始化不出对象,也就不能实现继承了
🌟🌟我们在类的后面加一个关键字finial,变成最终类
final还有其他用途,修饰虚函数,虚函数不可以被重写。
2.override
override用于检查虚函数是否完成重写,如果没有则报编译错误
四.对比重载,重定义,重写
🌟重载:同一作用域中,函数名相同,函数参数不同的函数
🌟 重定义(隐藏):父类和子类两个不同的作用域,只要父类和子类的函数名相同就可以。
🌟重写(覆盖):两个函数分别在子类和父类的作用域,两个函数必须是虚函数,这两个函数函数名,参数,返回值相同(有例外,特殊考虑)。
🌟如果子类和父类中既满足重写又满足隐藏,重写优先
五.抽象类
在虚函数后面加上=0,就属于纯虚函数,拥有纯虚函数的类称为抽象类。抽象类不用做定义对象,只作为基类被继承
抽象类定义不出对象,抽象类的继承也定义不出对象。
只有抽象类的继承重写了纯虚函数才可以定义出对象
接口继承和是实现继承
普通函数调用是实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
多态调用是接口继承,派生类继承的是父类虚函数的接口,目的是为了重写,达成多态。
六.多态的原理
我们来看一下这段代码的结果是多少
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
如果按照我们之前的理解,这里的class类里面只存储成员变量,不存储成员方法,只有一个int类型,大小为4字节,答案就是4。
我们运行一下看一看
这里的结果是8,这是为什么呢??
我们打开监视窗口看一看
我们看到,这里面不仅仅存了一个b还存了一个vfptr,看后面的值,好像是个指针。
这个指针我们叫做虚函数表指针,虚函数表中存放的是虚函数的地址。一个含有虚函数的类都会有一个虚函数表指针。
我们对上面的代码进行增加,再来理解一下这一块
1.我们增加一个派生类Derive去继承Base
2.Derive中重写Func1
3.Base再增加一个虚函数Func2和一个普通函数Func3
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 Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
我们到监视窗口看一下
🌟派生类d中不仅仅包含自己的成员变量,还包括继承父类的那一部分,虚函数表也被继承下来
🌟我们发现父类的虚函数表指针第一个元素和子类中继承的虚函数表指针并不相同,我们在子类中完成了对Fun1的重写,所有子类的Fun1覆盖了父类的Fun1。
🌟我们在父类中实现了三个函数,其中只有虚函数才可以进入函数表
🌟虚函数表本质是一个存储虚函数指针的指针数组。
我们在vs下会发现,这个虚函数表最后一个位置回放一个nullptr
🌟派生类虚表总结:先将基类的虚表内容拷贝一份到派生类虚表中,如果子类中完成了父类某个虚函数重写,这部分要被覆盖。
派生类自己新增加的虚函数按照声明次序增加到派生类虚表后面
这个虚函数指针,虚函数,虚函数表分别存在哪里呢??
虚函数肯定和普通函数一样,存在代码段中的,虚函数指针存在于对象中,经过测试,虚函数表也是存在代码段中。
多态本质就是一个这样的模型
多态是如何通过调用不同的对象完成不同的任务的呢??
调用时,在指向对象的虚函数表中找虚函数的地址,进行调用
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
}
调用Mike对象,就去Person中的虚函数表中找对应的虚函数地址进行调用。
调用Johnson对象,就去Student中的虚函数表中找对应的虚函数地址进行调用。
七.动态绑定与静态绑定
动态绑定:在运行时,根据具体拿到的类型确定程序的行为,调用哪一个,也称为后期绑定,晚绑定
静态绑定: 在编译时,确定类型,去符号表中找对应的地址进行调用具体函数,也称为前期绑定,早绑定
我们分别来看一下动态绑定和静态绑定的汇编代码。
动态绑定:运行起来以后去对象中找对应的地址
静态绑定:编译时已经从符号表确定了函数地址
总结
以上就是今天要讲的内容,本文仅仅详细介绍了C++多态的相关内容。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘