资讯动态

C++基础语法(多态)

发布时间:2026/8/6 18:05:09 来源:尧图企业网站定制
多态的学习是建立在继承之上的如果你没有事先了解学习过继承请去看看笔者写的关于继承的文章对继承有概念之后再来学习多态。多态的坑是相当的多如果未来就业公司对多态的考察也是让人直呼这谁能想得到。但是不要慌笔者带你一起刨析多态从头到尾给你讲明白什么是多态目录什么是多态实现多态调用的条件虚函数重写的两个例外重载 隐藏(重定义) 覆盖(重写)final override抽象类接口继承实现继承多态的原理查看虚表什么是多态在一些C书籍上是没有多态概念的而是采用虚函数来称呼多态一词多用于Java语言国内叫习惯了也就这么叫了。事实上有很多花里胡哨的名词实际上指的都是同一个东西多是翻译的原因。但多态这个翻译其实还挺应景缺点是可能让同笔者一样的新手感到抽象晦涩在具体讲述多态之前回忆一下继承中的相关概念还记得在继承中提到的隐藏/重定义概念吗子类和父类之间的成员如果同名的话那么就会构成隐藏关系而且我们也提到子类是可以切片赋值给父类的这个过程不涉及到类型转换现在我们有这么一个场景分别有三个类分别是普通人类学生类军人类然后我们写一个买票函数buy()通过把不同的类的对象传给buy()buy()在内部会调用类中的buy_ticket()根据不同类的身份进行买票代码如下buy函数的参数就是person tmp因为学生类和军人类都是继承自普通人类都可以切片式转换成personclass person { public: void buy_ticket() { cout 普通人身份全价买票 endl; } }; class student: public person{ public: void buy_ticket() { cout 学生身份半价买票 endl; } }; class soldier: public person{ public: void buy_ticket() { cout 军人身份免费买票 endl; } }; void buy(person tmp) { tmp.buy_ticket(); }我们希望buy函数能够在买票的时候去调不同类各自的buy_ticket()然而执行buy函数之后我们发现买的票都是普通人身份这是因为我们把student和soldier都转换成person之后此时的tmp对象是person类型student和soldier的buy_ticket()和person中的buy_ticket()同名构成了隐藏关系此时调用buy_ticket()调用的都是person类中实现的buy_ticket()这不符合我们的预期我们想的是student和soldier转成person对象后调用的buy_ticket()是自己实现的buy_ticket()不要构成隐藏关系了而是把person的buy_ticket()实现覆盖掉具体来说person实现了buy_ticket()student和soldier也各自实现了buy_ticket()那么student在切片赋值成person后再调用buy_ticket()就不要用person实现的buy_ticket()了而是换成我student实现的buy_ticket()同理soldier也是如此把person的buy_ticket()换成soldier自己实现的buy_ticket()像这种在继承关系下父类和子类的同名同参的函数由不同的对象来调用能够实现各自类对应的功能我们就称之为多态多态调用对应上面的代码就是person类的对象test1传给buy后调用person的buy_ticket()student类的对象test2传给buy后调用student的buy_ticket()soldier类的对象test3传给buy后调用soldier的buy_ticket()实现多态调用的条件实现多态不是我们嘴上说说那么简单呀我们该怎样操作才能实现多态调用呢满足多态调用的条件如下1.实现虚函数重写2.必须由父类的指针或者引用去调用何为虚函数重写前面说到student和person的buy_ticket()不是构成了隐藏关系嘛现在我们要转成重写/覆盖关系。因为在用student的对象调用时要把person的buy_ticket()函数重写覆盖成自己的具体的做法就是要给buy_ticket()函数加上virtual关键字进行修饰表示这是一个虚函数可以被重写覆盖。同时student类和person类中的buy_ticket()函数要满足三同即函数名相同参数相同返回值相同满足这三同再加上virtual修饰为虚函数就构成了虚函数重写加下来我们按照这个规则把上面的代码进行改写实现多态调用代码如下//三同的条件我们已经满足了,三个类中的buy_ticket()函数名相同参数相同返回值也相同 //只需要给每个函数加上virtual修饰就可以构成虚函数重写了 class person { public: virtual void buy_ticket() { cout 普通人身份全价买票 endl; } }; class student : public person { public: virtual void buy_ticket() { cout 学生身份半价买票 endl; } }; class soldier : public person { public: virtual void buy_ticket() { cout 军人身份免费买票 endl; } };多态调用的虚函数重写条件我们已经满足了现在还差使用父类的指针或引用调用这个条件我们是在buy函数中调用buy_ticket()函数接下来看看buy函数void buy(person tmp) //buy函数使用的是父类的引用满足调用条件 { tmp.buy_ticket(); }那赶紧试试能不能实现多态调用吧测试结果如下成功了我们实现了多态调用解决了我们的应用场景。大家现在对多态调用这么多规矩不理解很正常会使用就行了后面笔者会讲多态调用实现的原理看完实现原理大家再回头看看就很容易理解为什么有这些规定虚函数重写的两个例外在上述虚函数重写的三同条件中返回值是可以不同的但也局限了父类虚函数返回父类对象的指针或者引用子类虚函数返回子类对象的指针或者引用我们称其为协变还有一个就是建议给析构函数无脑加上virtual修饰这样做可以避免内存泄漏的情况这是什么情况请分析一下下述代码person类中申请了10个int类型空间并写了析构函数student类中申请了20个int类型空间也写了析构函数在调用的时候把student对象赋值给一个person指针变量class person { public: int* _per new int[10]; ~person() { delete[] _per; cout person已经完成析构 endl; } }; class student : public person { public: int* _stu new int[20]; ~student() { delete[] _stu; cout student已经完成析构 endl; } }; int main() { person* test1 new person; person* test2 new student; delete test1; delete test2; return 0; }运行结果如上可以发现student类中的析构函数并没有被调用person的析构被调用了两次这意味着test1完成了它自己的析构调用释放了_per指向的内存test2中只释放了它继承自person类中_per指向的内存但是还有_stu指向的内存没有被释放造成内存泄漏delete在释放test1以及test2指向的空间时会使用指针去调用test1及test2对应类的析构函数此时test1和test2都是person*类型呀肯定是去调用person的析构函数我们希望test1去调用person对应的析构test2去调用student对应的析构函数这不就是多态的应用场景嘛所以我们要给析构函数加上virtual修饰让其满足多态调用的条件从而让test2去调用student的析构避免内存泄漏可能你会说给析构函数加上virtual怎么会满足多态调用呢三同中的函数名相同并没有被满足呀person的析构函数名为~person()student的析构函数名为~student()有此担心就放心好了因为编译器会统一将析构函数做同名化处理给析构加上virtual修饰看看运行结果重载 隐藏(重定义) 覆盖(重写)这三者的概念看着很像该如何区分这三者呢重载和重写重定义之间还是好区分的重载要求两个函数在同一作用域下重写和重定义要求两个函数分别在基类和派生类的作用域下也就是父类和子类需要区分的是重写和重定义父类和子类的函数只要函数名相同那么就构成了重定义关系重写比重定义的要求要高很多不仅要三同还要是虚函数如此才能构成重写final overridefinal源自于一个要求 写一个不能被继承的类按照C98的做法就是把构造函数都放到private区域子类调不了父类的构造函数连初始化都做不到更别谈继承了。但是这样自己创建的对象也没法调用构造连对象都创建不了C11提供了一个关键字final被该关键字修饰的类无法被继承如果用final去修饰一个虚函数那么该虚函数不能被重写也就无法多态调用override则是一个用于检查的关键字在想实现多态调用给函数加上virtual修饰时有时并不知道该虚函数是否能重写构成多态调用。此时可以给该函数再加上一个override关键字该关键字可以帮你检查该虚函数是否能重写如果不能重写就会编译报错抽象类抽象一般是指对某些事物共有特征的一种概述我们觉得抽象难以理解因为它看不到摸不着要靠我们脑子去想去理解是没有实体的概念抽象类一样抽象类意味着它不能够创建实例化具体的对象子类继承抽象类之后也不能实例化出具体的对象可能你会觉得一个类连实例化对象都做不到要这样的类有什么用呢我们不能理解一些概念知识除了某些前置知识的缺乏还有就是没有遇到合适的使用场景没有需求自然觉得无用我们先看看哪些类算是抽象类吧抽象类是作用在虚函数在类中可以称为虚方法之上的也就是被virtual修饰的函数在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类如下图的person即为抽象类不能实例化出对象子类继承后也不能实例化出对象只有重写纯虚函数子类才能实例化出对象。纯虚函数规范了子类必须重写某种程度上抽象类是想自己的函数被继承下去是作为一个接口被使用它不需要实例化出对象它会逼着子类重写纯虚函数也就是子类继承了它的虚函数接口后根据接口重写自己的功能实现多态否则子类也无法实例化出对象接口继承实现继承这里我们又引出了一个概念何为接口继承除了接口继承还有实现继承实现继承又是什么意思呢可以看出到了继承和多态这部分概念明显变得多了起来因为继承和多态作用于面向对象编程是由类的简单使用到更为抽象的使用普通函数的继承就是一种实现继承子类继承了父类函数可以使用函数继承的是函数的整体的实现我们前面所谈论的虚函数的继承是属于接口继承子类继承了父类的虚函数但是注意只继承了虚函数的接口函数头不继承虚函数具体的实现部分函数体子类要自己重写函数体不明白函数头和函数体概念请一定搜索查看接下来笔者用程序分别验证实现继承和接口继承//这是实现继承的代码验证 class person { public: void test(int val 10) { cout 我是父类的一个普通函数我的值是 val endl; } }; class student : public person { public: void test(int val 20) { cout 我是子类的一个普通函数我的值是 val endl; } }; int main() { student stu; stu.person::test(); stu.test(); return 0; }实现继承子类会继承父类函数的整体实现因此用子类对象调用父类的test()函数时函数的调用结果将是我是父类的一个普通函数我的值是10//这是接口继承的代码验证 class person { public: virtual void test(int val 10) { cout 我是父类的一个普通函数我的值是 val endl; } }; class student : public person { public: virtual void test(int val 20) { cout 我是子类的一个普通函数我的值是 val endl; } }; int main() { //因为虚函数多态调用必须用父类引用或指针 //这里把子类对象转换成父类指针 person* ptr new student; ptr-test(); return 0; }按照我们前面所说的虚函数继承就是接口继承子类只继承接口部分函数头不继承实现部分函数体那意味着ptr调用test()函数时函数头将使用从父类继承下来的函数体将会被子类重写最终结果就是我是子类的一个普通函数我的值是10很多同学曾经搞不明白为什么子类的多态调用打印的值是却是10而不是20不是说了子类会将父类的虚函数重写吗现在你是否能明白虚函数继承是接口继承重写的只是函数体而不是把整个函数都给重写了接口函数头还是要用父类的故而打印父类的缺省值10同时也能解释另一个问题就是多态要求的虚函数三同的条件虚函数表明该函数是接口继承也就是子类继承了父类虚函数后可以重写该函数的函数体再看看三同是啥函数名返回值参数不就是函数头的全部内容嘛我子类要想把父类虚函数的函数体给替换掉首先我得保证自己的函数头接口和父类的函数头接口是一致的只有接口一致才能对接替换嘛否则不就乱了套了多态的原理知道多态是什么以及如何使用了下一步就是探究多态的实现原理我们将通过通过监视和内存窗口来看看多态调用到底是怎么回事首先监视看一下没有构成多态关系时类中都有啥结果符合预期因为两个类都没有成员变量成员函数也不放在类中所以监视里查看两个类时并没有什么东西接下来让两个类的成员函数之间构成多态关系再次查看监视窗口这次再打开监视窗口就发现不一样了person里面多了一个_vfptr这个东西是什么有什么用呢别急至少我们目前知道它跟多态有关仔细观察_vfptr里面包含了一个元素把鼠标放到第一个元素[0]处看看是什么我们发现它是person::test(void)这就是我们在person里实现的test函数我们大概能推断出来_vfptr是一个函数指针数组在test函数不是虚函数时没有出现这张表可以推断这张表是存放虚函数的至于对不对我们在person里加一个非虚函数来验证一下在person里加了非虚函数no_virtual_test() 和另一个虚函数 other_virtual_fun() 。查看_vfptr里并没有出现no_virtual_test() 而出现了other_virtual_fun() 可以判断出_vfptr是一张虚函数表这张表里存放的都是虚函数如果能明白上述的内容那么我们再看看st因为st继承了person类所以它也继承了person类的虚函数表通过监视窗口是可以看到的我们再仔细看看监视图我们发现pt对象中的虚函数test()的地址和st中继承下来的虚函数test()的地址不一样图中红色方框同时可以发现pt对象中的虚函数other_virtual_fun()的地址和st中继承下来的虚函数other_virtual_fun()的地址是一样的图中绿色方框我们看看代码就明白了student类中也定义了test()函数这意味着student类中的test()函数实现将会重写person类中test()函数中的实现所以你会看到红色方框中的两个函数的地址不一样这是因为student类对test()函数重写了把person类中test()函数中的地址替换成自己的从而调用了自己实现的test()函数而这就是多态的原理现在我们也能明白为什么调用多态时必须要用父类的指针或者引用就拿上图student类来说我们把student类转换成person指针或者引用时地址就会自动偏移到person类在stuent类中的偏移地址处就能找到_vfptr这张虚函数表从而调用被重写的虚函数查看虚表接下来看看虚表存放在内存的哪个区域是放在栈区代码区堆区还是静态区虚表的地址一般是位于父类的起始位置处在32位机下我们取父类的前4个字节的内容打印出来不就是虚表的地址吗只要能找到地址我们就能大概推断出虚表在哪个区class person { public: virtual void test() { cout 我是父类的一个虚函数 endl; } }; int main() { person test; int a; cout 栈区的大概地址 (void*)a endl; int* b new int; cout 堆区的大概地址 (void*)b endl; static int c 10; cout 数据段/静态区的大概地址 (void*)c endl; const char* str test; cout 代码段/常量区的大概地址 (void*)str endl; cout 虚表的地址 (void*)*((int*)test) endl; //这串代码同时适配32位机和64位机 cout 虚表的地址 (void*)*((void**)test) endl; return 0; }从运行结果可以得知虚表的地址最接近代码段所以虚表位于代码段区域通过去内存窗口查看虚表的地址我们可以发现虚表最后会以全0标志结束(vs2022)那我们就可以写一段代码把虚表中的函数地址全部都打印出来代码如下class person { public: virtual void test() { cout 我是父类的一个虚函数 endl; } virtual void test_fun() { cout 我是父类的另一个虚函数 endl; } }; //虚函数表是个函数指针数组 void print_virtual_fun(void(*arr[])()) { for (int i 0; arr[i] ! 0; i) { printf(虚函数表地址[%d]%p\n, i 1, (void*)arr[i]); arr[i](); //这里是调用虚表里的函数检测是否是虚函数 } } int main() { person test; print_virtual_fun((void(**)()) *(void**)test); return 0; } //*(void**)test 这部分是取test类的前一个指针大小也就是虚函数表的地址 //(void(**)()) 将虚函数表地址强转成函数指针数组根据运行结果可知确实是这样最后我们简单提一提在多继承情况下哪个父函数最先被初始化的问题这取决于继承的顺序class A { public: int _a; }; class B { public: int _a; }; class C: public A, public B //先初始化A后初始B且A被继承后位于C对象的起始位置 { public: int _a; };为什么会提到这个呢是因为有一个题目考察过这点这并非无意义的事因为只有你明白了这一点才能说你理解了继承和多态的概念题目如下正确答案是C如果不理解可以画一张继承图来看看至此本篇文章就结束了本篇文章主要是对多态概念及其实现的浅浅探讨了解这么多足够使用多态了

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

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

免费获取报价