【C++】多态详解——虚函数、重写、原理、抽象类(超详细)
文章目录一、多态开篇二、多态的概念三、多态的定义及实现1. 多态的两个必须条件背下来2. 虚函数3. 虚函数的重写覆盖特别注意4. 多态使用示例5. 动物叫声示例经典四、多态中的重点坑点1. 析构函数的重写面试必考超重要2. 协变了解3. override / final 关键字C111override2final4. 重载 / 重写 / 隐藏 对比面试必考五、纯虚函数 和 抽象类1. 纯虚函数2. 抽象类六、多态的原理最难1. 虚函数表指针_vfptr2. 多态底层原理3. 静态绑定 vs 动态绑定4. 虚表存放位置七、多态知识点总结一、多态开篇前面我们把继承讲完了今天我们来学习面向对象最后一个、也是最难、最重要的特性——多态。多态是面试必考、底层原理最难、但写代码最实用的知识点。如果还没学继承的小伙伴一定要先看继承【C】继承详解——基类/派生类、作用域、默认函数、菱形继承今天我会把多态概念、构成条件、虚函数、重写、override/final、抽象类、多态原理、虚表全部讲透全程高能希望大家最后能有收获二、多态的概念什么是多态通俗来说多种形态。同一件事不同的对象去做会表现出不同的结果。比如普通人买票全价学生买票半价军人买票优先比如猫叫喵~狗叫汪汪这就是多态多态分为两种静态多态编译时多态函数重载、模板——编译时就确定调用哪个函数(之前讲过的内容在我的主页可以查看)动态多态运行时多态运行时才确定调用哪个函数——我们今天重点讲这个三、多态的定义及实现1. 多态的两个必须条件背下来要实现多态必须同时满足两个条件必须通过基类的指针 或者 引用 调用虚函数被调用的函数必须是虚函数且子类必须对父类虚函数完成重写两个条件缺一不可2. 虚函数成员函数前面加virtual关键字这个函数就是虚函数。注意只有类的成员函数才能加 virtual普通函数不行classPerson{public:// 虚函数virtualvoidBuyTicket(){cout买票-全价endl;}};3. 虚函数的重写覆盖重写覆盖的要求函数名相同参数相同返回值相同两个函数必须都是虚函数满足这些子类就重写了父类的虚函数。classPerson{public:virtualvoidBuyTicket(){cout买票-全价endl;}};classStudent:publicPerson{public:// 重写父类虚函数virtualvoidBuyTicket(){cout买票-半价endl;}};特别注意子类重写时不加 virtual 也能构成重写因为虚函数特性会被继承。但规范写法必须加 virtual4. 多态使用示例voidFunc(Person*ptr){// 多态调用看指向的对象不看指针类型ptr-BuyTicket();}intmain(){Person ps;Student st;Func(ps);// 全价Func(st);// 半价return0;}结果买票-全价 买票-半价这就是多态同一个函数调用指向不同对象执行不同逻辑。5. 动物叫声示例经典classAnimal{public:virtualvoidtalk()const{}};classDog:publicAnimal{public:virtualvoidtalk()const{cout汪汪endl;}};classCat:publicAnimal{public:virtualvoidtalk()const{cout喵~endl;}};voidletsHear(constAnimalanimal){animal.talk();}intmain(){Cat cat;Dog dog;letsHear(cat);// 喵letsHear(dog);// 汪汪return0;}四、多态中的重点坑点1. 析构函数的重写面试必考超重要父类析构函数写成虚函数子类析构函数就会自动构成重写。为什么编译器会把所有析构函数名都处理成destructor()所以名字统一了。为什么要这样做避免内存泄漏如果父类析构不是虚函数那么当使用父类Person的指针ps指向子类Student对象st后如果delete释放ps此时编译器只知道ps是父类不知道有多态就会只调用父类析构那么子类如果有堆上的资源就会造成内存泄漏只有将父类写成虚函数子类析构才能自动构成重写此时编译器就会按照正常顺序析构子类后析构父类classA{public:virtual~A(){cout~A()endl;}};classB:publicA{public:~B(){cout~B()endl;delete_p;}protected:int*_pnewint[10];};使用intmain(){A*p2newB;deletep2;// 如果父类析构不是虚函数只会调用~A()造成泄漏// 是虚函数就会先调用~B() 再 ~A()return0;}结论基类析构函数尽量写成虚函数2. 协变了解重写时返回值可以不同父类返回父类指针/引用子类返回子类指针/引用这叫协变实际很少用了解即可。3. override / final 关键字C111override检查子类函数是否成功重写写错直接编译报错。classCar{public:virtualvoidDrive(){}};classBenz:publicCar{public:// 写错会直接报错virtualvoidDrive()override{coutBenz-舒适endl;}};2final修饰类该类不能被继承修饰虚函数该函数不能被重写classCar{public:virtualvoidDrive()final{}};// 报错不能重写final函数classBenz:publicCar{virtualvoidDrive(){}};4. 重载 / 重写 / 隐藏 对比面试必考我给你总结成最清晰表格概念作用域函数名参数返回值关键字重载同一类相同不同随意无重写父子类相同相同相同virtual隐藏父子类相同随意随意无口诀同一作用域、参数不同 →重载父子类、都是虚函数、完全相同 →重写父子类、同名、不是重写 →隐藏五、纯虚函数 和 抽象类1. 纯虚函数虚函数后面写0就是纯虚函数。virtualvoidDrive()0;2. 抽象类包含纯虚函数的类叫抽象类。抽象类不能实例化对象子类必须重写纯虚函数否则子类也是抽象类。classCar{public:// 纯虚函数 → 抽象类virtualvoidDrive()0;};classBenz:publicCar{public:virtualvoidDrive(){coutBenz-舒适endl;}};使用// Car car; 报错Car*pBenznewBenz;pBenz-Drive();意义强制子类重写规范接口。六、多态的原理最难1. 虚函数表指针_vfptr只要类里有虚函数对象中就会多一个指针_vfptr 虚表指针。这个指针指向一张表虚函数表vftable。我们看大小classBase{public:virtualvoidFunc1(){coutFunc1()endl;}protected:int_b1;char_chx;};// 32位平台413(对齐)4(虚表指针) 12字节coutsizeof(Base)endl;// 输出122. 多态底层原理多态调用时去对象中取_vfptr通过虚表指针找到虚函数表在表中找到对应虚函数地址调用函数这就是运行时确定也就是动态多态。父类对象 → 用父类虚表 → 调用父类虚函数子类对象 → 用子类虚表 → 调用子类虚函数一句话多态是靠虚表指针 虚函数表 重写实现的3. 静态绑定 vs 动态绑定静态绑定编译时确定地址普通函数动态绑定运行时查虚表确定多态满足多态条件就是动态绑定否则静态绑定。4. 虚表存放位置虚函数代码段虚表代码段常量区虚表指针在对象内部栈/堆七、多态知识点总结多态是父类指针指向不同对象同样函数调用产生不同行为多态条件虚函数 重写 指针/引用虚函数virtual 成员函数声明重写父子类关系、父类是虚函数、函数声明完全相同析构函数建议全部写成虚函数防止造成内存泄漏override 的作用是检查重写final 的作用是禁止重写/继承纯虚函数就是虚函数声明后面加上 0 → 有纯虚函数的类是抽象类不能实例化多态底层虚表指针 虚函数表动态绑定运行时查虚表静态绑定编译时确定那么今天关于C 多态就全部讲完了内容非常多、非常细大家一定要多画图、多敲代码。有什么不懂欢迎私信问我我会及时做出解答下一篇我们开始学习二叉搜索树敬请期待吧bye~