1.什么是多态
多态按照字面意思就是多种状态,在面向对象语言就是接口的不同的实现方式。
举例子例如同样是买票,你作为大学生买的学生票和作为成年人买的票的价格不同,这就是多态的一种形式。
2.多态的定义及其实现
1多态定义的构成条件
- 调用函数的对象必须为指针或者引用
- 被调用的函数必须为虚函数
例如
class man//父类
{
public:
virtual void test()//虚函数
{
cout<<"is a man"<<endl;// 有一张虚表
}
} ;
class student:public man//子类
{
public:
virtual void test()
{
cout<<"is a student"<<endl;
}
};
void need(man &a)//调用的函数
{
a.test();
}
虚函数的例外:协变
虚函数重写有一个例外:一般的重写覆盖需要返回值相同,但是重写的虚函数的返回值可以不同,但必须分别是基类指针和派生类指针或者引用。
不规范的重写行为
在派生类中重写的成员函数可以不加virtual也能重写,这是因为基类的虚函数被继承下来了,所以派生类中依然是虚函数,所以可以,但是这种写法并不规范。
析构函数的重写问题
基类的析构函数如果是虚函数,则派生类的析构函数就重新了基类的析构函数,但是我们知道重写行为的函数,必须函数名相同,不考虑函数协变的情况下,返回类型也要相同。但是析构函数的名字并不同,这里可以理解为编译器对析构函数的函数名称统一处理为destructor。
接口继承和实现继承
普通函数的继承是一种实现继承。子类继承父类的函数,就可以使用父类的函数。虚函数的继承是接口继承,目的是为了重写,从而达到实现多态的目的,继承的是接口。
- 重载,覆盖(重写),隐藏(重定义)
- 重载:两个函数的作用域在同一类内,形参列表不同
- 重写:两个函数分别在基类和派生类的作用域,函数名/参数/返回值相同(协变除外),函数必须是虚函数。
- 重定义:两个函数分别在基类和派生类的作用域,函数名必须相同,且不构成重写。
3.抽象类
在虚函数后面写=0,则这个函数为纯虚函数,有纯虚函数的类叫做抽象类也叫接口类,抽象类不能实例化对象,派生类继承纯虚函数后,必须重写纯虚函数才能实例化对象,纯虚函数的作用就是规定了,继承类必须重写,提醒了接口继承
class A
{
public:
A(){
}
virtual void test()=0;//纯虚函数
};
class B: public A//继承函数
{
public:
B(){
}
virtual void test()
{
cout<<"B";
}
};
需要注意纯虚函数的写法为 virtual +返回值 +函数名 +形参列表 + =0 ,没有函数体。
小知识点:关键词final和override
override在派生类的虚函数后面写要求其必须基类的同名函数,否则程序会报错
class
{
public:
A() {
}
virtual void test()
{} ;
};
class B : public A
{
public:
B() {
}
virtual void test() override
{
cout << "B";
}
};
作用和纯虚函数有些相似,出现函数是在基类的虚函数后面加上=0,verride是在派生类的虚函数后面加上的。
final关键词的作用想法,有时候我们不想希望函数被重写覆盖,在后面加上final就可以实现。
4.多态的原理
class a
{
public:
a()
{};
virtual void test()=0;
}
通过sizeof可以得到类的大小为4,同时我们知道一个空类的大小为1,通过vs’2016的调试功能,可以看到有一个_vfptr的指针,对象中的这个指针我们称为虚函数表指针,有时也叫做虚表,注意与虚基表区别。
虚表的具体操作如下
class A
{
public:
A() {
}
virtual void test1() {};
virtual void test2() {};
void test3() {};
};
class B : public A
{
public:
B() {
}
virtual void test1()
{
cout << "B";
}
private:
int _b;
};
int main()
{
A a;
B b;
cout << sizeof(b) << endl;
}
从vs2016的监视窗口可以看到如下结果
首先基类三个函数test1,test2,test3,其中test1和test2为虚函数,test3为普通函数,派生类重写了test1函数,从监视窗口可以看到
- 派生类对象b也有一个虚表指针,d对象由两部分组成一部分是父类继承的成员,另一部分是自己的成员
- 基类对象和派生类对象的虚表不完全相同,我们可以看到test1被重写所以b的虚表指针里存的是B::test1函数,所以虚函数的重写也叫覆盖,覆盖就是指虚表中虚函数的覆盖,重写是代码的叫法,覆盖是原理层的叫法。
- 可以看到,test2在两个虚表的指针相同,因为在B中并没有重写test2的函数,而是继承下来,还有注意虽然test3也被B继承下来了,但是因为test3为普通函数所以不会放进虚表里面。
- 虚函数表本质是一个存虚函数指针的指针数组,最后一个成语放nullptr。
- 总结一下派生类的虚表生成:首先将基类的虚表内容复制拷贝一份到派生类虚表中,如果派生类重写了某个虚函数,用派生类自己重新的虚函数覆盖虚表中的基类的虚函数,按其声明次序增加到派生类虚表的最后
- 注意虚表存的是虚函数指针,而不是虚函数,虚函数和普通的函数一样存在代码段中,对象存的不是虚表而是虚表指针在vs中经过测试虚表存在代码段中。
小知识点: - 满足多态的函数调用,不是在编译中确定的,是运行起来在对象里找的,而不满足多态的函数调用时编译时确认好的。
- 在函数编译期间确定了程序的行为,称为静态多态,函数的重载。
- 在程序运行期间,根据具体的类型确定程序的具体行为调用具体函数,称为动态多态。