目录
- 多态的定义及实现
- 多态的条件
- 多态的原理
- 虚函数表
- 动态绑定和静态绑定
- 总结多态原理
- 单继承和多继承下的虚函数表
- 单继承下的虚函数表(有虚函数覆盖)
- 多继承下的虚函数表(有虚函数覆盖)
- 为什么子类对象赋值给父类对象,也可以切片,但为什么实现不了多态
我们知道面向对象的三大特性是:封装、继承和多态。
封装:就是隐藏类的内部细节,只提供对外开放的接口;举个例子,就是你去餐厅吃饭,你需要到前台点餐或者是扫二维码点餐;前台就是对外开放的接口,你不用管具体的细节
继承:就是面向对象设计使代码可以复用的重要手段,它可以让我们在保持原有类特性的基础上进行扩展,增加功能;你可以把公共的属性提取出来封装成一个类,再让别的类去继承它;
多态:简单来说多态就是同一个行为具有不同的表现方式;具体点就是多态就是在继承的基础上,重写父类中成员函数并定义父类引用指向子类对象;当你用一个父类引用去接收一个子类对象,并且这个子类重写了父类的成员函数,那么当你调用这个函数时,就是调用子类重写后的函数;这就是一个行为多种表现方式
多态的定义及实现
多态的条件
构成多态有两个条件:
-
子类虚函数重写父类虚函数(重写:三同(返回值类型、函数名字、参数列表完全相同)+虚函数)
-
父类指针或引用接收子类对象,然后取调用虚函数
其实多态的定义上面已经讲得很清楚了,那么这里再总结一下吧!
多态其实就是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
举个例子:比如说动物的叫声,同样是“叫”这个行为,不同的动物有不同的叫声。
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void cry() = 0;
};
class Cat:public Animal
{
public:
virtual void cry()
{
cout << "喵喵" << endl;
}
};
class Dog :public Animal
{
public:
virtual void cry()
{
cout << "旺旺" << endl;
}
};
int main()
{
Cat c;
Dog d;
Animal& cat = c;
Animal& dog = d;
cat.cry();
dog.cry();
return 0;
}
运行结果:
OK,到这里,什么是多态也差不多该懂了;这里解释一下上面的代码,主要是补充纯虚函数和抽象类两个概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写。
多态的原理
接下来我们来讲多态的原理
虚函数表
我们先来认识一下什么是虚函数表,先看下面代码,猜一猜运行结果
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun()
{
cout << "fun" << endl;
}
};
int main()
{
Base b;
cout << sizeof(Base) << endl;
return 0;
}
运行结果:
提示一下:这是在32位平台下运行的
为什么这个类的大小是4呢?
我们来看一下类的大小由什么组成
- 非静态成员变量的内存占用之和
- 考虑内存对其的问题;
- 虚函数产生的额外内存开销,即虚函数表指针(Virtual Table Pointer);
看到这里我想应该明白为什么是4了吧,答案就是因为这个Base类中有虚函数fun所以,它有一个虚函数指针,这个虚函数指针指向虚函数表,虚函数表存着虚函数的地址。
打开监视窗口一看,确实是这样子的
知道虚函数表和虚函数指针后,我们继续往下面分析
class Base1
{
public:
virtual void fun1()
{
cout << "Base1:fun1()" << endl;
}
virtual void fun2()
{
cout << "Base1:fun2()" << endl;
}
private:
int _b1;
};
class Derive:public Base1
{
public:
virtual void fun1()
{
cout << "Derive:fun1()" << endl;
}
virtual void fun3()
{
cout << "Derive:fun3()" << endl;
}
virtual void fun4()
{
cout << "Derive:fun4()" << endl;
}
private:
int _d;
};
int main()
{
Derive d;
Base1 b;
b.fun1();
return 0;
}
代码中Base1有虚函数:fun1(),fun2()
Derive继承了Base重写了Base1的虚函数:fun1();自己有虚函数fun3()和fun4()
我们来看一下监视窗口下的b对象
在这里插入图片描述
ok,通过上面的代码,我们可以得出以下几点
- 派生类对象d中有一个虚表指针,d对象有两部分,一部分是父类继承下来的成员,另部分是自己的成员
- 基类对象b和派生类对象d的虚表是不一样的,这里的fun1发生了重写,所以d的虚表中存的是重写的Derive::fun1,所以虚函数的重写也叫做覆盖,覆盖指的就是虚表中虚函数的覆盖。
- fun2也被继承下来了,又是虚函数,所以放进了d的虚表
- 虚函数表本质是一个存放虚函数指针的指针数组,所以一般情况下这个数组最后面放了一个nullptr(这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。)
总结派生类虚表的生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生类重写了基类的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增的虚函数按在派生类中的声明次序增加到派生类虚表的最后(关于这一点,肯定有人有疑惑了,为什么你上面的对象d的虚表里没有fun3和fun4呢?这里是编译器的监视窗口故意隐藏了这
两个函数,也可以认为是他的一个小bug)
到这里可能就有问题了
虚函数存在哪里呢?
答:虚函数跟普通函数一样都存在代码段里
虚表存在哪里?
答:在vs下是存在代码段中
动态绑定和静态绑定
我们再来理解两个概念,动态绑定和静态绑定
-
静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
-
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。
总结多态原理
OK,有了上面的铺垫,我们就能总结出多态的原理了
多态的原理是通过虚表指针和虚函数表来实现的,对象里存着虚函数指针,这个虚函数里面存在基类虚函数表的内容,如果派生类重写了基类的某个虚函数,在派生类的虚函数表里会用派生类自己重写的虚函数地址覆盖掉虚函数表中基类的虚函数地址;然后在运行时,去指向对象的虚函数表中查调用的函数地址,然后执行;这样就形成了多态
单继承和多继承下的虚函数表
接下来,我打算介绍一下,单继承和多继承下的虚函数表是长什么样的
单继承下的虚函数表(有虚函数覆盖)
class Base1
{
public:
virtual void fun1()
{
cout << "Base1:fun1()" << endl;
}
virtual void fun2()
{
cout << "Base1:fun2()" << endl;
}
private:
int _b1;
};
class Derive:public Base1/*,public Base2*/
{
public:
virtual void fun1()
{
cout << "Derive:fun1()" << endl;
}
virtual void fun3()
{
cout << "Derive:fun3()" << endl;
}
virtual void func4()
{
cout << "Derive:fun4()" << endl;
}
private:
int _d;
};
int main()
{
Derive d;
Base1 b;
b.fun1();
return 0;
}
对象d的对象模型:
多继承下的虚函数表(有虚函数覆盖)
class Base1
{
public:
virtual void fun1()
{
cout << "Base1:fun1()" << endl;
}
virtual void fun2()
{
cout << "Base1:fun2()" << endl;
}
private:
int _b1;
};
class Base2
{
virtual void fun1()
{
cout << "Base2:fun1()" << endl;
}
virtual void fun2()
{
cout << "Base2:fun2()" << endl;
}
};
class Derive:public Base1,public Base2
{
public:
virtual void fun1()
{
cout << "Derive:fun1()" << endl;
}
virtual void fun3()
{
cout << "Derive:fun3()" << endl;
}
virtual void func4()
{
cout << "Derive:fun4()" << endl;
}
private:
int _d;
};
int main()
{
Derive d;
Base1& b1 = d;
Base2& b2 = d;
return 0;
}
对象b1的对象模型:
对象b2对象模型:
向上转型会切片,所以不管是b1还是b2都访问不到子类独有的部分和其他父类的部分,简单来说向上转型后b1只能访问Base1的东西,查的也是Base1的vfptr;
这里再补充一点:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
对于菱形继承的虚函数表,这里就不展开了,下次有机会再补充
为什么子类对象赋值给父类对象,也可以切片,但为什么实现不了多态
最后再分享一下这个问题
#include <iostream>
using namespace std;
class Base1
{
public:
virtual void fun1()
{
cout << "Base1:fun1()" << endl;
}
virtual void fun2()
{
cout << "Base1:fun2()" << endl;
}
private:
int _b1;
};
class Base2
{
virtual void fun1()
{
cout << "Base2:fun1()" << endl;
}
virtual void fun2()
{
cout << "Base2:fun2()" << endl;
}
};
class Derive:public Base1,public Base2
{
public:
virtual void fun1()
{
cout << "Derive:fun1()" << endl;
}
virtual void fun3()
{
cout << "Derive:fun3()" << endl;
}
virtual void func4()
{
cout << "Derive:fun4()" << endl;
}
private:
int _d;
};
int main()
{
Derive d;
Base1 b3 = d;
return 0;
}
核心点就是一个类只有一张虚表 每个对象都是有一个虚表指针 指向这张虚表
你父类对象里面存的指针很肯定是指向父类的虚函数表 跟子类无关 所以无法形成多态。
最后如果文中有什么错误的地方,请大家指正。如果有所收获,请各位给作者一键三连