c++多态发生的两个条件
c++多态发生的两个条件(牢记):
1、派生类继承含有虚函数的基类,并对基类的虚函数发生重写
2、通过 基类的指针或引用 调用派生类虚函数
多态过程详解
一个案例(黑马)-分析条件一:
//含有虚函数的基类
class Animal
{
public:
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
int animal;
};
//含有虚函数的派生类继承基类
//派生类本身有自有的 和基类虚函数同名同参 的虚函数
class Cat:public Animal
{
public:
virtual void speak()//子类的函数前面不加virtual 也可以重写,但是不规范不建议。
{
cout << "小猫在说话" << endl;
}
int cat;
};
下面用到的小工具:
使用vs studio 2017的cl命令查看c++类的模型结构-CSDN博客
用工具查看 含有虚函数的基类-结构:
其中有个,虚函数表指针 vfptr(virtual function pointer):
这个指针指向类的虚函数表($vftable),虚函数表中存着该类的所有虚函数的入口地址。如下,假如Aninmal类中再增加两个虚函数,那么虚函数表就如下:
含有虚函数的派生类-结构:
分析派生类结构,Animal的所有成员都继承过来了,包括虚函数指针。
最核心且唯一的改变是:(派生类自己的虚函数指针没了)父类的虚函数指针还在,虚函数指针指向的虚函数表被重写了:
重写更新原则:
1、父子类原来本身都有的虚函数,虚函数表中更新成子类的虚函数入口。【本案例的例子】
2、只有父类才有的虚函数,虚函数表中持有不变。
3、只有子类才有的虚函数,新增到虚函数表里。
关于后面两个案例,可以分别在基类和派生类中添加虚函数印证:
二、完整案例-分析条件二
首先了解 “赋值/类型 兼容规则”,这个是语法前提:
一句话:指在需要基类对象的任何地方,都可以使用public派生类对象来替代(反之不能兼容),当然,也只能访问原基类部分的成员。
举例:
- 使用 子类对象 为 父类对象 初始化:
这样基类的对象b中所有数据成员都将具有派生类对象d中对应数据成员的值
Base b; //定义基类base的对象b Derived d; //定义基类Base的公有派生类Derived的对象d b=d; //用派生类Derived的对象对基类对象b赋值
- 使用 父类指针 指向 子类对象:
Derived d; //定义基类Base的公有派生类Derived的对象b Base *bp=&d; //把派生类对象的地址&d赋值给指向基类的指针bp //也就是说使指向基类对象的指针bp也可以指向派生类对象d
- 将 子类对象 赋给 父类类型的引用:
Base b; //定义基类Base的对象b Derived d; //定义基类Base的公有派生类Derived的对象d Base &br=d; //定义基类Base的对象的引用br,并用派生类Derived的对象d对其初始化
其中后两个,指针和引用的使用,是多态第二个条件的核心组成部分:如果函数的形参是基类对象或基类对象的引用,在调用函数时可以用派生类对象作为实参。
其次了解 指针/引用 调用本质
“一个pointer或一个reference之所以支持多态,是因为它们并不引发内存任何“与类型有关的内存委托操作; 会受到改变的。只有它们所指向内存的大小和解释方式 而已”
//引用方式调用
void DoSpeak(Animal & animal)
{
animal.speak();
}
//指针方式调用
void DoSpeak(Animal *animal)
{
animal.speak();
}
//直接值传递
//这个应该是调用了基类的拷贝构造函数,定义了一个基类对象,因此得到的是一个纯粹的基类。自然不能实现多态
void DoSpeak(Animal animal)
{
animal.speak();
}
https://blog.csdn.net/shichao1470/article/details/89893508#_2
下面进行 使用过程分析
下面是前面案例的完整代码,在test01方法中,分别给 DoSpeak 传递了不同派生类型的类对象参数 给 DoSpeak的形参(他们基类的引用)。DoSpeak中对传入的不同类对象做了相同的处理,产生了不同的执行效果。
class Animal
{
public:
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
void DoSpeak(Animal & animal)
{
animal.speak();
}
//
//多态满足条件:
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象
void test01()
{
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
}
int main() {
test01();
system("pause");
return 0;
}
至此,不难分析出,为什么要用基类的指针或者引用 调用派生类虚函数? 这样就实现了接口和实现方法的唯一(都按照基类去处理)。实际的处理对象却是不同派生类的基类部分。
参考:
C++赋值兼容规则-CSDN博客
【C++】继承 ⑥ ( 类型兼容性原则 | 父类指针 指向 子类对象 | 使用 子类对象 为 父类对象 进行初始化 )-CSDN博客