多态
多态是面向对象语言中封装,继承外的第三个基本特征。(封装和继承都是为了多态服务的)
多态性提供了接口和具体实现间的一层隔离。
多态性改善了代码的可读性和组织性,同时使创建的程序具有可扩展性(项目不仅在最初创建时期扩展,当以后项目需要新功能的是时候也能扩展。)
多态分为两种:静态多态和动态多态,静态多态包括函数重载(函数名相同,但是由于参数不同,调用的函数就不同,但是接口只有一个)还有运算符重载等。静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)
静态联编和动态联编
class Animal
{
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
void doSpeak(Animal&animal)
{
animal.speak();
}
int main()
{
Cat cat;
doSpeak(cat);//动物在说话
return 0;地址
}
打印的其实是动物在说话,因为doSpeak函数中传进来的参数的类型就是动物类,所以无论传进来的是小猫对象还是小狗对象调用的都是父类中的speak函数。这种提前将地址绑定好的数据静态联编。
如果想调用小猫说话,那么就不能提前绑定地址,应该在程序进行的时候再绑定地址(动态联编)。
在函数面前加关键字:virtual。
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
这样函数就变成了虚函数。 再运行就变成了小猫在说话。
这样就是动态多态。 动态多态的产生条件:先有继承的关系,父类中有虚函数,子类重写父类中的虚函数。
且父类中的指针或引用指向子类的对象。
子类重写虚函数的时候virtual加或不加都可。
虚函数
如果对上面的Animal(在添加关键字virtual之前)进行求字节:普通的成员函数是不算在类的字节中的,所以相当于空类。
在加上关键字之后,就比变成了四个字节。(因为多了一个指针vfptr)
v是virtual,f是function,ptr是pointer。虚函数指针。指向的是虚函数表(vftable)。
虚函数表内部记录的是虚函数入口地址。
Animal类的内部结构:
![img](https://typora-kevin.oss-cn-qingdao.aliyuncs.com/20220215150218.png)
在cat子类还没有重写函数的时候,只是继承了父类Animal 时 的类内部结构:
![img](https://typora-kevin.oss-cn-qingdao.aliyuncs.com/20220215150608.png)
当重写了以后(子类重新写父类中的虚函数)(重写:返回值相同,函数名相同,形参列表相同)(注意:重载和重写是不同的,重载不需要在意返回值和形参列表,只需要函数名相同,而重写要求都一样。)这个继承过来的虚函数入口地址也是要进行改变的,变成了&Cat::speak。
例如:
Animal*animal = new Cat;
animal->speak();//小猫叫
创建的是Cat类型的对象,只是用Animal类型的指针去接收,调用函数的时候调用的还是Cat中的函数。从子类中找speak的入口。
总结:当类内部出现虚函数的时候,类的内部本质发生了变化,类的内部多了一个vfptr的指针,会指向虚函数表。虚函数表中记录着虚函数的入口地址。所以虽然用的是父类的指针指向子类的对象,但是当调用虚函数的时候会从cat类的虚函数表中寻找speak 的入口。看的是指向的对象而不是指针的类型。创建(new)的对象是猫就从猫里找,是狗就从狗里找。
看一下Cat类中的内部结构:
有个虚函数指针,是从Animal父类中继承下来的,指向虚函数表。表中是speak等函数的接口,由于Cat类中已经将speak函数重写,所以Cat类中的入口就不再是继承下来的入口了,而是自己的函数入口。地址是0,如果还有其他的函数就以4字节为单位,地址变成2。
在上面的小猫叫代码中,可以直接利用speak()来调用。也可以利用指针偏移来找函数的地址。
指针animal就是函数的首地址,因为需指针就是在0地址的位置。
(int *)animal
就是规定步长为4字节,不需要加1,因为已经是虚指针了。
*(int *)animal
解引用到了虚函数表的内部
(int*) *(int *)animal
再进行强转是为了规定步长,也不需要+1,因为就一个函数正好是要找的函数地址。
* (int*) *(int *)animal
再解引用到函数speak的具体的地址了。(这个就是个地址0x001)
找到了函数的地址,怎么调用函数?
使用一个函数指针指向这个地址(这里的函数speak指针的返回值是void,形参列表是空的)
(void(*)(//形参列表是空的))//这就是函数指针
((void(*)())(*(int *) * (int * )animal))();
这样就调用了。函数返回值+函数名(地址)+();
函数调用的本质是:通过地址偏移,找到虚函数地址入口,然后调用
如果再加个动物吃饭的虚函数,Cat类再重写:
![img](https://typora-kevin.oss-cn-qingdao.aliyuncs.com/20220215160045.png)
![img](https://typora-kevin.oss-cn-qingdao.aliyuncs.com/20220215160102.png)
计算器案例
初始的案例:
class calculator
{
public:
int getResult(string oper)
{
if (oper == "+")
{
return m_A + m_B;
}
else if (oper == "-")
{
return m_A - m_B;
}
else if (oper == "*")
{
return m_A * m_B;
}
}
int m_A;
int m_B;
};
int main()
{
calculator c;
c.m_A = 10;
c.m_B = 10;
cout << c.getResult("+") << endl;
return 0;
}
这种写法不好,因为,如果算除法或者其他运算的时候有没有考虑到的特殊情况,发现后需要修改,那么就必须在类中(原码)上进行修改。从头开始找出错的位置。设计原则:开闭原则(对扩展进行开放,对修改进行关闭 )
所以最好利用多态来实现计算器。(写一个父类抽象计算器,然后里面写一个虚函数(getResult),以及两个属性ab。然后再将原先的加法等运算分别写成计算器类)
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>
class AbstractCalculator//抽象计算器类
{
public:
virtual int getResult()
{
return 0;
}
int m_A;
int m_B;
};
class AddCalculator :public AbstractCalculator//加法
{
public:
virtual int getResult()
{
return m_A + m_B;
}
};
class SubCalculator :public AbstractCalculator//减法
{
public:
virtual int getResult()
{
return m_A - m_B;
}
};
class MulCalculator :public AbstractCalculator//乘法
{
public:
virtual int getResult()
{
return m_A * m_B;
}
};
int main()
{
AbstractCalculator* calculator = new AddCalculator;//父类的指针指向子类对象
calculator->m_A = 200;
calculator->m_B = 100;
cout << calculator->getResult() << endl;//300
delete calculator;
calculator = new SubCalculator;
calculator->m_A = 200;
calculator->m_B = 100;
cout << calculator->getResult() << endl;//100
return 0;
}
这样虽然代码的量比刚才的要多,但是如果出了错或者要添加新的代码功能是十分方便 的。
这个calculate指针一直有,释放了加法计算器还可以让这个指针指向新创建的减法计算器。
个人总结:在同意层次上的不同的函数可以总结一个“抽象”类,类中写个函数的模板。将多个函数改写成不同的类,变成多态的形式。
猫吃饭,狗吃饭,人吃饭。就是一个层次的相似的函数。可以抽象出动物的抽象类,然后将猫,狗,人改写成不同的类,然后在类中对抽象类中的虚函数进行改写。
纯虚函数和抽象类
在上面的例子中,抽象计算机类中的虚函数getResult的返回值是用不到的,实现的内容也是用不到的,所以可以把这个getResult虚函数改成纯虚函数。
virtual int getResult() = 0;//纯虚函数
如果一个类中包含纯虚函数,那么这个类就无法实例化对象,这个类就称为抽象类。
抽象类的子类必须要重写父类的纯虚函数,否则也属于抽象类。
class drink
{
public:
virtual void one() = 0;
virtual void two() = 0;
virtual void three() = 0;
virtual void last() = 0;
void MakeDrink()
{
one();
two();
three();
last();
}
};
class tea:public drink
{
virtual void one()
{
cout << "准备茶叶" << endl;
}
virtual void two()
{
cout << "煮水" << endl;
}
virtual void three()
{
cout << "煮茶" << endl;
}
virtual void last()
{
cout << "喝茶" << endl;
}
};
class coffee:public drink
{
virtual void one()
{
cout << "准备咖啡" << endl;
}
virtual void two()
{
cout << "煮水" << endl;
}
virtual void three()
{
cout << "煮咖啡" << endl;
}
virtual void last()
{
cout << "喝咖啡" << endl;
}
};
void DoBussiness(drink* drink)
{
drink->MakeDrink();
delete drink;
}
int main()
{
drink* drink = new coffee;
DoBussiness(drink);
DoBussiness(new coffee)
return 0;
}
虚析构和纯虚析构
一个例子:
class Animal
{
public :
Animal()
{
cout << "Animal的构造调用" << endl;
}
virtual void speak()
{
cout << "动物在说话" << endl;
}
~Animal()
{
cout << "Animal的析构调用" << endl;
}
};
class Cat :public Animal
{
public:
Cat(const char* name)
{
cout<< "Cat 的构造调用" << endl;
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)
{
delete[] this->m_name;
this->m_name = NULL;
}
}
char* m_name;
};
int main()
{
Animal* animal = new Cat("ketti");
animal->speak();
delete(animal);//需要手动释放,因为animal是开辟到堆上的。
return 0;
}
输出的结果:
Animal的构造调用
Cat 的构造调用
小猫在说
Animal的析构调用
发现在手动释放对象的时候并没有调用小猫的析构(没有将起的名字的空间释放)
用多态的方式将子类的属性放在了堆区(子类中有指向堆区的属性),这样在释放父类指针(指向的对象)的时候,是不会调用子类的析构的。解决方法就是将父类中的析构改写成virtal虚析构。这样就会调用小猫的析构了。如果子类中的属性全是栈上的,那么就不需将父类中的析构写成虚析构。
纯虚析构
将父类中的析构改写成:
virtual ~Animal() = 0;
但是纯虚析构也是需要有实现的,不像上面的父类中的纯虚函数只需要声明,因为子类会进行实现,但是这里的析构父类也可以调用,如果父类指针指向的对象调用析构呢(有一点父类中的东西要删除)?所以这里的纯虚析构既需要声明也需要实现(需要类内声明(类内没地方写了),类外实现。)
class Animal
{
public :
Animal()
{
cout << "Animal的构造调用" << endl;
}
virtual void speak()
{
cout << "动物在说话" << endl;
}
virtual ~Animal() = 0;
};
Animal::~Animal()
{
cout << "Animall的纯虚析构调用" << endl;
}
注意:如果一个类中,有了纯虚析构,那么这个类也是属于抽象类,无法实例化对象
这个和上面的父类对象调用析构不冲突,因为父类对象调用多态创建的子类对象,也就是说一个对象可以调用两个析构了,
总结:
原先不加virtual 的时候只能调用父类中的对象,加了virtual后两个都可以调用了。但是不是纯虚析构,不能保证父类是抽象类,所以如果要变成纯虚析构,那么就要类内只声明,类外实现(只类内声明不实现会报错)。
向上向下类型转换
Animal* animal = new Animal();
Cat* cat = (Cat*)animal;
这样的写法称为向下类型转换,会出现越界的情况。因为Animal申请的空间比Cat申请的空间要小。寻址范围也小。
相反的就是向上类型转换,这个是没有问题的。
如果发生多态(父类指针指向子类对象),那么转换是永远安全的
Animal* animal = new Cat();
Cat* cat = (Cat*)animal;
//按照子类的空间申请的,所以不会越界