1. 多态的概念
多态的概念:通俗来说,就是当不同的对象去完成时会产生出不同的状态。
2. 多态的定义及实现
2.1多态的构成条件
在继承中要构成多态还有两个条件:
1. 必须通过父类的指针或者引用调用虚函数。
2. 子类对虚函数进行重写。
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
2.2虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }//1.对虚函数进行重写
};
注:子类重写的虚函数前面可以不加virtual,虽然构成重写,但并不规范。
2.3虚函数重写的两个例外
1. 协变(基类与派生类虚函数返回值类型不同)
父类与子类的虚函数的返回值必须是类对象的指针/引用,并且返回值也构成父子类关系。
class A {};
class B : public A {};
class Person {
public:
virtual A* f() { return new A; }
};
class Student : public Person {
public:
virtual B* f() { return new B; }
};
A,B类构成父子类关系。父类Person返回父类A,子类Student返回子类B,仍构成虚函数重写。反过来Person返回B,不构成。
2. 析构函数的重写(基类与派生类析构函数的名字不同)
基类和派生类的析构函数名不一样能构成重写吗?
编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
建议析构函数设计成虚函数,不设计成虚函数容易发生下面的问题。
这里只调用了基类的析构函数,如果派生类申请了空间,而不进行析构就会发生内存泄漏。
2.4 重载,隐藏(重定义),覆盖(重写)的对比
1.重载:1.两函数要在同一作用域 2.函数名相同,参数不同。
2.隐藏:1.两函数分别在基类派生类 2.函数名相同。
3.重写:1.两函数分别在基类派生类 2.virtual 必须是虚函数 3.函数名/参数/返回值(除协变外)
下面是一个大厂面试题,看下面代码会输出什么。
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
为什么会出现这种结果呢?
1.p指向对象的类型是B子类,p->test()中this指针的类型是什么?因为test函数在父类中,所有this指针类型仍是A。
2.因此test调用的func函数this类型为A父类,满足多态条件,会调用子类的func函数。
3.func函数具体实现是父类接口+子类实现,func函数val的缺省值仍是父类的1。
3. 抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承抽象类后必须重写所有纯虚函数,才能实例化。
class Car//抽象类
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
4.多态原理
4.1虚函数表
sizeof(A)大小为多少呢?
class A
{
public:
virtual void Func1()
{
cout << "A::Func1()" << endl;
}
virtual void Func2()
{
cout << "A::Func2()" << endl;
}
void Func3()
{
cout << "A::Func3()" << endl;
}
private:
int _b = 1;
};
A父类中只有一个int成员为什么会是8呢?
除了_b成员,还多一个__vfptr指针放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)
在一个类中如果有虚函数,它生成的对象中就会存在一个指针指向虚表。
下面我们看看子类中虚表和父类虚表有什么区别。
#include"test.h"
class A
{
public:
virtual void Func1()
{
cout << "A::Func1()" << endl;
}
virtual void Func2()
{
cout << "A::Func2()" << endl;
}
void Func3()
{
cout << "A::Func3()" << endl;
}
private:
int _b = 1;
};
class B : public A
{
public:
virtual void Func1()
{
cout << "B::Func1()" << endl;
}
private:
int _d = 2;
};
从上面图中我们可以看到
1.子类对象d也存在虚函数指针,况且指向的虚函数表地址与父类不同。说明子类也会生成自己的虚函数表。
2.子类虚函数表中关于Func1的地址与父类不同,这是因为子类中对Func1虚函数进行了重写。(如果子类中没有对任何虚函数进行重写,子类的虚函数表仍会生成且与父类的虚函数表内容相同,因此如果不进行重写就不要定义虚函数)
3.子类和父类的虚函数表中只有func1 func2的相关地址,func3没有。这是因为func3是普通函数,只有虚函数才会放到虚函数表中。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一nullptr。
5. 总结一下派生类的虚表生成:
1.先将基类中的虚表内容拷贝一份到派生类虚表中。
2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
虚函数指针在哪里?虚函数表在哪里?虚函数在哪里?
1.虚函数指针在每个对象中,通过指针找到虚函数表的位置。2.虚函数表一般在常量区
3.虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。
关于多态的实现,根据对象中的虚函数指针找到虚表,再根据虚表找到对应函数的实现过程。
所以满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
4.2动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。