一、多态的基本概念
多态是c++面向对象的三大特性之一。
多态分为两类:
- 静态多态:函数重载 和 运算符重载
- 动态多态:派生类 和 虚函数 实现运行时多态。
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定:编译阶段进行绑定。
- 动态多态的函数地址晚绑定:运行阶段进行绑定。
class Animal
{
public:
virtual void speak(){ //虚函数virtual关键字
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;
}
上述代码,如果我们运行就会发现,输出结果是“小猫说话”。如果我们去掉virtual关键字,那么就变成了“动物在说话”,这就是多态的基本使用。
从上述例子中可以看出,多态满足的条件是:
- 有继承关系;
- 子类重写父类中的虚函数;(重写:函数返回值类型 函数名 参数列表 完全一致的)
多态使用条件:
- 父类指针或引用 指向 子类的对象。
二、多态基本原理
根据上述代码,我们分析:
Aniaml类的内部结构,如果没有virtual关键字,那么他就是一个空类,sizeof可以看到的大小是1。但是加上virtual之后,就相当于类内部多了一个指针vfptr(虚函数(表)指针),这个指针指向了vftable(虚函数表),而vftable里存储了虚函数表,表内部记录了虚函数的地址,所以这时候用sizeof可看到Animal类的大小变为了4,多了一个指针的大小,这个指针指向虚函数的地址,也就是&Aniaml.speak函数的地址,加取址符。
同理,Cat类内部也一样,因为继承了Animal类,所以也拥有了父类中的指针vfptr和指针指向的虚函数表vftable。如果这时候子类Cat并没有重写speak函数的话,子类的虚函数表vftable内记录的会是父类的地址,也就是&Aniaml.speak。如果子类对speak进行了重写,子类的虚函数表vftable记录的地址将会产生覆盖,即变成了&Cat.speak。
- 当子类重写父类的虚函数,子类中的虚函数表内部会 替换 成子类的虚函数地址。
下面我们进行验证:
class Animal{
public:
void speak(){
cout << "动物说话" << endl;
}
};
可以看到当我们没有使用virtual时,Animal类内大小为1。如果我们加上virtual的话:
class Animal{
public:
virtual void speak(){
cout << "动物说话" << endl;
}
};
这时候我们可以看到,这时候类的大小变为4,并且类内多了一个指针vfptr,指向了vftable函数表,表内记录了Animal::speak的地址(Animal类作用域下的speak函数的地址)。这时候我们看一下Cat类的情况:
Class Cat{
public:
};
可以看到如果我们什么都不写的话,Cat类内会继承到父类的vfptr指针,和vftable的虚函数表,而内部记录的是Animal作用域下的speak函数;这时候我们在子类中重写一下speak函数:
class Cat{
public:
void speak(){
cout << "小猫说话" << endl;
}
};
这时候我们会看到,Cat类内的指针指向的虚函数表中记录的地址发生了变化,它记录的是Cat类作用域下的speak函数了。验证了我们上面提到的。
三、纯虚函数和抽象类
在多态中,通道父类中虚函数的实现是没有意义的,主要都是调用子类重写的内容。因此可以将父类中的虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有纯虚函数,这个类也就称为抽象类。抽象类的特点如下:
- 无法实例化对象;
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
Class Base{
public:
virtual int Addfunc() = 0;
};
Class Son : public Base{
public:
int Addfunc(){
cout << "子类调用" << endl;
}
};
在上述代码中,如果我们在实例化对象时写入Base b;则会发生报错,因为抽象类中不允许实例化对象。同时,如果我们在子类中不重写纯虚函数的话,也一样无法实例化对象出来。
四、虚析构和纯虚析构
在多态的使用中,如果子类中有属性开辟到堆区,那么父类指针在释放时是无法调用到子类中的析构函数的。解决的方法就是把父类中的析构函数改为虚析构或纯虚析构。
虚析构和纯虚析构的特性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构的区别:
- 如果是纯虚析构,那么该类属于抽象类,无法实例化对象。
虚析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名()= 0; (类外)类名::~类名(){};
Class Animal{
public:
virtual void speak(){}
Animal(){
cout << "父类构造函数" << endl;
}
~Animal(){
cout << "父类析构函数" << endl;
}
};
Class Cat : public Animal{
public:
string *m_Name;
Cat(string name){
cout << "子类构造函数" << endl;
m_Name = new string(name);
}
void speak(){
cout << *m_Name << "小猫说话" << endl;
}
~Cat(){
if(m_Name != NULL){
cout << "子类析构函数" << endl;
delete m_Name;
}
}
};
void test01(){
Animal * animal = new Cat("Tom");
animal->speak();
delete animal;
}
在上述代码中,我们运行之后会发现输出的是:
并没有进行子类析构函数的调用,也就是说子类中开辟的堆区数据并没有进行释放,这就是多态中的问题。这时候我们可以把父类中的析构函数改为虚析构或者纯虚析构,这个时候如果我们在运行就会发现子类析构函数被成功调用了。
总结:
- 虚析构或者纯虚析构就是用来解决通过父类指针释放子类对象;
- 如果子类中没有堆区数据,可以不用写虚析构或纯虚析构;
- 拥有纯虚析构函数的类也属于抽象类。