资讯动态

C++多态详解

发布时间:2026/9/10 16:49:00 来源:尧图企业网站定制
目录实现条件虚函数重写的其他问题1.协变2.析构函数的重写也有点问题两个关键字overridefinal重载重写隐藏的对比--面试抽象类多态原理虚函数表与虚函数表指针--面试传参还有两个问题1.虚函数存在哪里虚函数表中吗2.虚函数表在哪里静态多态和动态多态我们之前就接触过多态比如函数重载函数模板在编译时通过传入不同的类型来实例化出从不同的函数之所以称作静态多态是因为在编译阶段完成我们把编译时归为静态运行时归为动态动态多态是调用同一个函数产生不同行为。源码被编译器预处理编译形成汇编语言汇编形成二进制代码也就是.o目标文件最后链接形成可执行文件.exe。编译期就是做翻译检查语法类型推导模板实例化此时程序没有运行内存里没有真实变量数据运行期加载.exe到内存里初始化变量的值CPU执行代码静态多态编译时多态编译阶段就能确定要调用哪个函数编译器查看func(a)a变量是int类型进行类型推导实例化一份int的普通函数最后exe里没有模板函数只有实例化的函数编译器已经确定调用哪个函数运行多态运行期间才能知道要调用哪个函数依赖对象成员变量中的序表指针vptr比如对于买票这一函数对于学生来讲价格会减半对于普通人来说价格仍然不变。这里我们着重介绍动态多态即运行确定的多态实现条件前提多态是一个继承关系下的类对象通过调用同一函数产生了不同行为。比如Student 继承了 PersonPerson买票是全价Student买票是半价。要求1.通过基类的指针或者引用指向子类对象/基类对象进而调用虚函数来实现多态说明基类的指针可以指向基类对象也可以指向子类对象base* 指针变量里存的是基类或者子类在虚拟地址空间中的起始地址它不像拷贝构造那样发生切片而是内存中保留完整基类、子类对象能调用到实际指向对象的虚函数实现不同效果。有个问题同一个类多个对象它们只是成员变量的值不同函数都是同一个每一个对象都会实例化出同一份函数还是多个对象都指向同一个个函数1.普通成员函数非virtualclass Person{int age;void show{...};};Person p1,p2内存布局p1p2内存只存age函数放在代码段不属于对象为了区分不同对象调用同一个函数得到不同的结果调用函数时候会隐式传入this指针来表明当前是哪个对象调用的这个函数参数传哪个对象的成员变量。2.虚函数 virtualclass Person{int age;virtual void show{...};};Person p1,p2内存布局p1vptr虚表指针agep2vptr虚表指针age同样虚函数被多个对象调用因此放到代码段属于类不会每个对象都复制一份p1p2都是同一个类Person因此它们的虚函数表存的函数地址都是相同的因此虚函数表相同但如果是Base p1,Person p1;它们是两个类它们有各自的虚函数因此虚函数表不同为了区分 不同类指向的不同虚函数表或者可以说 不同对象指向的 类的虚表在每个对象中加入一个成员变量vptr来区分当前对象属于哪个类会调用哪个类对应的虚函数p1.vptr和p2.vptr一个指向Base的虚表一个指向Person的虚表而之前的则是p1.vptr和p2.vptr指向同一张Person的虚表2.被调用的是virtual虚函数父类中声明虚函数子类中完成对父类虚函数的重写/覆盖说明子类必须对父类的虚函数进行重写/覆盖这样才能使基类和派生类之间的函数不同实现多种形态。什么叫做虚函数在类成员前面加关键字virtual修饰非成员函数不能加virtual。什么叫做虚函数的重写子类中定义一个与父类的虚函数完全相同的函数这里的完全相同是指函数名参数列表返回值类型完全相同仅仅是函数体不同。注意子类进行重写虚函数时即使子类的虚函数不加virtual关键字也能构成重写因为父类的虚函数被继承下来了在子类中仍保留着虚函数的属性但是不建议这么写但父类的虚函数不能不加virtual。这点笔试常考会故意挖坑判断是否会构成多态。一道经典的题等会讲原理总结1.继承时遵循查找当B类型对象调用test函数时会先查找B中是否有该函数没有的话会去继承的父类A中查找test若A中存在test则直接使用A中的test版本。而B中不会独立存储test调用的是A中定义的test逻辑。2.子类虚函数重写本质上是父类A的声明子类虚函数体的重写的组合。Effective C 条款37 绝不重新定义继承而来的缺省值虚函数重写的其他问题1.协变正常情况下虚函数重写要满足两个条件一是父类指针或引用二是虚函数的完全相同三同但协变打破了三同之中子类函数与基类函数返回值可以不同这一点 即基类返回基类的指针/引用派生类返回派生类的指针/引用class Person { public: virtual Person* Buy() { cout 正常价格 endl; return new Person; } }; class Student :public Person { public: virtual Student* Buy() { cout 打折 endl; return new Student; } }; void func(Person* p) { p-Buy(); } int main() { Person p; Student s; func(p); func(s); return 0; }2.析构函数的重写也有点问题如果基类的析构为虚函数派生类析构只要定义了不管加不加关键字virtual都构成重写因为编译器对析构函数的函数名做了特殊处理统一为destructor进行这样的处理一定是为了解决什么问题基类的指针可以指向基类本身也可以指向子类我们看这样一个场景当基类指针删除子类对象时只会调用基类的析构函数子类的析构函数被跳过从而无法正确地释放子类资源class A { public: ~A() { cout ~A() endl; } }; class B:public A { public: ~B() { delete _b; cout ~B() endl; } protected: int* _b new int[0]; }; int main() { A* p new B;//基类指向派生类对象B delete p;//仅调用A的析构函数如果不构成多态那就导致子类调用父类的析构 //结果~A() return 0; }那这时我们就要解决一个问题就是这个指向子类的父类指针调用析构函数期望先调用子类的析构再调用父类的析构这时候就期望通过多态来解决就是调用的析构不和类型A*有关而是期望和指针p指向的对象有关delete 父类调用父类的析构delete子类期望调用子类的析构多态的两个条件1.父类的指针/引用已经达成delete p父类;通过父类的指针调用析构2.虚函数的重写加virtual无返回值无参数函数名被处理成distructor这就是要析构函数为什么要处理成相同函数名的原因----面试class A { public: virtual ~A() { cout ~A() endl; } }; class B:public A { public: ~B() { delete _b; cout ~B() endl; //确保父类中的对象被销毁 //A::~A(); } protected: int* _b new int[0]; }; int main() { A* p new B; delete p; //结果~B() ~A() return 0; }这里如何确保在子类对象调用析构后父类对象也被销毁需要手动在子类析构函数中调用父类析构吗不需要编译器在子类析构函数执行完毕后会自动插入父类析构函数的调用代码保证先子后夫而且不要自己显式调用析构因为会破坏对象的生命周期等到对象生命周期结束之后编译器自己调用析构就好。即派生类先析构子类那一部分 再析构父类--合成版本--自动调用析构两个关键字overridefinaloverride检查是不是完成重写函数名写错/参数写错final我们不想让派生类重写这个虚函数重载重写隐藏的对比--面试抽象类抽象类是指包含纯虚函数的类啥是纯虚函数仅定义接口但不能实例化出对象。加 0实现比如这样一个场景我们要定义一个形状的系统包括矩形圆形两个类基类需要定义计算面积这样的函数但是计算面积是一个抽象的概念我们不知道这个函数具体要怎么实现但此时我又必须得实现。class Shape { public: virtual double Area() { return 0.0; //没有实际意义 } }; class Circle : public Shape { private: double _r; public: Circle(double r) : _r(r) {} // 子类中重写Area()实现圆形面积 double Area() override { return 3.14 * _r * _r; } };这样的话调用基类s.Area()没有意义因此采用纯虚函数的模式只需要将抽象的函数进行声明而不用定义这样避免了基类的Area无意义而且子类想要使用Area时只需要对这个纯虚函数进行重写否则子类也会成为抽象类无法实例化class Shape { public: virtual double Area() 0; }; class Circle : public Shape { private: double _r; public: Circle(double r) : _r(r) {} // 子类重写Area(),否则会无法实例化 double Area() override { return 3.14 * _r * _r; } }; int main() { //Shape s;//E0322,不允许是由抽象类类型的Shape对象 Circle c(1); cout c.Area() endl; return 0; }纯虚函数的继承是让子类继承其接口然后子类通过这个接口来重写纯虚函数才能实例化保证了多态接口的一致。多态原理多态的原理是什么为什么同一个接口传入的对象类型不同产生的结果也不同我们看下面一段代码Base类类型的b对象的字节数是多少b中包含两个成员对象_b和_ch最大对齐数是它们的字节数是41结构体的总大小就是结构体成员最大对齐数的整数倍应该是8个字节但实际得到的是16说明b对象中还有别的成员。class Base { public: virtual void Func1() { cout Func1() endl; } protected: int _b 1; char _ch x; }; int main() { Base b; cout sizeof(b) endl; return 0; }虚函数表与虚函数表指针--面试对象中的_vfptr指针叫做虚函数表指针virtual function它是一个指针指向了一个虚函数表虚函数表是一个函数指针数组。编译器会为每个包含虚函数的类对象前面自动添加一个隐藏的成员变量_vfptr_vfptr的作用是指向当前所属类的vtable虚函数表啥是虚函数表呢它是储存虚函数地址的表该表中存储了类中所有的虚函数的内存地址包括子类从父类中继承的未重写的虚函数以及子类重写后的虚函数针对后者若子类重写了虚函数子类的虚函数表中会将重写后的虚函数地址覆盖父类对应的虚函数地址重写所以也称覆盖若子类新增了自己的虚函数会在子类虚函数表的末尾加上这个新增虚函数的地址。若子类没有重写继承的虚函数子类的虚函数表中会保留父类对应的虚函数地址。class Parent { public: virtual void func1() {} // 父类虚函数1 virtual void func2() {} // 父类虚函数2 }; class Child : public Parent { public: void func1() override {} // 子类重写func1 virtual void func3() {} // 子类新增虚函数func3 };我们看这段代码父类的虚函数表[Paretn::func1Parent::func2]子类的虚函数表[Child::func1Parent::func2Child::func3]子类重写的虚函数func1覆盖父类地址子类新增的虚函数func3新增到虚函数表子类中没有重写的虚函数会保留对应父类的虚函数地址。结果我们可以看到子类虚函数func3也在子类的虚函数表中。也可以看到子类和父类的虚函数表指针_vfptr中存放的虚函数表地址不同因为表内的虚函数地址不同。可以理解为同类型的对象虚表共用不同类型虚表各自独立。传参接下来我们分析为什么传入父类/子类对象的指针/引用能通过相同的父类指针/引用的形参接收来实现产生不同的结果class Person { public: virtual void BuyTicket() { cout 买票全价 endl; } protected: int _num; }; class Student :public Person { public: virtual void BuyTicket() { cout 买票半价 endl; } protected: int _id; }; void Func(Person* ptr) { ptr-BuyTicket(); } int main() { Person p; Student s; Func(p); Func(s); return 0; }1.如何拿到_vfptr中存储的虚函数表地址通过提取对象s的头四个字节来得到Person p; Person* pp p; printf(Person虚表的地址%p\n, *(int*)pp);让一个Person类型的指针pp指向Person对象p即pp得到的是p对象的地址但是如果直接*pp解引用得到的是整个Person类型的p对象内容我们现在将pp强转为int*类型这样进行 *pp 解引用得到的就是 p 中头四个字节_vfptr 的内容也就是指向的虚表地址了。2.那ptr-BuyTicket()具体是怎么实现的与普通函数不同虚函数先从Student s对象中取出隐藏的_vfptr指针_vfptr指向Student对象的虚函数表表中存储着Student::BuyTicket的地址接着调用该地址指向的函数。主要区别是在编译阶段ptr的类型尽管是Person*但编译器不能确定它所指向的对象是Person类型的对象还是Student类型的对象不能直接绑定Student::ButTicket的地址而是在运行时通过ptr指向的_vfptr查表找到Student::Buyticket的地址call进行调用从而实现不同的结果。我们称在运行时进行函数地址的绑定为动态绑定。还有两个问题1.虚函数存在哪里虚函数表中吗虚函数和普通函数一样都是存在于代码段的只是将虚函数的地址复制到虚表中2.虚函数表在哪里类似之前的取对象指针所指的头四个字节的内容找到存储的虚表地址来与栈区堆区静态区常量区对比存放在常量区const char* p2 xxxxxxxx; printf(常量区: % p\n, p2);、 Person * p3 b; printf(Person虚表地址: % p\n, *(int*)p3);

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价