一、多态的基本语法
多态分为两类:
静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址
这篇文章我主要说的是动态的多态!
举个例子:
#include<iostream>
using namespace std;
class Animal
{
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat:public Animal
{
void speak()
{
cout << "小猫在说话" << endl;
}
};
void doSpeak(Animal &a)
{
a.speak();
}
int main()
{
Cat c;
doSpeak(c);
return 0;
}
运行结果:
我们给doSpeak函数传入的是一个Cat的实例对象,虽然doSpeak在定义的时候要求传入一个Animal类的对象引用,但c++中父类的引用是可以指向对象的。我们传入Cat的对象肯定想获得
“小猫在说话”的运行结果,但事实上结果是“动物在说话”,如何解决这个问题呢。
多态的基本语法:
多态的满足条件:
1.Animal类和Cat类有继承关系(满足)
2.子类重写父类中的虚函数(上述代码不满足)
根据“2.子类重写父类中的虚函数”这一原则我们试试新的代码
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat:public Animal
{
virtual void speak()
{
cout << "小猫在说话" << endl;
}
};
void doSpeak(Animal &a)
{
a.speak();
}
int main()
{
Cat c;
doSpeak(c);
return 0;
}
对比两端代码,我只是在父类Animal的speak()函数前加了一个virtual
(在子类Cat类中void speak()函数前的virtual可以加可以不加)
多态的使用条件:
发生函数重写
发生函数重写的条件:
父类指针或引用指向子类对象
例子中我们在定义doSpeak的时候要求传入一个Animal类的对象的引用(Animal &a),但实际上我们传入的是他的子类Cat类的对象c,这样不太明显,我改一下代码给大家看看:
改法1:父类的引用指向子类对象
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat:public Animal
{
virtual void speak()
{
cout << "小猫在说话" << endl;
}
};
void doSpeak(Animal &a)
{
a.speak();
}
int main()
{
Cat c;
Animal& a = c;
doSpeak(a);
return 0;
}
我添加了一行Animal &a=c,并且doSpeak函数传入的是父类Animal的引用a不再是子类Cat的对象c。
改法2:父类的指针指向子类
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat:public Animal
{
virtual void speak()
{
cout << "小猫在说话" << endl;
}
};
void doSpeak(Animal *a)
{
a->speak();
}
int main()
{
Animal *animal=new Cat;
doSpeak(animal);
return 0;
}
可以看 main函数我更改为: Animal *animal=new Cat(父类Animal类的指针指向子类Cat);函数doSpeak也更改为传入一个Animal类的指针。
二、多态的原理(动态多态)
之前说过,动态多态的函数地址晚绑定,函数重写是实现晚绑定的条件,通过函数重写来实现动态多态。因此我这里主要说说函数重写的原理
函数重写指的是子类重写父类的函数,重写与重载不同的是,子类的函数和父类函数完全一样(函数返回值类型、函数名、函数的形参列表全部相同)
如:
class Animal
{
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat:public Animal
{
void speak()
{
cout << "小猫在说话" << endl;
}
};
父类Animal类和子类Cat类的函数完全一样。
这时我们只要在父类Animal类要重写的函数(void speak())前加上virtual将它变为虚函数即可
(子类Cat类对应的重写函数(void speak())前可以加virtual也可以不加)。
如
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat:public Animal
{
virtual void speak()
{
cout << "小猫在说话" << endl;
}
};
函数重写父类虚函数的原理
在未将Animal类的speak函数变为虚函数前,我们知道Animal类的大小为1字节,因为成员函数并未存储在Animal类中(详细可见另一个博主的博客C++中类所占的内存大小以及成员函数的存储位置_SOC罗三炮的博客-CSDN博客_类成员函数存储在哪),当我们把Animal类的speak函数变为虚函数后,Animal类的大小变为4个字节,是因为Animal类中多了一个vfptr:
这个指针指向一个vftabel
表中记录的Animal类的虚函数地址。
因此Animal类的内部结构为:
而我们知道,Cat类是Animal的子类,理论上Cat继承了Animal类的所有属性,
因此Cat类的内部结构为:
重点来了!:
当我们满足条件:
父类指针或引用指向子类对象时,就会发生子类重写父类的虚函数。
此时Cat的内部的虚函数表就会发生变化,Animal的speak函数地址会被Cat中的speak函数地址覆盖掉,即用&Cat::speak覆盖掉&Animal::speak。
因此我们在程序运行时,会执行子类Cat类中的speak函数。
但是要注意的是,子类重写父类虚函数时,父类的虚函数表是不会变的,子类只会重写自己类内的虚函数表。
这里我再补充一下当父类的引用指向子类对象时:
如:
void speak (Animal &a)
Cat c;
Animal &a= c;
speak(c);
传入的虽然是Animal类的引用,但这个引用指向子类Cat类的对象c,因此编译器会从Cat类的虚函数表中去找speak函数的地址,调用Cat类的speak函数。
当父类的引用指向子类对象时:
如:
void speak (Animal *a)
Animal *animal=new Cat;
speak(animal);
传入的虽然是Animal类的指针,但这个指针指向的地址是子类Cat类的地址,因此编译器会从Cat类的虚函数表中去找speak函数的地址,调用Cat类的speak函数。