多态性原理(详)

发布时间:2026/7/13 15:31:44
多态性原理(详) 多态性原理(详)一、多态概述多态是面向对象编程的三大特性之一指同一个接口多种不同的实现。C 支持两种多态类型别名绑定时机实现方式效率编译时多态静态多态、早期绑定编译阶段函数重载、运算符重载、模板高运行时多态动态多态、晚期绑定运行阶段继承 虚函数 指针/引用略低二、编译时多态静态多态2.1 编译时多态的核心原理编译时多态是指在编译阶段就能确定调用哪个函数编译器根据函数名、参数类型、参数个数等信息直接生成调用代码。编译时多态的处理流程 ------------------------------------------------------- 源代码 编译过程 func(10); --- 编译器分析参数类型(int) func(3.14); --- 编译器分析参数类型(double) func(a); --- 编译器分析参数类型(char) 根据参数类型选择对应函数 ↓ 生成直接调用指令 call _func_int call _func_double call _func_char2.2 函数重载的实现原理2.2.1 名字粉碎Name Mangling编译器通过名字粉碎技术为每个函数生成唯一的修饰名从而实现重载。// 源代码intadd(inta,intb){returnab;}doubleadd(doublea,doubleb){returnab;}intadd(inta,intb,intc){returnabc;}编译器生成的修饰名Visual C源代码函数修饰名编码含义add(int, int)?addYAHHHZYAHHH 返回int两个int参数add(double, double)?addYANNNZYANNN 返回double两个double参数add(int, int, int)?addYAHHHHZYAHHHH 返回int三个int参数2.2.2 函数重载的调用解析过程voidprint(intx){coutint: xendl;}voidprint(doublex){coutdouble: xendl;}voidprint(constchar*s){coutstring: sendl;}intmain(){print(10);// 1. 精确匹配 → print(int)print(3.14);// 2. 精确匹配 → print(double)print(hello);// 3. 精确匹配 → print(const char*)print(a);// 4. 类型提升char → int→ print(int)print(10L);// 5. 类型转换long → int→ print(int)return0;}重载解析优先级精确匹配 const转换 类型提升 标准转换 用户定义转换2.3 运算符重载的实现原理运算符重载本质上是函数重载的一种特殊形式编译器将运算符调用转换为函数调用。classComplex{doublereal,imag;public:Complex(doubler0,doublei0):real(r),imag(i){}// 重载 运算符Complexoperator(constComplexother)const{returnComplex(realother.real,imagother.imag);}};intmain(){Complexa(1,2),b(3,4);Complex cab;// 编译器转换为c a.operator(b)return0;}编译器转换过程运算符重载的转换过程 源代码c a b; 步骤1识别 a 和 b 的类型Complex ↓ 步骤2查找 Complex 类中的 operator 函数 ↓ 步骤3转换为函数调用c a.operator(b); ↓ 步骤4进一步转换为c operator(a, b); 最终生成汇编指令伪代码 push b ; 压入参数 push a ; 压入 this 指针 call Complex::operator mov c, eax ; 返回值存入 c2.4 函数模板的实现原理函数模板在编译时根据实参类型实例化出具体的函数。templatetypenameTTmax(T a,T b){returnab?a:b;}intmain(){intxmax(10,20);// 实例化int max(int, int)doubleymax(3.14,2.71);// 实例化double max(double, double)charzmax(a,b);// 实例化char max(char, char)return0;}模板实例化过程函数模板实例化过程 源代码max(10, 20) 步骤1编译器推导 T int ↓ 步骤2生成具体函数代码 int maxint(int a, int b) { return a b ? a : b; } ↓ 步骤3调用生成的函数 生成的函数名修饰 - MSVC: ?maxYAHHHZ - GCC: _Z3maxii2.5 编译时多态的优缺点优点缺点执行效率高直接调用代码膨胀模板实例化类型安全编译期检查灵活性低无运行时开销修改需要重新编译易于调试二进制兼容性差三、运行时多态动态多态3.1 运行时多态的核心原理运行时多态是指在程序执行阶段才能确定调用哪个函数通过虚函数表vtable实现动态绑定。运行时多态的处理流程 源代码 Animal* p new Dog(); p-speak(); 编译过程编译器生成间接调用代码 mov eax, [p] mov ecx, [eax] call [ecx offset] 运行时根据 p 实际指向的对象类型通过虚表找到正确函数 p 指向 Dog → 调用 Dog::speak() p 指向 Cat → 调用 Cat::speak()3.2 虚函数表vtable详解3.2.1 虚表的结构虚表本质一个存储虚函数指针的指针数组存储位置只读数据段.rodata生命周期程序运行期间始终存在每个类只有一份虚表所有对象共享3.2.2 虚表指针_vfptrclassObject{private:intvalue;public:Object(intx0):value(x){}virtualvoidadd(){coutObject::add()endl;}virtualvoidfun(){coutObject::fun()endl;}virtualvoidprint()const{coutObject::print()endl;}};构造函数的任务设置虚表指针_vfptr指向当前类的虚表然后初始化成员变量示例classAnimal{public:virtualvoidspeak(){coutAnimal speakendl;}virtualvoideat(){coutAnimal eatendl;}virtualvoidmove(){coutAnimal moveendl;}};内存布局3.2.3 继承关系中的虚表classObject{public:virtualvoidadd(){coutObject::add()endl;}virtualvoidfun(){coutObject::fun()endl;}virtualvoidprint()const{coutObject::print()endl;}};classBase:publicObject{public:virtualvoidadd()override{coutBase::add()endl;}virtualvoidfun()override{coutBase::fun()endl;}virtualvoidshow(){coutBase::show()endl;}// 新增虚函数};classTest:publicBase{public:virtualvoidadd()override{coutTest::add()endl;}virtualvoidprint()override{coutTest::print()endl;}virtualvoidshow()override{coutTest::show()endl;}};各类的虚表布局3.3 对象构造过程的虚表指针设置classObject{private:intvalue;public:Object(intx0):value(x){// ① 首先设置 _vfptr 指向 Object 的虚表// ② 然后初始化 value}virtualvoidadd(){coutObject::add()endl;}};classBase:publicObject{private:intnum;public:Base(intx0):Object(x),num(x10){// ① 先调用 Object 构造函数设置 _vfptr 指向 Object 虚表// ② 然后设置自己的 _vfptr 指向 Base 虚表// ③ 最后初始化 num}virtualvoidadd()override{coutBase::add()endl;}};classTest:publicBase{private:intcount;public:Test(intx0):Base(x),count(x10){// ① 先调用 Base 构造函数设置 _vfptr 指向 Base 虚表// ② 然后设置自己的 _vfptr 指向 Test 虚表// ③ 最后初始化 count}virtualvoidadd()override{coutTest::add()endl;}};构造过程中的虚表指针变化Test test; 构造过程 阶段1Object 构造函数执行 test._vfptr → Object 虚表 此时调用 add() 会执行 Object::add() 阶段2Base 构造函数执行 test._vfptr → Base 虚表 此时调用 add() 会执行 Base::add() 阶段3Test 构造函数执行 test._vfptr → Test 虚表 此时调用 add() 会执行 Test::add() 不要在构造函数中调用虚函数 因为此时派生类部分尚未构造不会调用派生类的重写版本3.4 虚函数调用过程intmain(){Test test;Object*optest;op-add();// 调用 Test::add()return0;}调用过程详解1. 通过对象指针获取虚表指针 vptr op-_vfptr 2. 通过虚表指针找到虚函数表 vtable vptr 3. 在虚函数表中查找对应函数的地址偏移量在编译时确定 // add() 是第一个虚函数偏移量为 0 funcPtr vtable[0] 4. 通过函数指针调用函数 funcPtr(this, args...) 伪代码表示 // 编译时确定函数在虚表中的偏移量假设为 0 #define OFFSET_OF_ADD 0 // 运行时通过偏移量调用 (*(op-_vfptr OFFSET_OF_ADD))(op);3.5 运行时多态的必要条件条件说明示例公有继承派生类 is-a 基类class Dog : public Animal虚函数基类声明为 virtualvirtual void speak()重写派生类提供自己的实现void speak() override基类指针/引用通过基类指针或引用调用Animal* p new Dog()四、虚函数重写的注意事项4.1 函数签名必须完全相同classObject{public:virtualvoidadd(intx){coutObject::add(int)endl;}};classBase:publicObject{public:// 错误参数不同这是同名隐藏不是重写virtualvoidadd(intx,inty){coutBase::add(int, int)endl;}};intmain(){Base base;Object*opbase;op-add(10);// 调用 Object::add(int)不是 Base 的版本// 因为 Base::add(int,int) 不是重写而是隐藏return0;}4.2 使用 override 关键字C11classObject{public:virtualvoidadd(intx){}virtualvoidfun(doublex){}};classBase:publicObject{public:// 正确明确表示要重写voidadd(intx)override{}// 错误参数不匹配编译错误// void fun(int x) override { }// 正确voidfun(doublex)override{}};override是 C11 关键字用于派生类成员函数显式重写清晰表明重写基类虚函数增强可读性。编译检查不符基类虚函数签名则报错避免错误。保障多态确保正确重写让多态正常工作。五、两种多态的对比5.1 核心区别对比对比维度编译时多态运行时多态绑定时机编译阶段运行阶段实现方式函数重载、模板、运算符重载虚函数 继承函数地址确定编译时确定运行时确定调用指令直接调用call address间接调用call [vptroffset]性能开销无额外开销一次间接寻址代码膨胀模板可能导致虚表每个类一份灵活性较低较高类型安全编译期检查运行时检查RTTI