目录
1. 多态的定义
多态就是多种形态。
1.1 多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
比如Student继承了 Person。Person对象买票全价,Student对象买票半价。
【条件】
1. 虚函数重写。
2. 必须父类指针或引用去调用虚函数。
1.2 虚函数
virtual修饰的成员函数称为虚函数。
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl;} };
1.3 虚函数重写
子类中有一个跟基类完全相同的虚函数(返回值、函数名、参数列表完全相同),然后修改子类的函数体,称子类的虚函数重写了基类的虚函数。
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } }
【两个例外】
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;} };
2. 析构函数重写,(虚函数的函数名不同)
虽然基类与派生类析构函数名字不同,但是编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person { public: virtual ~Person() {cout << "~Person()" << endl;} }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; // 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函 数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。 int main() { Person* p1 = new Person; Person* p2 = new Student; delete p1; //p1->destructor() + operator delete(p1) delete p2; //指向父类调父类,指向子类调子类 return 0; }
1.4 重载,重写,重定义
1.5 final
1. final修饰类,类不能被继承。
2. final修饰虚函数,虚函数不能被重写。
class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() {cout << "Benz-舒适" << endl;} //报错 };
1.6 override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car { public: virtual void Drive(){} }; class Benz :public Car { public: virtual void Drive() override {cout << "Benz-舒适" << endl;} };
2. 抽象类
1. 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
2. 包含纯虚函数的类叫做抽象类(也叫接口类)。
3. 抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。间接强制子类虚函数重写,因为你不重写就不能实例化对象。
class Car { public: virtual void Drive() = 0; }; class Benz :public Car { public: virtual void Drive() { cout << "Benz-舒适" << endl; } }; class BMW :public Car { public: virtual void Drive() { cout << "BMW-操控" << endl; } };
【接口继承和实现继承】
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。
3. 多态的原理
3.1 虚函数表
1. 一个含有虚函数的类中成员变量会多一个指针,这个指针是虚函数表指针,这个指针指向虚函数表,虚函数表存放着虚函数的地址,虚函数表也简称虚表。
2. 对象存的是虚表指针,虚表存的是虚函数指针,虚表和虚函数存在代码段。
3.2 子类的虚函数表
1. 子类由两部分组成,一部分是继承父类,一部分是自己的,继承父类里包含虚表指针,但这个指针和父类不是同一个,指针指向的虚函数表也是先从父类拷贝下来,如果子类有虚函数重写就会用重写后的新函数地址去覆盖原本在虚函数表中的地址。
2. 为什么只能父类的指针或引用,父类对象调用不能多态吗?
因为子类赋值给父类对象相当于子类的父类部分成员拷贝给父类,但是虚函数表指针不会拷贝。
3. 同一个类的对象共用一张虚函数表,父子类是不同的,哪怕没有重写。
4. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
5. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后放了nullptr。
3.3 多态本质
1. 所以,为什么能实现多态,首先你是父类的指针或引用,那么你调父类虚函数就去找父类虚函数地址找到虚函数表然后找对应函数地址,你调子类虚函数就去找子类中父类部分的虚函数地址找虚函数表然后找对应函数地址,但是此时这个函数地址已经变了,因为被重写然后被新地址覆盖了,所以实现了同样的操作却能调不同的函数。本质也就是运行起来进行指定的操作去对应的表里面去找函数地址,又由于地址被改了,所以出现不同的效果。
3.4 动态绑定和静态绑定
1. 普通调用在编译链接时就确定了地址,动态调用运行时去虚表里面找函数地址调用。
2. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
3. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
4. 多继承关系的虚表
4.1 单继承的虚表
1. 单继承中类里面只有一个虚表,虚表内容拷贝父类的,如果有重写就覆盖,如果自己也有虚函数就加在后面。
4.2 多继承的虚表
1. 当一个子类继承了两个父类时,子类会有两个虚表,因为继承了两个父类,如果有个重写也会覆盖两个。
2. 子类自己的虚函数会加在第一个父类虚表后面。
【细节】
为什么子类func1覆盖了两个父类的func1,但它们的地址不一样?
1. 首先从汇编的角度,它们的目的地是一样的func1,但过程经过多次中转。
2. 这里的func1是子类的,也就是说调用的时候this是子类类型的指针,指向子类的开头,所以用父类指针调用的时候需要偏移回子类开头。
总结:以前是单继承的时候,父类指针和子类指针都是指向子类的开头所以不用偏移,现在有多个父类了,下面的父类指针就需要偏移到最上面,这样传入的this指针才是整个子类,因为多态调用子类的函数需要传入子类的this指针。所以本质是修正指针,所以不能直接调用func1函数,需要一些中转操作修正。
5. 选择题
5.1
以下程序输出结果是什么
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. 调用test()是正常调用,因为继承了,所以去父类部分调用test()。
2. 因为在父类部分,所以此时的test隐藏的this是A*类型。
3. A*去调用func()触发了多态,虚函数继承了接口(除了函数体的所有部分),重写的是实现(函数体)。
【答案】
B->1
6. 问答题
6.1
inline函数可以是虚函数吗?
答:可以,如果是普通调用,inline就起作用,如果是多态调用,inline不起作用。
6.2
静态成员函数可以是虚函数吗?
答:不可以,因为静态成员函数类似于全局函数只不过受类域限制,只有真正的成员函数才能是虚函数。
6.3
构造函数可以是虚函数吗?
答:虚表在编译的时候生成,对象中的虚表指针在初始化列表初始化,虚函数多态调用要到虚表找,此时指针还没初始化。
6.4
析构函数可以是虚函数吗?
可以,场景:父类指针new子类对象,delete父类指针。只有构成多态才能正确调用子类析构。
6.5
普通调用一样快,多态调用虚函数慢,因为要到虚表寻找。