多态
什么是多态?
父类的引用或者指针指向子类对象。
C++支持编译时多态(静态多态——即运算符重载和函数重载)和运行时多态(动态多态——即派生类和虚函数)。
静态多态和动态多态的区别
函数地址是早绑定(静态联编)还是晚绑定(动态联编)。
- 如果函数的调用,在编译节点就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就说地址是早绑定的;
- 如果函数的调用地址不能编译不能在编译期确定,而需要在运行时才能决定,这就属于晚绑定(动态多态,运行时多态)。
静态联编的例子
下面代码中cat继承自animal类,对于函数doSpeak,他声明的参数是animal类型,因此调用doSpeak的时候,即使你传入的参数是cat类型,运行结果也是调用animal类型的speak函数
#include<iostream>
using namespace std;
class Animal {
public:
void speak() {
cout << "动物在说话" << endl;
}
};
class Cat :public Animal {
public:
void speak() {
cout << "小猫在说话" << endl;
}
};
void doSpeak(Animal & animal) //Animal & animal = cat
{
animal.speak();
}
void test01() {
Cat cat;
//如果发生了继承的关系,编译器允许进行类型转换
//因此函数doSpeak里面的参数要求是Animal类型,传入子类cat类型也不会报错
doSpeak(cat);
}
原因:
上述代码中,调用doSpeak的时候,会在编译阶段找参数的类型,因此调用的是animal类型的speak。speak函数的地址在编译时期就已经绑定好了——早绑定——静态联编。
如果想调用cat类型的speak,那就需要进行晚绑定,即在运行时确定函数地址,即动态联编
动态联编(讲到了重写的概念)
写法——将speak改成虚函数
class Animal {
public:
virtual void speak() {
cout << "动物在说话" << endl;
}
};
修改后的运行结果
此时我们看一下animal的大小,执行以下程序获得类animal的大小为4
#include<iostream>
using namespace std;
class Animal {
public:
virtual void speak() {
cout << "动物在说话" << endl;
}
};
class Cat :public Animal {
public:
void speak() {
cout << "小猫在说话" << endl;
}
};
void test02(){
cout << sizeof(Animal) << endl;
}
原因:
animal内部的结构中有一个指针vfptr(虚函数指针 virtual function pointer),该指针指向了一张虚函数表中的animal::speak的地址。表中结构如下:
对于类cat,如果是如下写法:
class Animal {
public:
virtual void speak() {
cout << "动物在说话" << endl;
}
};
class Cat :public Animal {
public:
};
则此时cat类的内部结构如下:
首先cat类会把animal类的vfptr指针继承下来,还会把父类的虚函数表继承下来。调用构造函数时,将所有虚函数表指针都指向自己的虚函数表,这个操作我们是看不到的
将speak函数写回cat中
class Cat :public Animal {
public:
void speak() {
cout << "小猫在说话" << endl;
}
};
则cat会用这句的speak覆盖掉存储在自己虚表中的父类的speak函数(父类的speak还在)。子类重新写父类中的虚函数speak——重写。重写的返回值、参数个数、类型、参数顺序都相同。
重写并不会改变父类中的东西,只是把子类的虚表中的存有的父类的东西进行了覆盖。
此时cat的内部结构如下:
则此时运行如下代码时,发生了多态,使得父类指针指向子类,调用的是cat里面的speak函数
Animal *animal = new Cat;
animal->speak();
那么父类指针指向子类是怎么实现的?(下面讲解通过函数指针调用cat里面的speak函数)
1、Animal *animal 是一个父类指针,找到了地址指向图中箭头所指的位置。
找到地址之后,做了一个类型的强制转换,以此来设置步长
(int*)animal
之后对改地址进行取 * 操作,从而找到了自己的虚函数表。虚函数表内部也是一个数组,数组的类型也是int*,因此代码变为:
(int)*(int*)animal
对于上侧代码再一次进行取 * 操作即可获得函数地址
*(int)*(int*)animal
由于函数指针可以指向函数的地址,因此可得如下函数指针
((void(*)()) (*(int)*(int*)animal))()//后面跟着的()起调用函数的作用
因此,这两句的作用是相同的。
animal->speak();
((void(*)()) (*(int*)*(int*)animal))();
练习,如下代码,调用cat类里面的eat函数
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Animal {
public:
virtual void speak() {
cout << "动物在说话" << endl;
}
virtual void eat() {
cout << "动物在吃饭" << endl;
}
};
class Cat :public Animal {
public:
void speak() {
cout << "小猫在说话" << endl;
}
virtual void eat() {
cout << "小猫在吃鱼" << endl;
}
};
void test02(){
//父类指针指向子类对象 多态
Animal * animal = new Cat;
//animal->speak();
// *(int*)*(int*)animal 函数地址
//((void(*)()) (*(int*)*(int*)animal))();
// *((int*)*(int*)animal+1)猫吃鱼的地址
//((void(*)()) (*((int*)*(int*)animal + 1)))();
}
int main() {
test02();
system("pause");
return EXIT_SUCCESS;
}
上侧已经找到了cat的speak的地址
(int*)*(int*)animal
将该指针+1,再对整体取地址,则找到了eat的地址
*((int*)*(int*)animal+1)
通过函数指针即可调用cat类的eat函数
((void(*)()) (*((int*)*(int*)animal + 1)))();
做一个计算器
第一版——每次有新功能,都要修改类中的代码,难以维护和扩展
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Calculator{
public:
void setv1(int v){
this->val1 = v;
}
void setv2(int v){
this->val2 = v;
}
int getResult(string oper){
if (oper == "+"){
return val1 + val2;
}
else if (oper == "-"){
return val1 - val2;
}
}
private:
int val1;
int val2;
};
void test01(){
Calculator cal;
cal.setv1(10);
cal.setv2(10);
cout << cal.getResult("+") << endl;
cout << cal.getResult("-") << endl;
}
开闭原则
对扩展开放,对修改关闭。
第二版——利用多态
//利用多态实现计算器
class abstractCalculator {
public:
virtual int getResult() { return 0; };
void setv1(int v) {
this->val1 = v;
}
void setv2(int v) {
this->val2 = v;
}
public:
int val1;
int val2;
};
//加法计算器
class PlusCalculator :public abstractCalculator{
public:
virtual int getResult() {
return val1 + val2;
};
};
class SubCalculator : public abstractCalculator {
public:
virtual int getResult() {
return val1 - val2;
};
};
class ChengCalculator :public abstractCalculator {
public:
virtual int getResult() {
return val1 * val2;
};
};
void test02() {
abstractCalculator * abc;
//加法计算器
abc = new PlusCalculator;
abc->setv1(10);
abc->setv2(20);
cout << abc->getResult() << endl;
delete abc;
abc = new SubCalculator;
abc->setv1(10);
abc->setv2(20);
cout << abc->getResult() << endl;
delete abc;
abc = new ChengCalculator;
abc->setv1(10);
abc->setv2(20);
cout << abc->getResult() << endl;
}
抽象类和纯虚函数
上面例子中,父类的这个return 0 ,没有任何意义
virtual int getResult() { return 0; };
因此可以改一下
virtual int getResult() = 0;
改完这一句之后,两种代码的运行结果是一样的,那么他改变了什么?
- 如果父类中有了纯虚函数,那么子类继承父类就必须实现所有的纯虚函数,否则子类也是抽象类。
- 如果子类中有了纯虚函数,那么父类就无法实例化对象了。
- 一个类有了纯虚函数,则这个类叫抽象类——抽象类无法实例化对象
- 纯虚函数不需要做实现,但是在虚表中会为其保留一个位置,但是这个位置不放地址。
虚析构和纯虚析构
虚析构
观察下面代码
#include "pch.h"
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
~Animal();
};
class Cat :public Animal
{
public:
Cat(const char * name)
{
this->m_Name = new char[strlen(name) + 1];
strcpy(this->m_Name, name);
}
virtual void speak()
{
cout << "小猫在说话" << endl;
}
~Cat()
{
cout << "Cat的析构调用" << endl;
if (this->m_Name != NULL)
{
delete[] this->m_Name;
this->m_Name = NULL;
}
}
char * m_Name;
};
void test01()
{
Animal * animal = new Cat("TOM");
animal->speak();
delete animal;
}
int main() {
test01();
system("pause");
return EXIT_SUCCESS;
}
小猫的析构函数并没有被调用。因为普通的析构不会调用子类的析构函数。
原因:
基类指针指向了派生类对象,而基类中的析构函数却是非virtual的,之前讲过,虚函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete时候只会调用基类的析构函数,而不会调用派生类的析构函数。
因此需要将析构函数改写成虚析构函数。
#include "pch.h"
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
//普通析构 是不会调用子类的析构的,所以可能会导致释放不干净
//利用虚析构来解决这个问题
virtual ~Animal()
{
cout << "Animal的析构调用" << endl;
}
};
class Cat :public Animal
{
public:
Cat(const char * name)
{
this->m_Name = new char[strlen(name) + 1];
strcpy(this->m_Name, name);
}
virtual void speak()
{
cout << "小猫在说话" << endl;
}
~Cat()
{
cout << "Cat的析构调用" << endl;
if (this->m_Name != NULL)
{
delete[] this->m_Name;
this->m_Name = NULL;
}
}
char * m_Name;
};
void test01()
{
Animal * animal = new Cat("TOM");
animal->speak();
delete animal;
}
int main() {
test01();
system("pause");
return EXIT_SUCCESS;
}
纯虚析构
virtual ~Animal() = 0;
- 纯虚析构 ,需要声明 还需要实现 类内声明,类外实现
- 如果函数中出现了 纯虚析构函数,那么这个类也算抽象类
- 抽象类 不可实例化对象
虚析构不影响实例化,纯虚析构不可实例化。
向上类型转换向下类型转换
没有多态的情况下
下面的代码就不安全(animal是cat的父类):
new Animal,申请的空间如下图中左侧,cat类型的指针寻址能力如下图中的右侧:
因此上侧的指针类型转换是不安全的。所谓的安全不安全指的是指针的寻址范围。
new一个animal指针,其寻址范围就是animal类的区域。如果强转成cat类,cat类的范围会更大,容易操作到不是自己的内容。
总结:
- 基类转派生类不安全——向下转换——指针寻址范围会超过申请的空间
- 派生类转基类安全——向上转换——指针寻址范围不会超过申请的空间
发生多态的情况
如果发生多态,就总是安全的。
对于下侧代码:
Animal *animal = new Cat;
Cat *cat = (Cat*)animal;
我们 new 了一个cat那么大的空间,而指针的寻址范围是animal那么大的,因此指针不会操作不属于自己的空间,因此是安全的。
哪怕把animal指针强转成cat类型,我们申请的空间就已经有cat那么大了,因此不用担心指针操作不属于自己的空间。