多态--总结复习巩固
一、多态核心概念1.定义多态同一接口不同实现一个行为不同对象做不同事。1.1静态多态编译器发生时机编译时期确定调用哪个函数实现方式函数重载、运算符重载、模板底层原理名字修饰特点效率高但灵活性差1.2动态多态运行期发生时机运行期确定调用哪个函数实现方式虚函数继承底层原理虚函数表vtable)虚函数指针vptr特点灵活性高但有一定性能开销2.动态多态三要素1.继承关系2.基类有虚函数子类重写3.基类指针/引用调用虚函数二、静态多态的底层原理名字修饰静态多态之所以能在编译期确定函数调用是因为 C 编译器会对函数名进行 修饰将函数的参数类型、个数、顺序等信息编码到函数名中。编译后的函数名GCC编译器void func(int) 被修饰为_Z4funcivoid func(double) 被修饰为_Z4funcd名字修饰规则GCC_Z:C函数的统一前缀4函数名func的长度func原函数名iint类型参数ddouble类型参数三、动态多态的核心虚函数表与虚函数指针动态多态的实现依赖于两个关键数据结构虚函数表和虚函数指针。1.虚函数表是一个静态的、只读的函数指针数组每个含有虚函数的类包括其他派生类都有且仅有一个虚函数表表中存储的是该所有虚函数的地址虚函数表在编译期生成存储在程序的只读数据段2.虚函数指针是一个指向虚函数表的指针每个含有虚函数表的类的对象都有一个虚函数指针位于对象内存布局的最开头GCC和MSVC都是如此在构造函数中被初始化指向该类的虚函数表四、单继承下的多态实现代码示例1编译器做了什么对于Base类编译器生成了一个虚函数表里面按声明顺序存放两个虚函数地址Base::func1, Base::func2在每个Base对象中插入一个虚指针指向这个虚表对于Derived类由于它重写了func1并新增了func4编译器生成一个新的虚表内容为第一个槽位Derived::func1(覆盖了基类的func1)第二个槽位Base::func2(未重写保留基类版本)第三个槽位Derived::func4(新增的虚函数)在每个Derived对象中插入的vptr指向这个新虚表。关键无论对象如何构造vptr在构造函数执行时被正确初始化。基类部分构造时vptr指向基类虚表派生类构造函数执行后vptr被更新为派生类虚表。2内存图1、对象的内存布局2、虚表的内部结构五、单继承下的多态实现主函数调用编译器将ptr-func1()转换为类似以下的伪代码步骤1.取vptr从 ptr 指向的对象头部取出 vptr 的值。vptr *( (void***)ptr ); // ptr 指向的对象的第一个8字节64位系统就是 vptr2.定位虚表槽位由于 func1 是 Base 的第一个虚函数编译器知道它在虚表的偏移为 0索引0。func_ptr vptr[0]; // 取虚表第一个槽位的内容即函数地址3.调用(*func_ptr)(ptr); // 调用该函数并传入 this 指针即 ptr因为 ptr 实际指向一个 Derived 对象其 vptr 指向 Derived 虚表而该虚表的第一个槽位被替换成了 Derived::func1所以最终调用的是派生类的版本。如果是非虚函数 ptr-func3()编译器根本不去查虚表直接在编译期确定地址调用 Base::func3静态绑定。为啥ptr-func2() 调用的是基类版本因为 Derived 没有重写 func2所以 Derived 虚表的第二个槽位里存放的仍然是 Base::func2。过程同上vptr[1] 取出这个地址最终执行 Base::func2。这正是虚函数“非重写则继承基类行为”的底层原理。六、多重继承下的多态实现1B 和 C 各自独立存在时B 类每个 B 对象内部有一个 vptr指向 B 的虚表表里是 B::b_func1 和 B::b_func2。C 类每个 C 对象内部有一个 vptr指向 C 的虚表表里是 C::c_func1 和 C::c_func2。2当 A 同时继承 B 和 C 时A的对象内部包含B 子对象和C 子对象两部分还有自己的成员。因为 B 和 C 都引入了虚函数所以 A 对象内部会嵌入两个 vptr第一个 vptr位于 B 子对象的开头指向“融合了 B 接口和 A 新增虚函数”的虚表简称A-in-B 虚表。第二个 vptr位于 C 子对象的开头指向“融合了 C 接口”的虚表简称A-in-C 虚表通常不包含 A 新增的虚函数a_func 一般只挂在第一张表上。A 对象的内存布局两张虚表的内容A-in-B 虚表对应 B 子对象A-in-C 虚表对应 C 子对象3调用过程验证两个虚指针的作用场景1通过 B 基类指针调用pb 指向 A 对象的起始地址即 B 子对象头部通过第一个 vptr 找到 A-in-B 虚表取第0槽位得到 A::b_func1调用正确。场景2通过 C 基类指针调用这里发生了指针调整new A() 返回的是整个 A 对象的地址即 B 子对象地址但赋值给 C* 时编译器会自动调整 pc 的值让它指向 A 对象内部的C 子对象开头也就是第二个 vptr 的位置。然后通过第二个 vptr 找到 A-in-C 虚表取第0槽位得到 A::c_func1调用正确。七、纯虚函数与抽象类纯虚函数的声明virtual void func() 0;纯虚函数的底层实现纯虚函数在虚函数表中对应的位置存储的是0GCC或指向__cxa_pure_virtual函数的指针MSVC如果尝试调用纯虚函数会触发运行时错误含有纯虚函数的类是抽象类不能实例化对象派生类必须重写所有纯虚函数才能实例化八、多态的性能开销与优化1.性能开销内存开销每个对象多一个虚函数指针8 字节64 位系统时间开销虚函数调用比普通函数调用多两次内存访问无法内联虚函数调用是间接调用编译器通常无法内联2.优化方法避免不必要的虚函数只有需要多态的函数才声明为 virtualfinal 关键字C11 引入的 final 关键字可以禁止类被继承或函数被重写编译器可以将虚函数调用优化为直接调用CRTP奇异递归模板模式用模板实现静态多态完全消除运行时开销