c++多态学习小记
2021/5/22 22:55:14
本文主要是介绍c++多态学习小记,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
- 联编
- 多态的引入
- 多态的实现
- 1.函数重载--静态联编
- 2 虚函数-动态联编
- 虚函数的实现方法:
- 基类的构造函数不允许定义为虚函数
- 析构函数一般定义为虚函数
联编
首先先了解啥是联编,先看书上的概念
联编: 是指确定函数调用和函数代码段之间的映射关系。
静态联编:是只在编译时确定了函数调用的具体操作对象;
动态联编:是只在程序运行过程中动态确定函数调用的具体对象。
换句话说
联编就是找到应该调用哪个函数的过程;
看个例子
#include <iostream> using namespace std; class animal{ public: void breathe() { cout << "animal breathe" << endl; } };
在编译以上代码后,编译器会记住函数 breathe() 的地址,每次对animal的实例调用该函数时,编译器都会按照固定的调用路径执行,为静态联编,即在编译时就已确定函数的调用路径。例子1就是静态联编。
多态的引入
多态的目的即一种接口,多种实现,如下例子1,
#include <iostream> using namespace std; class animal{ public: void breathe() { cout << "animal breathe" << endl; } }; class fish: public animal{ void breathe(){ cout<<"fish bubble"<<endl; } }; int main() { fish f; animal *p = &f; p->breathe(); return 0; } //输出:animal breathe
上例的本意是,通过父类指针 去访问子类的成员方法,即应输出 “fish bubble”,但其结果仍调用的是父类的成员方法。
原因在于:
在编译器编译时,需要确定每个对象调用函数的地址,称为早期绑定。在构造fish类的对象时,先要调用animal的构造函数,再调用fish类的构造函数。二者拼成一个完整的fish对象。
当将fish类的对象f 的地址赋给animal类的指针p时,需要进行类型转换(可以类比 int a = 1.5),即p此时会指向animal的地址,调用的就是animal的breathe()函数了。
多态的实现
1.函数重载–静态联编
看下面例子2
#include <iostream> using namespace std; class point{ private: int x; int y; public: point(int xx, int yy) { x = xx; y = yy; } double area() { return 0.0; } }; class circle: public point{ private: int r; public: circle(int xx, int yy, int rr):point(xx,yy){ r = rr; } double area() { return 3.14 * r * r; } }; int main() { point p(1,1); circle c(2,2,2); cout<<p.area()<<endl; cout<<c.area()<<endl; cout<<c.point::area()<<endl; return 0; } 输出 0 12.56 0
这里,采用跳转指令,告诉了编译器该调用哪个类的实例的成员方法。
2 虚函数-动态联编
同样是还是上面的例子2修改如下
#include <iostream> using namespace std; class animal{ public: virtual void breathe() { cout << "animal breathe" << endl; } }; class fish: public animal{ void breathe(){ cout<<"fish bubble"<<endl; } }; int main() { fish f; animal *p = &f; p->breathe(); return 0; } 输出 fish bubble
只是声明基类的成员方法为虚函数,即实现了我们想要的功能,即:通过基类指针访问子类的成员变量和成员函数。注意,如果不声明为虚函数,则基类指针只能访问子类的成员变量,并不能访问成员方法。
虚函数的实现方法:
当brearhe() 被声明为虚函数时,编译器在编译时会为每个包含虚函数的类创建一个虚表(为1维数组),这个虚表里会存放虚函数的地址,并为每个类提供一个虚表指针,指向各自对应的虚表。进而,在程序运行的时候,编译器根据对象的类型去调用各自对应的函数。对于上例,当fish类的实例f创建后,其内部的虚表指针会指向fish类的虚表,进而会调用fish类的breathe方法。
上述描述刚看起来可能有点不好理解,借用别人博客上的一张图:
以base基类和derive子类为例,当类中有虚函数时,一个对象的构成会包括一个虚表指针vfptr 和这个对象本身,32位编译器下这个指针占4个字节。这个虚表指针会指向某一个虚表, 虚表是一个一维数组,里面包括各个虚函数的地址。当调用虚函数时,编译器首先会查看对象中的虚表指针,然后转到相应的虚函数地址表。这里需要注意, 无论类中有多少个虚函数,只需要在类中添加一个虚表指针即可,只是这个虚表的大小不同了。,可以想到,对于多重继承,会存在多个虚表指针和多个虚表,每个虚表指针指向各自对应的虚表。
由上可见,在使用虚函数时也会带来一定的成本,包括:
1)每个对象都会增大,增大一个存储地址的空间;
2 )创建类的时候会额外创建一个虚表;
3) 调用函数时会增加额外的在虚表中查找地址的操作。
总结,对于虚函数的调用来说,每个对象内部都会有一个虚表指针,被初始化为指向本类的虚表。不管对象类型如何去转换,该虚表指针是固定的,因此实现了动态的对象函数的调用。
基类的构造函数不允许定义为虚函数
自己测试如上。这个问题网上有很多回答:包括,虚函数的调用需要虚函数表指针,而该指针存放在对象的内容空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数
我个人比较认同的一个回答是:
假如基类的构造函数是虚的,那么我们在实例化一个子类对象时,由于虚函数的特性,子类会调用子类自己的构造函数。又由于继承机制,子类实例化时,会先调用基类的构造函数,再调用子类的构造函数。这样下来二者冲突。故 这样做没啥意义。
析构函数一般定义为虚函数
如下例子
#include <iostream> using namespace std; class animal{ public: animal(){ cout<<"animal"<<endl; }; ~ animal(){ cout<<"~animal"<<endl; }; }; class fish: public animal{ public: fish(){ cout<<"fish"<<endl; }; ~ fish(){ cout<<"~fish"<<endl; }; }; int main() { fish f; animal *p = &f; delete p; return 0; } 输出: animal fish ~animal
如果析构函数不采用虚函数,释放指针p只是释放了基类的内存,没有释放子类的内存;这样会导致内存泄漏。原因同静态联编。
为什么c++ 默认的类不采用虚析构函数呢,因为虚函数会导致额外的开销,对于不需要继承的类,声明为虚函数会浪费内存,故默认没有采用。。
由上可见,当需要该类作为基类时,一般采用虚析构函数;这样会导致内存泄漏。
这篇关于c++多态学习小记的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-06-06Package Easy(基于 NSIS 的打包exe安装包工具)使用方法-icode9专业技术文章分享
- 2024-06-06基于 casdoor 的 ELK 开源登录认证解决方案: elk-auth-casdoor-icode9专业技术文章分享
- 2024-05-29Elasticsearch慢查询日志配置
- 2024-05-29揭秘华为如此多成功项目的产品关键——Charter模板
- 2024-05-29海外IDC业务拓展的7大挑战
- 2024-05-29InLine Chat功能优化对标Github Copilot,CodeGeeX带来更高效、更直观的编程体验!
- 2024-05-29CodeGeeX 智能编程助手 6 项功能升级,在Visual Studio插件市场霸榜2周!
- 2024-05-29AutoMQ 生态集成 Apache Doris
- 2024-05-292024年IDC行业的深度挖掘:机遇、挑战与未来展望
- 2024-05-29五款扩展组件齐发 —— Volcano、Keda、Crane-scheduler 等,邀你体验