实例说明C++的virtual function的作用以及内部工作机制初探
C++为何要引入virtual function?
来看一个基类的实现:
1 class CBase
2 {
3 public:
4 CBase(int id) : m_nId(id), m_pBaseEx(NULL) {
5 printf(" Base constructor for id=%d\n", id);
6 if (id > 0) {
7 m_pBaseEx = new int[id];
8 for (int idx = 0; idx < m_nId; ++idx) {
9 m_pBaseEx[idx] = idx + 1 + m_nId;
10 }
11 }
12 }
13 //virtual
14 ~CBase() {
15 if (m_pBaseEx)
16 delete []m_pBaseEx;
17 printf(" Base destructor for id=0x%X\n", m_nId);
18 }
19 int getId() {
20 return m_nId;
21 }
22 //virtual
23 void action() {
24 if (m_nId > 0) {
25 for (int idx = 0; idx < m_nId; ++idx) {
26 printf(" %d+%d=%d ", idx + 1, m_nId, m_pBaseEx[idx]);
27 }
28 printf("\n");
29 }
30 }
31 private:
32 int m_nId;
33 int* m_pBaseEx;
34 };
CBase类内部有两个成员变量,m_nId记录一个整数,m_pBaseEx是一个指针,m_nId为正整数时,构造函数会动态申请一块内存以存放m_nId个整数(分别记录1到m_nId与m_nId的和),m_pBaseEx指向这块申请的内存;action方法用于输出一组加法算式。
一个派生类CDerived的实现如下:
1 class CDerived : public CBase
2 {
3 public:
4 CDerived(int id) : CBase(id), m_pEx(NULL)
5 {
6 printf(" Derived constructor for id=%d\n", id);
7 if (id > 0) {
8 m_pEx = new int[id];
9 for (int idx = 0; idx < id; ++idx) {
10 m_pEx[idx] = (idx + 1) * id;
11 }
12 }
13 }
14 ~CDerived() {
15 if (m_pEx)
16 delete []m_pEx;
17 printf(" Derived destructor for id=%d\n", getId());
18 }
19 void action() {
20 int id = getId();
21 if (id > 0) {
22 for (int idx = 0; idx < id; ++idx) {
23 printf(" %d*%d=%d ", idx + 1, id, m_pEx[idx]);
24 }
25 printf("\n");
26 }
27 }
28 private:
29 int* m_pEx;
30 };
CDerived类除了继承自CBase类的两个成员变量(即m_nId和m_pBaseEx),自身还另有一个成员变量int* m_pEx,m_nId为正整数时,CDerived的构造函数会动态申请一块内存以存放m_nId个整数(分别记录1到m_nId与m_nId的积),m_pEx指向这块申请的内存;action方法用于输出一组乘法算式。
main函数实现如下:
1 int main()
2 {
3 printf(" Size of derived obj: %u, size of base obj: %u.\n\n", sizeof(CDerived), sizeof(CBase));
4 while (true) {
5 std::vector<CBase*> vec;
6 for (int idx = 0; idx < 10; ++idx) {
7 CBase* pObj = new CBase(idx);
8 vec.push_back(pObj);
9 }
10 for (int idx = 0; idx < 10; ++idx) {
11 CDerived* pObj = new CDerived(idx);
12 vec.push_back(pObj);
13 }
14 for (size_t idx = 0; idx < vec.size(); ++idx) {
15 vec[idx]->action();
16 }
17 for (size_t idx = 0; idx < vec.size(); ++idx) {
18 delete vec[idx];
19 }
20 int nVal = getchar();
21 if (nVal == 'q') {
22 break;
23 }
24 }
25 return 0;
26 }
生成32位可执行程序运行结果如下:
Size of derived obj: 12, size of base obj: 8. Base constructor for id=0
Base constructor for id=1
......
Base constructor for id=9
Base constructor for id=0
Derived constructor for id=0
Base constructor for id=1
Derived constructor for id=1
......
Base constructor for id=9
Derived constructor for id=9
1+1=2
1+2=3 2+2=4
1+3=4 2+3=5 3+3=6
1+4=5 2+4=6 3+4=7 4+4=8
1+5=6 2+5=7 3+5=8 4+5=9 5+5=10
1+6=7 2+6=8 3+6=9 4+6=10 5+6=11 6+6=12
1+7=8 2+7=9 3+7=10 4+7=11 5+7=12 6+7=13 7+7=14
1+8=9 2+8=10 3+8=11 4+8=12 5+8=13 6+8=14 7+8=15 8+8=16
1+9=10 2+9=11 3+9=12 4+9=13 5+9=14 6+9=15 7+9=16 8+9=17 9+9=18
1+1=2
1+2=3 2+2=4
1+3=4 2+3=5 3+3=6
1+4=5 2+4=6 3+4=7 4+4=8
1+5=6 2+5=7 3+5=8 4+5=9 5+5=10
1+6=7 2+6=8 3+6=9 4+6=10 5+6=11 6+6=12
1+7=8 2+7=9 3+7=10 4+7=11 5+7=12 6+7=13 7+7=14
1+8=9 2+8=10 3+8=11 4+8=12 5+8=13 6+8=14 7+8=15 8+8=16
1+9=10 2+9=11 3+9=12 4+9=13 5+9=14 6+9=15 7+9=16 8+9=17 9+9=18
Base destructor for id=0x0
Base destructor for id=0x1
......
Base destructor for id=0x9
Base destructor for id=0x0
Base destructor for id=0x1
......
Base destructor for id=0x9
从上面的结果看,指向CDerived类对象的指针被加入std::vector<CBase*>后,通过vector访问这些指针指向的对象时,这些对象就完全被当作CBase类对象在使用了:
vec[idx]->action()执行的是CBase类的action方法,导致输出结果里加法表被输出了两篇;
delete vec[idx]也只是释放了CBase类对象所占用的动态申请的内存,导致内存泄漏的问题。
对于上面的程序,我们可以把 std::vector<CBase*> vec 分拆成两个:std::vector<CBase*> vec1 和 std::vector<CDerived*> vec2,10个CBase类对象指针加到vec1里,而10个CDerived类对象指针加到vec2里。这样的确可以解决问题,但是程序语言上这种限制(或曰缺陷)会导致编程上额外代码的开销,尤其是有很多派生类以及多级派生的情形,为了应对语言上的缺陷,额外的代码会显得臃肿而笨拙,这也有违面向对象编程的初衷。
C++解决上述缺陷的办法就是引入virtual function。以上面的示例为例,只需要把CBase类的构造函数和action方法加上virtual声明(即去掉CBase类代码段里第13行和第22行里的“//”),重新生成可执行程序,运行后得到输出结果如下:
Size of derived obj: 16, size of base obj: 12. Base constructor for id=0
Base constructor for id=1
......
Base constructor for id=9
Base constructor for id=0
Derived constructor for id=0
Base constructor for id=1
Derived constructor for id=1
......
Base constructor for id=9
Derived constructor for id=9
1+1=2
1+2=3 2+2=4
1+3=4 2+3=5 3+3=6
1+4=5 2+4=6 3+4=7 4+4=8
1+5=6 2+5=7 3+5=8 4+5=9 5+5=10
1+6=7 2+6=8 3+6=9 4+6=10 5+6=11 6+6=12
1+7=8 2+7=9 3+7=10 4+7=11 5+7=12 6+7=13 7+7=14
1+8=9 2+8=10 3+8=11 4+8=12 5+8=13 6+8=14 7+8=15 8+8=16
1+9=10 2+9=11 3+9=12 4+9=13 5+9=14 6+9=15 7+9=16 8+9=17 9+9=18
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81
Base destructor for id=0x0
Base destructor for id=0x1
......
Base destructor for id=0x9
Derived destructor for id=0
Base destructor for id=0x0
Derived destructor for id=1
Base destructor for id=0x1
......
Derived destructor for id=9
Base destructor for id=0x9
这个输出结果如期输出了加法表和乘法表,派生类对象也如期得到释放。
virtual function内部机制初探
那么,virtual function是怎么起作用的呢?具体来说,在上面的示例中, 通过std::vector<CBase*>对象vec内的一个单元指针调用action方法,具体是怎么判别该调用哪一个类的action方法的呢?
对比上面的两个输出结果的开头部分可以看到,CBase类的析构函数和action方法加了virtual声明后,CBase类的sizeof值由8变成了12,CDerived类的sizeof值也由12变成了16。CBase类里定义了两个成员变量,一个int型,一个int*型,在32位程序里,这两个分量各占4个字节,sizeof值等于8是符合预期的,加了virtual声明之后,内部成员发生了什么变化?利用VS2010调试器来跟踪看一下。
对照上图可以看到,加了virtual声明之后,C++的编译器给CBase类增加了一个叫做__vfptr的成员变量,virtual function pointer,虚拟函数指针,正是这个指针变量使得CBase的sizeof值增加了4个字节,增加了这个隐藏的成员变量之后,类的构造函数会相应地增加对这个变量进行赋值的操作;从图中可以看出这个__vfptr指向一个叫做 CBase::`vftable' 的实体,其内部数据如下:
CBase::`vftable' ,CBase的virtual function table(虚拟函数表),内部是一组指针:第一个指针指向CBase::`vector deleting destructor'(unsigned int),这个应该就是对应CBase的析构函数~CBase(),原来它内部表示是这样的,而且还带了一个unsigned int参数,不知道具体指代什么,后面还会看到派生类CDerived的析构函数内部表示是CDerived::`scalar deleting destructor(unsigned int);第二个指针指向CBase::action(void);随后是一个NULL指针,标示vftable的完结。
把上面示例代码中CBase类里两个virtual函数顺序反一下,即把析构函数放到最后,重新生成程序调试运行,发现CBase的虚拟函数表里的指针顺序相应也会调整,如下图所示:
进一步跟踪查看,可以发现10个CBase对象都有各自的__vfptr成员变量,但这些变量都指向同一张虚拟函数表,另外,从虚拟函数表的内容我们也能确认一个类里的函数/方法在内存里只有一个实例(实质上会对应成C语言的函数实例形式,内部会有一些转换处理,比如增加一个对应类的对象指针的参数)。
再来看CDerived类对象的情形:
从这个图看到CDerived类里没有自身的__vfptr成员变量,但它从CBase类里继承了__vfptr成员变量,如下图所示:
可以看到,CDerived类对象的__vfptr指向的是CDerived类的虚拟函数表,其内容如下图所示):
这就可以解释上面那个疑问了:通过std::vector<CBase*>对象vec内的一个单元指针调用action方法,怎么判别实际应该调用哪一个类的action方法。实例化一个CDerived类对象时,该类的构造函数会让__vfptr成员指向CDerived类的虚拟函数表,该函数表指明了action方法和析构函数的具体例程,这样,即便把CDerived类对象指针放进std::vector<CBase*>里,后续从vector里统一当成CBase*指针来调用action方法时,都会从其成员变量__vfptr所指向的虚拟函数表里查找匹配的调用实例。
从std::vector<CBase*>里看的效果如下图所示:
进而可以推断,派生类和基类的虚拟函数表是可以不一样大的,前者可以大于后者。实际验证一下,在CDerived类内部增加一个virtual函数,如下:
virtual void test() {
printf(" In derived.\n");
}
并增加一个派生于CDerived类的新类CDerived2:
1 class CDerived2 : public CDerived
2 {
3 public:
4 CDerived2(int id) : CDerived(id) {}
5 void test() {
6 printf(" In derived2.\n");
7 }
8 };
相应地把main函数实现改为:
1 int main()
2 {
3 printf(" Size of derived2 obj: %u, size of derived obj: %u, size of base obj: %u.\n\n", sizeof(CDerived2), sizeof(CDerived), sizeof(CBase));
4 while (true) {
5 std::vector<CBase*> vec;
6 std::vector<CDerived*> vec2;
7 for (int idx = 0; idx < 10; ++idx) {
8 CBase* pObj = new CBase(idx);
9 vec.push_back(pObj);
10 }
11 for (int idx = 0; idx < 10; ++idx) {
12 CDerived* pObj = new CDerived(idx);
13 vec.push_back(pObj);
14 vec2.push_back(pObj);
15 }
16 CDerived2* pObj = new CDerived2(11);
17 vec2.push_back(pObj);
18 for (size_t idx = 0; idx < vec.size(); ++idx) {
19 vec[idx]->action();
20 }
21 for (size_t idx = 0; idx < vec2.size(); ++idx) {
22 vec2[idx]->test();
23 }
24 for (size_t idx = 0; idx < vec.size(); ++idx) {
25 delete vec[idx];
26 }
27 delete vec2[vec2.size() - 1];
28
29 int nVal = getchar();
30 if (nVal == 'q') {
31 break;
32 }
33 }
34 return 0;
35 }
重新生成程序运行,得到如下输出结果:
Size of derived2 obj: 16, size of derived obj: 16, size of base obj: 12. Base constructor for id=0 ...... Base constructor for id=9
Base constructor for id=0
Derived constructor for id=0
......Base constructor for id=9
Derived constructor for id=9
Base constructor for id=11
Derived constructor for id=11
1+1=2
......
1+9=10 2+9=11 3+9=12 4+9=13 5+9=14 6+9=15 7+9=16 8+9=17 9+9=18
1*1=1
......
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81
In derived.
......In derived.
In derived2.
Base destructor for id=0x0
......
Base destructor for id=0x9
Derived destructor for id=0
Base destructor for id=0x0
......Derived destructor for id=9
Base destructor for id=0x9
Derived destructor for id=11
Base destructor for id=0xB
这个结果是符合预期的。进一步调试运行,依次来看这三个类的虚拟函数表:
这是vec里的一个指针指向的CBase类对象,其__vfptr成员指向CBase类的虚拟函数表,该函数表存有两个函数指针,符合预期。
只是不知为何,这里CBase类的析构函数的内部表示名却是CBase::`scalar deleting destructor'(unsigned int),而不是此前看到的CBase::`vector deleting destructor'(unsigned int)。
这是vec2里的一个指针指向的CDerived类对象,其__vfptr成员指向CDerived类的虚拟函数表,该函数表从图中__vfptr变量的展开视图只能看到两个函数指针,但从__vfptr变量指向的虚拟函数表的内存地址看确实是有三个函数指针,这样才是符合预期的。红框内的函数指针没有展现出来,这应该是VS2010调试器的一个bug。
这是vec2里最后那个指针指向的CDerived2类对象,其__vfptr成员指向CDerived2类的虚拟函数表,该函数表也有三个函数指针,符合预期。不过界面显示上也有上面发现的问题。
对照CDerived类和CDerived2类的虚拟函数表可以发现:
(1)CDerived2类的action()的例程地址和CDerived类的action()的例程地址是同一个,这是因为CDerived2类没有定义自己的action方法,所以就直接继承了直接上级类的action方法。
(2)CDerived2类也没有显式定义自己的析构函数,但是CDerived2类的析构函数的例程地址和CDerived类的析构函数的例程地址却不相同,这是因为C++编译器会为缺少显式析构函数的代码补充上缺省析构函数。
(3)CDerived2类有自己的test方法实现,而且test方法在CDerived类里被声明为virtual,所以CDerived2类的test方法的例程地址和CDerived类的test方法的例程地址应该不相同。
上面的验证也证实了前面的推测,即派生类的虚拟函数表的条目数是可以大于基类的虚拟函数表的条目数的。
由上面的探讨过程,很容易理解:对于一个带有virtual function的基类,强行把该基类的析构函数加上virtual声明是个很好的习惯,从而避免由此引起的内存泄漏问题。或许以后的C++会自动做到这一点。
最后再梳理一下虚拟函数表。前述的虚拟函数表就是一个函数指针数组,该数组以一个NULL指针结尾。光是这这个数组结构还不足于由基类对象指针和一个方法名就能匹配到数组里的函数指针的。虚拟函数表应该还有一块内存结构,用于由类名和方法名去匹配该类中虚函数的序号;有了这个序号就可以到函数指针数组里匹配到具体的函数指针了。
实例说明C++的virtual function的作用以及内部工作机制初探的更多相关文章
- 结合实例详解"pure Virtual function called"
作者:阿波 链接:http://blog.csdn.net/livelylittlefish/article/details/9750593 (4年前的一篇文章,翻出来共享一下.) 本实例即为经典的讲 ...
- C# abstract function VS virtual function?
An abstract function has to be overridden while a virtual function may be overridden. Virtual functi ...
- OD: Memory Attach Technology - Off by One, Virtual Function in C++ & Heap Spray
Off by One 根据 Halvar Flake 在“Third Generation Exploitation”中的描述,漏洞利用技术依攻击难度从小到大分为三类: . 基础的栈溢出利用,可以利用 ...
- why pure virtual function has definition 为什么可以在基类中实现纯虚函数
看了会音频,无意搜到一个frameworks/base/include/utils/Flattenable.h : virtual ~Flattenable() = 0; 所以查了下“纯虚函数定义实现 ...
- pure virtual function call
2015-04-08 10:58:19 基类中定义了纯虚函数,派生类中将其实现. 如果在基类的构造函数或者析构函数中调用了改纯虚函数, 则会出现R6205 Error: pure virtual fu ...
- Mindjet MindManager 2012 从模板创建出现“Runtime Error pure virtual function call” 解决方法
我的Mindjet MindManager 2012 Pro也就是MindManager10 在应用模板之后总会显示 Microsoft Visual C++ Runtime Library Runt ...
- OD: Windows Security Techniques & GS Bypassing via C++ Virtual Function
Windows 安全机制 漏洞的万源之本在于冯诺依曼设计的计算机模型没有将代码和数据进行区分——病毒.加壳脱壳.shellcode.跨站脚本攻击.SQL注入等都是因为计算机把数据和代码混淆这一天然缺陷 ...
- (转) Virtual function
原文地址:http://en.wikipedia.org/wiki/Virtual_function In object-oriented programming, a virtual functio ...
- [c++] polymorphism without virtual function
polymorphism without virtual function
随机推荐
- SpringBoot之yaml语法及静态资源访问
配置文件-yaml 在spring Boot开发中推荐使用yaml来作为配置文件. 基本语法: key: value:kv之间有空格 大小写敏感 使用缩进表示层级关系 缩进不允许使用tab,只允许空格 ...
- odoo学习笔记create函数
@api.multi def create_order_sale(self): """""" stage_list = [] for ord ...
- 以两种异步模型应用案例,深度解析Future接口
摘要:本文以实际案例的形式分析了两种异步模型,并从源码角度深度解析Future接口和FutureTask类. 本文分享自华为云社区<[精通高并发系列]两种异步模型与深度解析Future接口(一) ...
- Couchdb 任意命令执行漏洞(CVE-2017-12636)
影响版本:小于 1.7.0 以及 小于 2.1.1 该漏洞是需要登录用户方可触发,如果不知道目标管理员密码,可以利用CVE-2017-12635先增加一个管理员用户 依次执行如下请求即可触发任意命令执 ...
- Android技术分享| 实现视频连麦直播
视频连麦产品端核心步骤分析 游客申请连麦/取消申请 主播同意/拒绝申请 音视频发布取消 支持很多观众观看 支持多人连麦 低延时 IM 弹幕 视频连麦技术端调研 emmm,大致可以分为视频采集.编码,传 ...
- MySQL隔离级别的实现
虽然平时已经很少使用MySQL了,但是数据库作为基本技能仍然不能忘,最近在学习数据库隔离级别,在此写下个人理解以备复习. 大家都知道数据库事务ACID(原子性.一致性.隔离性和持久性)的四个特征,也知 ...
- 巧用map解决nginx的Location里if失效问题
需求: Nginx根据参数来输出不同的header 我们想用Nginx来判断一些通用的参数, 根据参数情况在输出中不同的header, 或者cookie, 那么根据正常思路, 有如下配置: locat ...
- Adaptive AUTOSAR 学习笔记 12 - 通信管理
本系列学习笔记基于 AUTOSAR Adaptive Platform 官方文档 R20-11 版本 AUTOSAR_EXP_PlatformDesign.pdf 缩写 CM:Communicatio ...
- [考试总结]noip模拟33
连炸两场... 伤心... 第一个题目首先因为有期望坐镇,然后跳过... 然后第二个题目发现题目挺绕的,然后转化了一句话题意,然后..... \(\huge{\text{转化错了!!!!}}\) 然而 ...
- Spring WebFlux 基础教程:WebSocket 使用
WebSocket 协议简介 WebSocket 协议提供了一种标准化的方式,在客户端和服务端建立在一个TCP 连接之上的全双工,双向通信的协议. WebSocket 交互开始于 HTTP 请求,使用 ...