ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

c++复习::深入理解多态

c++复习::深入理解多态 一、多态的概念直白来说就是去完成某个事情不同的对象会呈现出不同的状态价值1、举个例子比如说购买高铁票这个行为如果是普通人就是原价购买如果是学生的话就是半价购买如果是军人的话可以优先走绿色通道购买……总的来说在生活中去做一件事情不同的人去做会有不同的反馈这就是多态。二、多态的定义和实现2.1 构成多态的条件首先多态现象的产生是在继承的基础上产生的。多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。比如Student继承了Person。Person对象买票全价Student对象买票半价。#includeiostream using namespace std; class Person { public: virtual void Buyticket() { cout 买票全价 endl; } }; class Student:public Person { public: virtual void Buyticket() { cout 买票半价 endl; } }; void func(Personptr) { ptr.Buyticket(); } void test1() { Student s1; Person p1; func(s1); func(p1); } int main() { test1(); return 0; }想要构成多态基类的成员函数前面必须加入virtual而派生类可加可不加但为了增强可读性这里还是建议加上。写法上需要满足的条件公有继承基类函数加virtual派生类完成重写 override使用【基类指针 / 基类引用】接收派生对象构成多态需要以下两个条件重点1对父类虚函数的重写-三同函数名、参数、返回值2必须是父类的指针或者引用去调用我们来画图分析一下构成多态与不构成多态的情况#includeiostream using namespace std; class Person { public: virtual void Buyticket() { cout 买票全价 endl; } }; class Student:public Person { public: virtual void Buyticket() { cout 买票半价 endl; } }; void func1(Person*ptr) { ptr-Buyticket(); } void func2(Person ptr) { ptr.Buyticket(); } //构成多态 void test1() { Student s1; Person p1; cout 构成多态: endl; func1(s1); func1(p1); cout --------------------- endl; cout 不构成多态: endl; func2(s1); func2(p1); } int main() { test1(); return 0; }根据运行结果可以清楚的看出func1()支持多态而func2()却不支持多态。原因我们首先得知道虚函数指针(vfptr)指向了虚表存在于只读数据段全局唯一每一个类一份虚表而不是每个对象一份而每个vfptr存在于带有虚函数的基类子对象的头部也就是说每个基类子对象里面存放着虚指针。当s1被构造出来时Person子对象头部自带vfptr而这个vfptr自动指向Student的虚函数表然后当执行func时指针ptr指向了对象s1的起始地址也就是Perosn部分但是呢Student对象是完整保留的然后我们执行BuyTicket()时通过指针 p 拿到地址 APerson 子对象首地址从地址 A 处取出保存的vfptr虚函数指针vfptr 指向【Student 的虚函数表】在虚函数表里根据函数偏移找到Student::BuyTicket()函数地址跳转调用 Student::BuyTicket。而当你进行值传递时会调用拷贝构造形成一个新的Person对象可以理解为func2(Person ptr)内部这个ptr就是一个纯Person对象调用BuyTicket()当然是调用自己的了换句话理解新建出来的这个 Person 对象构造时把自身 vfptr 设置为Person 的虚函数表核心分水岭一句话区分Person* pp 只是地址引用操作原来完整的 Student 对象vfptr 保持 Student 的虚表Person p创建全新独立基类对象vfptr 被重置为基类自己的虚表和原来 Student 切断联系。一张图理解好思考你可能会有这样的疑惑-我直接用在函数体里面用if……else不也可以达到这样的效果吗为什么非得用多态来完成呢——答有些场景下必须得用多态才能解决比如父类的指针或者引用调用析构函数2.2 虚函数的重写虚函数即被virtual修饰的类成员函数称为虚函数虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。两个例外1.协变返回值可以不同-但是必须是父子关系的指针或者引用其实不一定是自己的父子类其他的父子类也行class Person { public: virtual Person* f() { return this; } }; class Student : public Person { public: virtual Student* f() { return this; } }; int main() { return 0; }2.子类的virtual可以省略因为虚函数的重写本身就是接口继承我把除函数体以外的全部部分都可以继承下来然后再去重写继承父类的这个函数的实现 这样只要父类写了virtual就可以了。class A { public: virtual void func(int val 1) { std::cout A- val std::endl; } virtual void test() { func(); } }; class B : public A { public: void func(int val 0) { std::cout B- val std::endl; } }; int main(int argc, char* argv[]) { B* p new B; p-test(); return 0; }我们刚才分析过了虚函数的重写是继承函数体以外的所有内容包括返回值和形参当你创造出B的对象时p-test()时因为B中没有test所以用了从A中继承下来的test这里满足父类的指针调用满足多态然后再调用func又因为A中的func和B中的func构成了函数重写然后this会根据你动态绑定的对象查询虚表调用B::func(int)在编译期发生替换将形参改成静态绑定的A中func函数的形参也就是int val1最后执行B中的函数体coutB-valendl;这里有个重点动态绑定和静态绑定:this 静态类型A* const由函数定义所在类决定test定义在 class A 中✅this 动态类型指向堆上的 B 对象运行时真实对象类型1. 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态编译时比如函数重载2. 动态绑定又称后期绑定(晚绑定)是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态运行时。函数版本选择时我们依靠的是动态绑定默认形参我们看的是静态绑定。析构函数的重写如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor。2.3 重载、覆盖和隐藏的对比三、抽象类3.1抽象类的介绍在虚函数后面加上0就形成了纯虚函数含有纯虚函数的类我们称为抽象类抽象类不能实例化出对象派生类继承后也不能实例化出对象必须对虚函数重写才能实例化出对象纯虚函数规定了虚函数必须重写更能体现接口的继承1.抽象类不能实例化出对象2.子类必须重新父类的虚函数不重写的话自己也无法实例化总的来说纯虚函数强制子类必须重写虚函数相当于是一种强制性的要求 如果不重写的话代价就是自己也和抽象类父类一样无法实例化出对象3.2总结实现继承与接口继承普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数充分说明了多态的意义就是为了重写虚函数而生的四、多态的底层原理4.1虚函数表首先我们先用一个例子来对底层进行一波剖析计算出size(base)的大小根据内存对齐规则很多人计算出的base是8但是这里是12(x86环境下)为什么呢因为类里面只要出现了虚函数就一定存在虚函数表指针指向虚表。_vfptr就是一个虚函数表指针一般这个_vfptr都放在所有对象的最前面虚函数表指针指向虚表虚表中存放着虚函数的地址。我们来分析一行代码class Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } virtual void Func() { cout 买票-全价 endl; } int _a 0; }; class Student : public Person { public: virtual void BuyTicket() { cout 买票-半价 endl; } int _b 1; }; void Func(Person* p) { p-BuyTicket(); } void test4() { Person* p1 new Person; Person* p2 new Student; } int main() { test4(); return 0; }我们来画图分析:从上的分析我们可以总结出1、虚函数表简称虚表本质上是一个虚函数指针数组2、子类在创建对象的时候会拷贝父类的虚函数表然后如果没有发生重写比如Func 那虚函数表存的就是父类的虚函数如果发生了重写比如BuyTicket()那么子类首先会继承父类的接口然后重写父类的实现然后用这个虚函数的地址覆盖掉原先拷贝父类虚函数的位置。3、通过对2的分析我们可以知道1重写是语法层的概念如果发生了多态那么子类会继承父类的接口然后重写父类的实现。2覆盖是原理层的概念当子类重写了父类的实现后会将虚表中对应的该函数的地址更新成新的虚函数地址。4.2多态的原理1在没有发生多态的时候相当于一个普通函数的调用是在编译时就确定了的。2如果发生了多态那么编译器只能通过找到对应的虚函数表的位置然后虚函数表里面存的函数是什么就调用什么。这充分说明了一个道理就是多态并不是在编译时就确定的编译器只是知道自己需要调用的函数在什么地方但是具体这个函数是什么他并不知道只有等到汇编代码转成二进制代码后执行了那么才会知道调用的是什么我们来看这段代码class Base { public: virtual void Func1() { cout Base::Func1() endl; } virtual void Func2() { cout Base::Func2() endl; } void Func3() { cout Base::Func3() endl; } private: int _b 1; }; class Derive : public Base { public: virtual void Func1() { cout Derive::Func1() endl; } virtual void Func4() { cout Derive::Func4() endl; } private: int _d 2; }; int main() { Base b; Derive d; }我们分析得出d应该存有三个虚函数b应该存有两个虚函数单一继承全程只有一个虚指针被派生类复用、指向Derive 专属的虚表。在vs的调试中Derive的虚函数表指针嵌套在了base当中但是这个虚函数表指针早就改变了指向指向了Derive的虚表。4.3 多态的思考1、虚表是什么阶段生成的——编译阶段一个类是否存在虚表关键就在于是否有虚函数而是否有虚函数取决于是否有virtual关键字所以是在编译阶段生成。2、对象中的虚表指针是什么时候初始化的——初始化列表阶段虚表指针的初始化还要早于构造函数虚表指针是在初始化列表阶段进行的。3、虚表是存在哪里的——代码段常量区因为我们知道虚函数是存在代码段常量区而虚表的地址和虚函数的地址其实挺接近的。但是这样其实不够严谨所以我们自己来写一个代码判断虚表具体是存在哪个区域。class Derive : public Base { public: // 重写基类的func1 virtual void func1() { cout Derive::func1 endl; } virtual void func3() { cout Derive::func1 endl; } void func4() { cout Derive::func4 endl; } protected: int b 2; }; int main() { int i 0; static int j 1; int* p1 new int; const char* p2 xxxxxxxx; printf(栈:%p\n, i); printf(静态区:%p\n, j); printf(堆:%p\n, p1); printf(常量区:%p\n, p2); Base b; Derive d; Base* p3 b; Derive* p4 d; printf(Person虚表地址:%p\n, *(int*)p3); printf(Student虚表地址:%p\n, *(int*)p4); printf(虚函数地址:%p\n, Base::func1); printf(普通函数地址:%p\n, Base::func5); return 0; }我们可以清楚的看到两个虚表的地址隔得挺近都在常量区而虚函数地址也在常量区两者却隔得很远。存在常量区的原因1、虚函数表的内容在编译的时候就确定了后面不需要修改因此适合放在只读区2、节省内存多个同类对象共享同一个虚函数表只需要在内存中存一份。
返回列表