自底向上地探究虚函数
作者:Jason Lee @http://blog.csdn.net/jasonblog
日期:2010-05-19
环境:Visual C++ Express2008
声明:本文发表在csdn博客,如有转载,请注明出处
[1]C++对象模型基础
一个类中可以包含静态数据成员、静态成员函数、非静态成员函数和非静态数据成员以及虚函数。其中,前三者(静态数据成员、静态成员函数、非静态成员函数)都并没有被放到对象的布局中,可以从以下两段代码得到验证:
上述的
Base 类是一个空类,占据了一个字节的内存空间,这是为了保证每个类实例化后都拥有独一无二的内存空间。接着我们往
Base 类中添加静态数据成员、静态成员函数和非静态成员函数:
在经过内容填充后,
Base 类的实例
a 仍然仅占据 1
个字节的内存空间,与空类无异,这说明了静态数据成员、静态成员函数和非静态成员函数并未被放在对象的内存布局当中 。
接下来往类中添加非静态数据成员:
从上面的代码可以看出:一,非静态数据成员是会被放到对象的内存布局中;二,数据成员是根据声明顺序有序地在内存中进行分布的;三,在没有虚函数的情况下对象所占据的内存大小就是数据成员所占据的空间之和。布局如下图:
那么如果添加了虚函数以后呢?先看一段代码:
Base
基类中有两个虚函数,这两个虚函数(如果只有一个虚函数情况也一样)出现后使得对象 a
的大小变为 12 ,相较于没有虚函数的情况多了
4 个字节,即
32 位,相当于一个指针所占的内存大小。
包含虚函数的类的对象实例中会在内存布局中多添加了一个
vptr
指针,这个指针指向(不仅)存放虚函数地址的虚表 vtbl
,所以不管类中只有一个虚函数或者有多个,在对象实例中只会多出一个指针需要的空间大小,即 4
个字节。
此外,
vptr
通常存放在对象内存布局中的起始处 。从上一段代码输出的地址就可以看出成员变量
a 占据
0012F 3CC 到
0012F 3CF 的空间,成员变量
b 占据
0012F 3D0 到
0012F 3D3 的空间,而对象首址
0012F 3C 8
到 0012F
3CB 则是用来存放 vptr
的。总计 12
字节,布局如下图:
[2] 继承关系下的模型和指针类型
在单继承的情况下,对象实例的内存布局中,基类部分位于子类部分前;而对于多继承,不同的基类部分会按照继承声明顺序在内存中陆续分布。如下是一个代码示例:
Base
基类因为有一个 vptr 和一个数据成员
bv ,所以占据了
8 个字节; Base1
基类同样有 vptr
和数据成员 b1v ,所以也占据了
8 个字节;而子类
Demo 继承了 Base
和 Base1
,又新增了一个数据成员 dv ,所以总共占据了
20 个字节的空间。
对象
d 占据了从
0012F 3B8 到
0012F 3CB 的
20 个字节内存空间,其中 Base
基类的部分位于 0012F
3B8 到 0012F
3BF 的 8
个字节, Base1 基类部分紧随其后,占据
0012F 3C 0
到 0012F
3C 7 的内存空间,最后是子类本身的数据成员
dv 。布局如下图所示:
vptr_Base
|
bv
|
vptr_Base1
|
b1v
|
dv
|
注意到指针
p 和
p1 的声明和赋值,以及所指向的首址。指针
p 的类型是 Base *
,它指向了对象 d
中的 Base 基类部分;指针
p1 的类型是
Base1 * ,它指向了对象 d
中的 Base1
部分。指针类型的作用是给予编译器信息,表明指针指向的对象类型 (包含首址以及大小等信息),因为子类中含有基类部分,所以基类指针可以指向子类,更实质地讲是指向子类中的基类部分。
既然
Base * 类型的指针指向的是内存中
Base 基类的部分,那么为什么运行下述语句会执行子类中的虚函数呢?
首先我们确定指针
p 能访问的只有
vptr_Base 和 bv
这两个成员,其中 vptr_Base
指向一个虚表,虚表中存放着类型信息和虚函数的地址。这里不妨认为 vptr_Base[1]
存放着虚函数 vf 的地址。
在编译阶段可以针对虚函数机制做的工作有:一,确定虚表的地址,即
vptr 的指向;二,确定虚表的大小和内容;三,针对不同虚函数的调用,转换为对虚表不同表项的索引。在确定虚表的内容时,如果子类重写了基类的虚函数,那么虚表中对应的表项会被修改指向子类重新实现的函数地址;否则的话,仍旧指向基类中定义的虚函数。
在上述代码中,由于
Demo 类中重写了虚函数
vf ,所以 vptr_Base[1]
指向了子类中的虚函数实体,而非 Base
中定义的 vf
。
经过编译后,我们知道对
vf 函数的调用相当于调用
vptr_Base[1] 所指向的函数,但是在这个阶段无法知道调用的虚函数到底是基类定义的还是子类中重写的,因为
Base *
类型的指针可以指向一个 Base
类型对象,也可以指向子类 Demo
中的 Base
基类部分 。因此,只有在执行期才可以知道
vptr_Base[1] 到底指向哪一个
vf 函数实体。
[3] 编码层次的虚函数
如果基类希望某个成员函数由子类重定义,那么应该将其声明为
virtual 类型。虚函数是动态绑定的基础,(只有)通过基类类型的指针或引用进行虚函数的调用才可以触发动态绑定,这体现了多态这一面向对象的关键思想。可以看出,基类类型的指针或引用既可以指向基类类型对象也可以指向子类类型对象的特性是动态绑定的关键 。
通过虚函数可以实现运行时多态,这有利于公共接口的实现。即在继承体系中处于不同层次的类使用同一接口,但运行时会根据具体对象类型的差异而采取不同的策略。这种风格在良好的软件体系结构可以经常发现,比如
Qt 。
在实际编码设计过程中需要注意以下几点:
1、
要发生动态绑定,实现运行时的多态,需要通过基类的指针或者引用调用虚函数,这样在运行时才会根据指针或引用所指向的对象的实际类型调用相应的目标函数。
2、
关键字 virtual
只能在类内部的成员声明中出现,而不能出现在类定义体外部。
3、
一经声明为虚函数,则一直是虚函数;子类可以不用显示声明
virtual ,但虚函数的特性不会改变。
4、
子类中虚函数的声明必须和基类的定义方式匹配,除了基类中虚函数返回对基类类型的指针或引用,在这种情况下,子类中的虚函数可以返回基类函数所返回类型的子类的指针或引用。
5、
子类中虚函数调用基类的虚函数必须使用显示作用域声明,或者会递归调用自身。
6、
纯虚函数仅作为抽象接口以供覆盖,包含纯虚函数的类是抽象基类,不能被实例化。
[4] 参考资料
《
C++ Primer 》
《
Inside The C++ Object Model
》
分享到:
相关推荐
books, this title concentrates on modern, best-practice techniques from the very beginning, which means you’ll get it right the first time. The web sites you’ll build will: Look good on a PC, Mac ...
WAYON维安一级代理分销KOYUELEC光与电子WAYON OVP IC INTRODUCTION过压保护方案与应用 WAYON维安一级代理分销KOYUELEC光与电子WAYON OVP IC INTRODUCTION过压保护方案与应用主要介绍了WAYON公司的过压保护方案和...
If you don’t have a lot of time, but still want to learn the latest in C++, you don’t have to learn C first. You might learn more by digging into current language features and classes from the very ...
Like any human language, C++ provides a way to express concepts. If successful, this medium of expression is significantly easier and more flexible than the alternatives as problems grow larger and ...
为了使用和理解这些源代码,你需要具备C/C++编程基础,了解基本的数据类型、控制结构、函数调用以及如何处理文件输入/输出。对于3路算法的具体实现,你需要理解位操作(如位移和异或)以及如何通过这些操作来执行...
《Learn C the hard way》是一本关于C语言编程的学习书籍,由Zed A. Shaw编写。这本书采用了一种非传统的教学方式,即通过让学习者实际编写和运行代码的方式来教授C语言,这种方式被作者称为“硬学习法”。书中不仅...
Managed VCL is the best components suite to work with .Net framework from Delphi and C++ Builder - feature named .Net interop. It is designed to provide a way to interact with applications written in ...
books, this title concentrates on modern, best-practice techniques from the very beginning, which means you’ll get it right the first time. The web sites you’ll build will: Look good on a PC, Mac ...
在C++编程语言中,交换函数(通常称为swap函数)是一种常见的操作,它用于在两个变量之间交换值。这个过程不涉及任何中间变量,而是直接完成两个变量的值的对调。交换函数对于数组排序、数据结构操作以及算法实现等...
In a very similar way, C++/CLI is layered on top of C++. C++/CLI provides a high degree of source code compatibility with C++. As a consequence, the following code is valid if you build the program ...
江苏省铜山区清华中学七年级英语下册 Unit 4 Finding your way Comic Strip&Welcome to the Unit学案(无答案)(新版)牛津版
Templates: The Hard Way Templates: The C++ Way Function Specialization Class Templates Class Specialization Implementation Details Advanced Features Summary Programming Exercises 25. Standard ...
The C++17 release includes changes that impact the way you work with C++; this new fourth edition covers them all, including nested namespaces, structured bindings, string_view, template argument ...
Exploring C++ divides C++ up into bite-sized chunks that will help you learn the language one step at a time. Assuming no familiarity with C++, or any other C-based language, you’ll be taught ...
Series: A Smarter Way to Learn Paperback: 258 pages Publisher: CreateSpace Independent Publishing Platform; 1 edition (March 13, 2015) Language: English ISBN-10: 150867387X ISBN-13: 978-1508673873 ...
I learned C++11 the hard way. Because I didn’t follow the standardization as it was happening I started to look at C++11 about two years ago. I really had trouble understanding it. But the people on ...
With this book by your side, you are well on your way to writing applications in both versions of C++ and becoming a successful C++ programmer. Ivor Horton's Beginning Visual C++ 2010: Teaches the ...