ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++之初识多态(Visual Studio 2019)的使用

C++之初识多态(Visual Studio 2019)的使用 此文章关于多态的代码全部是使用Visua Studio2019 (x86) 实现的C多态在不同编译器中的实现细节可能不同所以部分情况下相同代码运行结果可能不同在此声明。多态的概念多态顾名思义即多种形态。具体来说就是对于某个行为不同的对象去完成该行为会产生不同的状态。就像 “叫” 这个行为猫来完成时其叫声是 “喵喵喵” 而狗则是 “汪汪汪”。还有坐公交车老人和小孩坐车是免票而其他人则是全价票。同样的一个动作不同的对象来执行其实现的方法不同。多态的定义与实现多态是建立在继承基础上的在不同的继承关系下的类对象调用同一函数产生不同的行为。在C中继承体系下构成多态需要满足以下两个条件1.必须通过基类指针或引用调用虚函数。2.被调用函数必须是虚函数且派生类必须对基类虚函数进行重写。虚函数虚函数即被 virtual 修饰的类的成员函数。123456classA {public:virtualintadd(inta,intb) {returna b;}}使用virtual修饰虚函数时只需要修饰类内部修饰即可。12345678classA {public:virtualvoidprint_1();};virtualvoidA::print_1() {cout 1 : class A endl;}虚函数的重写虚函数的重写指的是派生类中有一个与基类完全相同的虚函数(即派生类的虚函数与基类虚函数的返回值类型参数列表函数名完全相同)称子类的虚函数重写了基类的虚函数。12345678910111213141516171819202122232425classA {public:virtualvoidprint_1() {cout 1 : class A endl;}};classB :publicA {public:virtualvoidprint_1() {cout 1 : class B endl;}};voidfunc(A a) {a.print_1();}intmain() {A a;B b;func(a);func(b);return0;}PS.子类重写父类虚函数时子类虚函数可以不加virtual关键字这样也可以构成重写——继承后基类的虚函数被继承下来了在派生类之中依旧保持虚函数属性。但是这种写法并不规范并不建议使用。虚函数重写的两个例外1.协变基类与派生类虚函数返回值不同派生类重写基类虚函数时若基类虚函数的返回类型是基类的指针或引用新的返回类型是派生类的指针或引用依旧可以构成重写。这样的重写被称为返回类型协变。123456789101112131415classA {public:virtualA print_1() {cout 1 : class A endl;return*this;}};classB :publicA {public:virtualB print_1() {cout 1 : class B endl;return*this;}};需要注意的是只有返回对应类型的指针或引用才会构成协变单纯返回派生类或基类类型并不构成协变。123456789101112131415classA {public:virtualA print_1() {cout 1 : class A endl;return*this;}};classB :publicA {public:virtualB print_1() {cout 1 : class B endl;return*this;}};2.析构函数的重写基类与派生类析构函数函数名不同当基类的析构函数为虚函数时此时派生类的虚构函数只要定义无论是否被virtual关键字修饰都与基类析构函数构成重写。1234567891011121314151617181920classA {public:virtual~A() {cout A endl;}};classB :publicA {public:virtual~B() {cout B endl;}};intmain() {A* a newB;deletea;return0;}析构函数的重写最常见的应用便是基类指针指向派生类对象的情况下如果我们所使用的派生类对象是从堆上 new 来的那么在没有重写析构函数的情况下在基类对象 delete 时只会调用基类的析构函数释放基类的空间而派生类对象所占用的空间则不会被释放就会导致内存泄漏。C11 override final1. override在C中override关键字用来表示当前虚函数重写了基类虚函数。在函数比较多继承关系负责的情况下override关键字可以起到提示编写者与其后继者某个函数重写了基类的虚函数——也代表这个函数不是派生类中定义的。override关键字可以强制编译器去检查某个函数是否重写了基类虚函数若否则会报错。1234567891011121314classA {public:virtualvoidprint() {cout a endl;}};classB :publicA {public:virtualvoidprint() override {cout b endl;}};当override修饰的函数并非重写的基类虚函数时会报错1234567891011121314classA {public:virtualvoidprint() {cout a endl;}};classB :publicA {public:virtualvoidnot_print() override {cout b endl;}};2.finalfinal用于修饰虚函数表明该虚函数不能被重写我们是可以直接用final修饰父类虚函数的表明该虚函数不能被重写但并不建议这样做虚类既然与平常的类不同其实例化时自然会比普通的类多一份开而final只是指明了这个虚函数不可以被重写但是虚类的相关开销依旧在。123456789101112131415161718192021222324252627282930313233343536373839404142classA {public:inta;virtualvoidprint() final{cout a endl;}};classB :publicA {public:intb;voida_print() {cout b endl;}};classC {public:inta;voidprint() {cout a endl;}};classD :publicC {public:intb;voida_print() {cout b endl;}};intmain() {B b;D d;b.b 3;b.a 2;d.a 2;d.b 3;while(0);return0;}重载重写重定义重载将语义或功能相似的函数用同一个名字表示。条件1.两个函数在同一个作用域2.函数名相同3.参数列表不同4.返回值可以不同重写 (覆盖)覆盖是C中多态的基础使派生类可以重新定义基类的虚函数。条件1.两个函数别属于基类成员函数与派生类成员函数2.两个函数都必须是虚函数3.函数名相同 (析构函数除外至少看起来不同)4.返回值类型相同 (协变例外)5.参数列表相同6.函数的访问权限可以不同(如基类中的虚函数的访问权限是 private 派生类中可以重写为public、protected)重定义 (同名隐藏)在继承体系中子类与父类中有同名成员子类可以屏蔽父类指针对子类中父类成员的直接访问。(但是可以使用 :: 访问父类成员)条件1.在不同作用域2.函数名相同3.返回值可以不同4.在继承体系中只要不构成重写就是重定义参数相同时基类没有关键字virtual修饰基类的函数被隐藏参数不同时无论有没有virtual关键字修饰基类函数都被隐藏12345678910111213141516171819202122classA {voidprint() {cout a endl;}};classB :publicA {public:intb;voidprint() {cout b endl;}};intmain() {A a;B b;a.print();b.print();while(0);return0;}纯虚函数及抽象类在有些时候基类的某些行为在没有被派生类重写之前会非常不明确或者说不合常理不好定义。比如我们先前举过的例子——动物的 “叫” 这个行为在动物这个基类中我们很难定义它具体是一种什么样的行为表达方式是怎样的。我们是无法对这个行为做出具体的定义的无法提供实现。但是作为一种 “共性” 我们又想要在基类中声明它。于是乎在虚函数的基础上C又提出了一种新的函数——纯虚函数。在虚函数后面写上 0则这个函数就是纯虚函数。包含纯虚函数的类叫做接口类——也被称为抽象类抽象类不能实例化对象。派生类继承抽象类后如果不重写基类的纯虚函数也无法实例化对象。纯虚函数规定了派生类必须重写同时也实现了接口继承。12345678910111213141516171819classA {public:virtualvoidprint() 0;};classB :publicA {public:intb;virtualvoidprint() {cout b endl;}};intmain() {B b;b.print();while(0);return0;}接口继承与实现继承普通函数的继承是一种实现继承派生类继承了基类的函数可以使用函数这种继承继承的是函数的实现。虚函数的继承则是一种接口继承派生类继承的只是虚函数的接口目的是为了重写从而达成多态继承的是一个接口。所以如果不实现多态就不要把函数设置为虚函数。多态的原理1234567891011121314151617181920classA {public:int_a1;int_a2;virtualvoidstudy() {cout study endl;}A():_a1(1),_a2(2){}};intmain() {A a;return0;}以上代码中类A的实例化对象a在实例化后其内存如上图所示明显在我们所定义的变量a1, a2之前还有着一串神奇的数字——00 75 9b 34。(据说有些编译器这玩意可能在后边)单从表面上来看它应该是一个指针。而实质上它的确是一个指针。在VS调试模式下的局部变量窗口中我们可以很清楚的看到这玩意还不是一个简单的指针它是一个二级指针指针名为 __vfptr v——virtual f——function我们通常称其为虚函数表指针。进一步将这个虚函数表指针展开来看很容易看到这个虚函数表指针指向的对象是一个函数指针而函数指针指向的对象则是我们代码中所定义的虚函数——A::study(void)一个含有虚函数的类中都至少有一个虚函数表指针因为虚函数的地址要被存放到虚函数表中。虚函数表也简称虚表。下面我们将之放入继承体系之中来分析。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647classA {public:int_a1;int_a2;virtualvoidfunc1() {cout A::func1 endl;}virtualvoidfunc2() {cout A::func2 endl;}virtualvoidfunc3() {cout A::func3 endl;}A():_a1(1),_a2(2){}};classB :publicA{public:int_b1;int_b2;virtualvoidfunc1() {cout B::func1 endl;}virtualvoidfunc2() {cout B::func2 endl;}B():_b1(3),_b2(4){}};intmain() {A a;B b;return0;}通过以上调试图片可以看出1.基类中有一个虚函数表指针虚函数表中存储着三个基类虚函数的地址。2.派生类对象中也有一个虚函数表指针且该虚函数表由两部分构成——派生类重写的虚函数以及从基类中继承的虚函数。3.基类对象与派生类对象的虚函数表中的内容是不同的由于我们在派生类代码中重写了虚函数func1以及func2所以派生类的虚函数表中对应位置存储的是被派生类重写后的B::func1(void)以及B::func2(void)。所以虚函数的重写也被称为覆盖覆盖指的是虚表中虚函数的覆盖派生类重写的虚函数地址覆盖了基类的虚函数地址。重写是语法层面的称呼覆盖是原理层面的称呼。4.虚函数表本质是一个存放虚函数指针的指针数组一般情况下这个数组最后会放一个nullptr。5.派生类虚表生成的过程如下首先将基类中虚表内容拷贝一份到派生类之中而后如果派生类重写了某个虚函数用派生类自身的虚函数的地址覆盖虚函数表中基类虚函数的地址最后派生类将派生类之中新增的虚函数增加到虚函数表的最后。6.虚函数表中只存放继承或派生类自身定义的虚函数非虚函数不会被存放。虚函数存在哪里虚表存在哪里与普通的函数一样虚函数存放于代码段中只是它的指针被存储到了虚表之中。虚表并不存放于对象之中对象中存储的是虚表指针。虚表存放于代码段之中。(个人实力原因暂时无法证明)多态的原理分析了这么多多态的原理究竟是什么这里就需要用到我们之前屡试不爽的方法——通过基类指针/引用调用函数1234567891011voidFunc(A* tmp) {tmp-func1();}intmain() {A a;B b;Func(a);Func(b);return0;}通过对以上代码进行反汇编我们可以看到在使用基类引用调用虚函数时的操作如下1mov eax,dword ptr [tmp]tmp中存储的是基类对象的指针将tmp中的值移动到eax寄存器中。1mov edx,dword ptr [eax]将eax的值指向的内容存储到edx中即将基类对象前4个字节(虚表指针)移动到edx中。1mov ecx,dword ptr [tmp]取tmp指针存储的值到ecx寄存器中1mov eax,dword ptr [edx]取edx寄存器中的值指向的内容相当于取了虚表的前四个字节即虚表中存储的第一个虚函数的地址。如果我们此处调用的是fun2或者func3则会在edx基础上偏移一定字节。1call eax调用eax寄存器中存储的指针指向的函数。动态绑定与静态绑定静态绑定静态绑定又被称为前期绑定或早绑定在程序编译期间便已经确定了程序的行为也被称为静态多态。动态绑定动态绑定又被称为后期绑定或晚绑定在程序运行期间根据具体拿到的类型来确定程序的行为调用具体的函数也称为动态多态。多继承下的虚函数表我们上文所主要讲解使用的是单继承情况下的虚函数表。这里我们就简单研究下如果在多继承体系下虚函数表又会有怎样的不同。
返回列表