C++| |多态
多态
1. 概念
对于不同的对象,去完成同一个行为的时候产生不同的状态
2. 定义和实现
2.1 定义
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价
在继承中要构成多态还有两个条件:
调用函数的对象必须是指针或者引用
被调用的函数必须是虚函数,且完成了虚函数的重写
什么是虚函数?
虚函数:就是在类的成员函数的前面加virtual关键字
例:
class Person { public: virtual void BuyTicket() {} }
什么是虚函数的重写?
虚函数的重写:派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数,完全相同是指:函数名,参数,返回值都相同。虚函数的重写也叫作虚函数的覆盖
例:
#include <iostream> class Person { public: virtual void BuyTicket() { std::cout << "买票全价" << std::endl; } }; class Student : public Person { public: virtual void BuyTicket() { std::cout << "买票半价" << std::endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
虚函数重写的例外:协变
协变:重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。
例:
#include <iostream> class A {}; class B : public A {}; class Person { public: virtual A* f() { std::cout << "In Person!" << std::endl; return new A; } }; class Student : public Person { public: virtual B* f() { std::cout << "In Student!" << std::endl; return new B; } }; void Func(Person& p) { p.f(); } int main() { Person p; Student stu; Func(p); Func(stu); return 0; }
不规范的重写行为
在派生类中重写的成员函数可以不加virtual关键字,也是构成重写的。因为继承后基类的虚函数被继承下来在派生类中仍然保持虚函数属性。
例:
class Person { public: virtual void BuyTicket() { std::cout << "买票全价" << std::endl; } }; class Student : public Person { public: void BuyTicket()//没有添加virtual关键字,但是也可以进行重写 { std::cout << "买票半价" << std::endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); return 0; }
析构函数的重写问题
基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,其实不然这里可以理解为编译器对析构函数的名称做了特殊的处理,编译之后析构函数的名称统一处理成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; delete p2; return 0; }
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,继承是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
2.2 重载,覆盖(重写),隐藏(重定义)
重载:
两个函数在同一作用域
函数名,参数相同
重写:
两个函数在不同作用域
函数名,参数,返回值都相同(协变)
两个函数必须是虚函数
重定义:
两个函数在不同作用域
函数名相同
两个基类和派生类的同名函数不构成重写就是重定义
3. 抽象类
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫作接口类),抽象类不能实例化出对象。派生类继承之后也不能实例化出对象,只有重写虚函数,派生类才能实例化出对象。存续函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
例:
class A { public: virtual void Show() = 0;//纯虚函数 }; class B : public A { public: virtual void Show() override { std::cout << "In Class B" << std::endl; } }; class C : public A { public: virtual void Show() override//完成虚函数的强制重写 { std::cout << "In Class C" << std::endl; } }; int main() { A* a1 = new B(); A* a2 = new C(); a1->Show(); a2->Show(); }
4. C++11 override和final
C++提供override和final来修饰虚函数
实际当中多使用纯虚函数+override的方式来强制重写虚函数,因为虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义
final修饰基类的虚函数不能被派生类重写
例:
class Person { public: virtual void Say() = 0;//纯虚函数 virtual void View() final { std::cout << "View You!" << std::endl; } }; class Student : public Person { virtual void Say() override { std::cout << "In Student" << std::endl; } }; int main() { Person* p = new Student(); p->Say(); p->View(); return 0; }
final的使用:对于final修饰的虚函数虽说不能被其派生类所重写,但是在基类当中自己一定要实现,否则继承下去还是一个虚函数并且没有完成重写就会导致无法实例化出对象
5. 多态的原理
5.1 虚函数表
class Base { public: virtual void Show() { std::cout << "haha" << std::endl; } private: int _b; }; int main() { Base a; return 0; }
结果是8个字节,除了_b成员外,还多了一个 _vfptr放在对象前面(有些平台可能放到对象的后面,这个和平台有关),对象中的这个指针我们叫做虚函数表指针。
一个含有虚函数的类都至少有一个虚函数表指针,因为虚函数的地址要放到虚函数表中,虚函数表也简称虚表
插图:
虚函数表指针监视
对于上述代码增加一个派生类去继承BaseDerive中重写Func1Base在增加一个虚函数Func2和一个普通函数Func3
class Base { public: virtual void Func1() { std::cout << "haha" << std::endl; } virtual void Func2() { std::cout << "he" << std::endl; } void Func3() { std::cout << "ha" << std::endl; } private: int _b; }; class Derive : public Base { public: virtual void Func1() { std::cout << "hehe" << std::endl; } }; int main() { Base a; Derive d; return 0; }
插图:虚函数表继承之后内存
通过观测和测试,我们发现了几点问题:
派生类对象d中也有一个虚表指针,d由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员
基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的徐表中存的是重写的Derive::Func1,所以虚函数重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法
另外Func2继承下来后是虚函数,所以放到了虚表,Func3业绩呈下来了,但是不是虚函数,所以不会放到虚表中
虚函数表本质是一个存虚函数表指针的指针数组,这个数组最后面放了一个nullptr
总结一下派生类虚表的生成:
先将基类中的虚表内容拷贝一份到派生类虚表中
如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
派生类自己新增加的虚函数按其在派生类中的声明顺序增加到平派生类虚表的最后
【问题】
虚函数存在哪的,虚表存在哪的?
虚函数和普通函数一样都是存在代码段的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。虚表存在于数据段
5.2 多态的原理
class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person Mike; Func(Mike); Student Johnson; Func(Johnson); return 0; }
插图:多态原理
对于Func函数传入Person调用的是Person::BuyTicket,传入Student调用的是Student::BuyTicket
p是指向Mike对象时,p->BuyTcket在Mike的虚表中找到虚函数是Person::BuyTicket
p是指向John对象时,p->BuyTicket在John的虚表中找到虚函数的是Student::BuyTicket
这样就实现了不同对象去完成同一行为时,展现出不同的形态
思考:为什么完成有两个条件,一个是虚函数覆盖,还有一个是对象的指针或者引用去调用虚函数
对于要虚函数覆盖就是为了对于基类和派生类对于同一个函数可以做出不同的行为
要拿指针或者引用去调用是为了可以对于不同的对象找到不同的虚函数表,如果不是使用指针或者引用去调用函数的话,就会变成一个传值调用,就会发生切片,就会都调用基类的虚函数表中的虚函数
满足多态的函数调用不是在编译是确定好的,是运行起来以后到对象中去找的。不满足多态的函数调用是在编译时就确认好的
5.3 动态绑定与静态绑定
静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定又称后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定确定程序的具体行为,调用具体的函数,也成为动态多态。比如:动态
6. 单继承和多继承关系的虚函数表
派生类对象的虚表模型
6.1 单继承中的虚函数表
class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; }; int main() { Derive d; return 0; }
插图:单继承虚函数表
观察监视窗口我们可以发现看不到func3和func4。那么我们如何来使用代码显示出虚表中的函数呢?
typedef void(* VFPTR) (); class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; }; void PrintVFPTR(VFPTR* vf) { std::cout << " 虚表地址>" << vf << std::endl; for (int i = 0; vf[i] != nullptr; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, vf[i]); VFPTR f = vf[i]; f(); } std::cout << endl; } int main() { Base a; Derive d; VFPTR* vf = (VFPTR*)(*(int*)&a); PrintVFPTR(vf); VFPTR* vf1 = (VFPTR*)(*(int*)&d); PrintVFPTR(vf1); return 0; }
插图:单继承的内存存储
思路:取出b,d对象的头4个字节,就是虚标的指针
先取b的地址,强制转化为一个int*的指针
在解引用取值,就去到了b对象头4四个字节的值,这个值就是指向虚表的指针
再强转为VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组
虚函数指针传递给PrintVFPTR进行打印虚表
注意:有的时候打印虚表的代码经常崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要清理解决方案就好了
6.2 多继承中的虚函数表
typedef void (*VFPTR) (); class A { public: virtual void func1() { std::cout << "A::func1" << std::endl; } virtual void func2() { std::cout << "A::func2" << std::endl; } }; class B { public: virtual void func1() { std::cout << "B::func1" << std::endl; } virtual void func2() { std::cout << "B::func2" << std::endl; } }; class C : public A ,public B { public: virtual void func1() { std::cout << "C::func1" << std::endl; } virtual void func3() { std::cout << "C::func3" << std::endl; } }; void PrintVFPTR(VFPTR* vftable) { std::cout << "虚函数地址:>" << vftable << std::endl; for (int i = 0; vftable[i] != nullptr; i++) { printf("第%d个虚函数表地址:0X%x,->", i, vftable[i]); VFPTR f = vftable[i]; f(); } std::cout << std::endl; } int main() { C c; VFPTR* vf = (VFPTR*)(*(int*)&c); PrintVFPTR(vf); VFPTR* vf1 = (VFPTR*)(*(int*)((char*)&c + sizeof(A))); PrintVFPTR(vf1); return 0; }
插图:多继承的内存存储
多继承的派生类未重写的虚函数放在第一个继承基类部分的虚函数表中
6.3 菱形继承和菱形虚拟继承
实际中我们不建议设计出菱形继承和菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型访问基类成员有一定的性能损耗
查看链接:
7. 多态常见的面试问题
什么是多态?
什么是重载,重写(覆盖),重定义(隐藏)?
多态的实现原理?
inline函数可以是虚函数吗?
不能因为inline函数没有地址,无法把地址放到虚函数表中
静态函数可以是虚函数吗?
不能,因为静态函数没有this指针,使用类型::成员函数的调用方式无法放进虚函数表
构造函数可以是虚函数吗?
不能,因为对象中的虚函数表是在构造函数初始化列表阶段才初始化的
析构函数可以是虚函数吗?
可以,并且把基类的析构函数定义为虚函数。这样才可以析构正确实现多态
对象访问普通函数块还是虚函数更快?
普通对象:一样快
指针对象或者是引用对象:调用普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找
虚函数是在什么阶段生成的,存在哪的?
虚函数是在编译阶段就生成的,一般情况下存在静态区的
C++菱形继承的问题?虚继承的原理?
虚表
什么是抽象类?抽象类的作用?
含有纯虚函数。抽象类强制重写虚函数,另外抽象类体现出接口继承关系