Way on c & c++ 小记 [八] – 自底向上地探究虚函数

自底向上地探究虚函数

作者:Jason Lee @http://blog.csdn.net/jasonblog

日期:2010-05-19

环境:Visual C++ Express2008

声明:本文发表在csdn博客,如有转载,请注明出处

 

[1]C++对象模型基础

一个类中可以包含静态数据成员、静态成员函数、非静态成员函数和非静态数据成员以及虚函数。其中,前三者(静态数据成员、静态成员函数、非静态成员函数)都并没有被放到对象的布局中,可以从以下两段代码得到验证:

#include <iostream>
using namespace std;

class Base {
};

int main(){
Base a;
cout << sizeof (a) << endl;// 输出1
return 0;
}

上述的
Base 类是一个空类,占据了一个字节的内存空间,这是为了保证每个类实例化后都拥有独一无二的内存空间。接着我们往
Base 类中添加静态数据成员、静态成员函数和非静态成员函数:

#include <iostream>
using namespace std;

class Base {
public :
Base(){}
~Base(){}
static int v1;
static void f1(){}
void f2(){}
};

int main(){
Base a;
cout << sizeof (a) << endl;// 仍然输出1
return 0;
}

在经过内容填充后,
Base 类的实例
a 仍然仅占据 1
个字节的内存空间,与空类无异,这说明了静态数据成员、静态成员函数和非静态成员函数并未被放在对象的内存布局当中 。

接下来往类中添加非静态数据成员:

#include <iostream>
using namespace std;

class Base {
public :
int a;
int b;
};

int main(){
Base a;
cout << sizeof (a) << endl;// 输出8
cout << hex << &a << endl;// 输出0012F 3CC
cout << hex << &a.a << endl;// 输出0012F 3CC
cout << hex << &a.b << endl;// 输出0012F 3D0
return 0;
}

从上面的代码可以看出:一,非静态数据成员是会被放到对象的内存布局中;二,数据成员是根据声明顺序有序地在内存中进行分布的;三,在没有虚函数的情况下对象所占据的内存大小就是数据成员所占据的空间之和。布局如下图:


a


b

那么如果添加了虚函数以后呢?先看一段代码:

#include <iostream>
using namespace std;

class Base {
public :
int a;
int b;
void doit(){ }
protected :
virtual void vf(){ cout << "hi" << endl; }
virtual void vf_(){}
};

int main(){
Base a;
cout << sizeof (a) << endl;// 输出12
cout << hex << &a << endl;// 输出0012F 3C 8
cout << hex << &a.a << endl;// 输出0012F 3CC
cout << hex << &a.b << endl;// 输出0012F 3D0
return 0;
}

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
字节,布局如下图:


vptr


a


b

 

[2] 继承关系下的模型和指针类型

在单继承的情况下,对象实例的内存布局中,基类部分位于子类部分前;而对于多继承,不同的基类部分会按照继承声明顺序在内存中陆续分布。如下是一个代码示例:

#include <iostream>
using namespace std;

class Base {
public :
void doit(){ vf(); }
int bv;
protected :
virtual void vf(){ cout << "Base" << endl; }
};

class Base1 {
public :
void doit1(){ vf1(); }
int b1v;
protected :
virtual void vf1(){ cout << "Base1" << endl; }
};

class Demo : public Base, public Base1 {
protected :
void vf(){ cout << "Demo" << endl; }
virtual void vf1(){ cout << "Demo1" << endl; }
virtual void vf2(){}
public :
int dv;
};

int main(){
Demo d;
cout << sizeof (d) << endl;// 输出
cout << hex << &d << endl;// 输出F3B8
cout << hex << &d.bv << endl;// 输出F3BC
cout << hex << &d.b1v << endl;// 输出F3C4
cout << hex << &d.dv << endl;// 输出F3C8
Base *p = &d;
p->doit();// 输出Demo ,即执行子类重写的虚函数
cout << hex << p << endl;// 输出F3B8
Base1 *p1 = &d;
cout << hex << p1 << endl;// 输出F3C0
return 0;
}

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 基类的部分,那么为什么运行下述语句会执行子类中的虚函数呢?

Base *p = &d;
p->doit();// 输出Demo ,即执行子类重写的虚函数

首先我们确定指针
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

 

时间: 2024-08-05 02:24:22

Way on c &amp; c++ 小记 [八] – 自底向上地探究虚函数的相关文章

中低成本影片亏损超八成 警惕投资虚热症

无论近几年中国电影市场的表象有多么繁荣,清醒的业界人士都明白个中实情:在这个热闹非凡的"圈子"里,赚钱的只有极少数人,大部分人带着幻想挤进来,最后却输得血本无归. 对于疯狂的热钱"操盘手"而言,如果没有认清中国电影业的实情和规律,就盲目进行投资势必要在电影市场碰壁吃亏. 行业问诊 中低成本影片八成都亏 "很多投资人看到电影业赚钱的一面,却忽略了它也是一个高风险的行业,并不是所有人都能从里面掘到金."北京新影联院线副总高军在接受记者采访时这样说.

PHP第八课 字符串拆分常用函数

课程概要: 通过这节课能够对字符串进行基本的操作. 字符串知识点: 1.字符串的处理介绍 2.常用的字符串输出函数 3.常用的字符串格式化函数 4.字符串比较函数 5.正则表达式在字符串中的应用 6.与per1兼用的正则表达式 1.pathinfo();//返回域名的path信息 2.parse_url(); 3.parse_str();//用来拆分参数用的 pathinfo(); <?php $str="http://blog.csdn.net/junzaivip"; $arr

(八十四)字符函数库cctype

cctype实际上就是一个函数库,他包括多个函数,在调用这些函数的时候,他会自动帮你判断,是否是该函数要求的类型,如果是,返回一个非0 int值(并非固定的)--但可以理解为true值(因为bool后是1),如果不是,返回一个0. 例如:(括号内填变量名,或者用''包含在一起的字符) isalpha()是查证是否是字母,大写字母返回1,小写字母返回2. 下列表格: 函数名 返回值(符合返回非0值) isalpha() 字母 isalnum() 字母或数字 iscntrl() 控制字符(这是什么?

日常小记:C++中的log10函数

今天有同学问我C++中有没有什么简单的办法可以求出两个数相加的和的位数,然后就有了如题的那种方法. log10(100)的返回值就是2,log(999)的返回值是二点几,不过如果你把返回值定位int型它就会自动转换成2了. #include<iostream> #include<cmath> using namespace std; int main(){ int a,b; while(cin>>a>>b){ a=log10(a+b)+1; cout<

[Qt教程] 第48篇 进阶(八) 3D绘图简介

[Qt教程] 第48篇 进阶(八) 3D绘图简介 楼主  发表于 2013-10-7 09:44:37 | 查看: 184| 回复: 0 3D绘图简介 版权声明 该文章原创于作者yafeilinux,转载请注明出处! 导语 OpenGL是一个跨平台的用来渲染3D图形的标准API.在Qt中提供了QtOpenGL模块,从而很轻松地实现了在Qt应用程序中使用OpenGL,这主要是在QGLWidget类中完成的.因为3D绘图涉及到了专业方面的内容,我们下面只是讲解最简单的使用,向大家演示在Qt中如何显示

[Qt教程] 第19篇 2D绘图(九)图形视图框架(上)

[Qt教程] 第19篇 2D绘图(九)图形视图框架(上) 楼主  发表于 2013-5-4 15:26:20 | 查看: 1798| 回复: 26 图形视图框架(上) 版权声明 导语 在前面讲的基本绘图中,我们可以自己绘制各种图形,并且控制它们.但是,如果需要同时绘制很多个相同或不同的图形,并且要控制它们的移动,检测它们的碰撞和叠加:或者我们想让自己绘制的图形可以拖动位置,进行缩放和旋转等操作.实现这些功能,要是还使用以前的方法,那么会十分困难.解决这些问题,可以使用Qt提供的图形视图框架.  

&lt;font color=&quot;red&quot;&gt;[置顶]&lt;/font&gt;

Profile Introduction to Blog 您能看到这篇博客导读是我的荣幸,本博客会持续更新,感谢您的支持,欢迎您的关注与留言.博客有多个专栏,分别是关于 Windows App开发 . UWP(通用Windows平台)开发 . SICP习题解 和 Scheme语言学习 . 算法解析 与 LeetCode等题解 . Android应用开发 ,而最近会添加的文章将主要是算法和Android,不过其它内容也会继续完善. About the Author 独立 Windows App 和

VB.NET是怎样做到的(搬家版)

VB.net能够实现很多C#不能做到的功能,如When语句.Optional参数.局部Static变量.对象实例访问静态方法.Handles绑定事件.On Error处理异常.Object直接后期绑定等等.VB和C#同属.net的语言,编译出来的是同样的CIL,但为什么VB支持很多有趣的特性呢.我们一起来探究一下. (一)局部静态变量 VB支持用Static关键字声明局部变量,这样在过程结束的时候可以保持变量的数值: Public Sub Test1()     Static i As Inte

VC++ 2008开发网络百家乐街机游戏(下)

4.2.3 系统管理功能组 系统管理功能组是后台服务端软件的核心部分,由[场局生成控制].[游戏路单打印].[历史营业记录].[营业利润统计]及[营业日报打印]等几个模块组成.[场局生成控制]负责每场百家乐游戏的场局生成及开局操作:[游戏路单打印]在本场百家乐游戏开局后由游戏管理人员以密闭信封打印出来置于箱中,以便游戏结束后由玩家核对的确保游戏公平:其余三个模块是游戏运营的数据分析,可以根据运营商的需求以各种方式统计出游戏的运行效益. 4.2.3.1 场局生成控制 场局生成控制负责每一场百家乐游