C++ 类的存储方式以及虚函数表

摘要:
1、 当C++成员函数通过使用类来定义对象而存储在内存中时,每个对象所占用的存储空间仅是对象的数据部分所占用的空间(虚拟函数指针和虚拟基类指针也属于数据部分)。为运行函数分配的本地变量、函数参数、返回数据、返回地址等存储在堆栈区域中;定义类时,类的静态成员变量已经在全局数据区域中分配了内存。定义类时,静态成员函数和非静态成员函数被放置在内存的代码区域中:

一、C++成员函数在内存中的存储方式

  用类去定义对象时,系统会为每一个对象分配存储空间。如果一个类包括了数据和函数,要分别为数据和函数的代码分配存储空间。按理说,如果用同一个类定义了10个对象,那么就需要分别为10个对象的数据和函数代码分配存储单元,如下图所示。

C++ 类的存储方式以及虚函数表第1张

        能否只用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码。如下图所示。

C++ 类的存储方式以及虚函数表第2张

        显然,这样做会大大节约存储空间。C++编译系统正是这样做的,因此每个对象所占用的存储空间只是该对象的数据部分(虚函数指针和虚基类指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间

  C++程序的内存格局通常分为四个区:全局数据区(data area),代码区(code area),栈区(stack area),堆区(heap area)(即自由存储区)。全局数据区存放全局变量,静态数据和常量;所有类成员函数和非成员函数代码存放在代码区;为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;余下的空间都被称为堆区。根据这个解释,我们可以得知在类的定义时,类成员函数是被放在代码区,而类的静态成员变量在类定义时就已经在全局数据区分配了内存,因而它是属于类的。对于非静态成员变量,我们是在类的实例化过程中(构造对象)才在栈区或者堆区为其分配内存,是为每个对象生成一个拷贝,所以它是属于对象的。

        应当说明,常说的“某某对象的成员函数”,是从逻辑的角度而言的,而成员函数的存储方式,是从物理的角度而言的,二者是不矛盾的。

        下面我们再来讨论下类的静态成员函数和非静态成员函数的区别:静态成员函数和非静态成员函数都是在类的定义时放在内存的代码区的,因而可以说它们都是属于类的,但是类为什么只能直接调用静态类成员函数,而非静态类成员函数(即使函数没有参数)只有类对象才能调用呢?原因是类的非静态类成员函数其实都内含了一个指向类对象的指针型参数(即this指针),因而只有类对象才能调用(此时this指针有实值)

二、虚函数表

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针

举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:

  • 如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。

  • 如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

======================================================================================

转自:http://www.cnblogs.com/malecrab/p/5572730.html

1. 概述

简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。例:

C++ 类的存储方式以及虚函数表第3张

其中:

  • B的虚函数表中存放着B::foo和B::bar两个函数指针。
  • D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz。

提示:为了描述方便,本文在探讨对象内存布局时,将忽略内存对齐对布局的影响。

2. 虚函数表构造过程

从编译器的角度来说,B的虚函数表很好构造,D的虚函数表构造过程相对复杂。下面给出了构造D的虚函数表的一种方式(仅供参考):

C++ 类的存储方式以及虚函数表第4张

提示:该过程是由编译器完成的,因此也可以说:虚函数替换过程发生在编译时。

3. 虚函数调用过程

以下面的程序为例:

C++ 类的存储方式以及虚函数表第5张

编译器只知道pb是B*类型的指针,并不知道它指向的具体对象类型 :pb可能指向的是B的对象,也可能指向的是D的对象。

但对于“pb->bar()”,编译时能够确定的是:此处operator->的另一个参数是B::bar(因为pb是B*类型的,编译器认为bar是B::bar),而B::bar和D::bar在各自虚函数表中的偏移位置是相等的。

无论pb指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应vptr了,就能找出真正应该调用的函数。

提示:本人曾在“C/C++杂记:深入理解数据成员指针、函数成员指针”一文中提到:虚函数指针中的ptr部分为虚函数表中的偏移值(以字节为单位)加1。

B::bar是一个虚函数指针, 它的ptr部分内容为9,它在B的虚函数表中的偏移值为8(8+1=9)。

当程序执行到“pb->bar()”时,已经能够判断pb指向的具体类型了:

  • 如果pb指向B的对象,可以获取到B对象的vptr,加上偏移值8((char*)vptr + 8),可以找到B::bar。
  • 如果pb指向D的对象,可以获取到D对象的vptr,加上偏移值8((char*)vptr + 8) ,可以找到D::bar。
  • 如果pb指向其它类型对象...同理...

4. 多重继承

当一个类继承多个类,且多个基类都有虚函数时,子类对象中将包含多个虚函数表的指针(即多个vptr),例:

C++ 类的存储方式以及虚函数表第6张

其中:D自身的虚函数与B基类共用了同一个虚函数表,因此也称B为D的主基类(primary base class)。

虚函数替换过程与前面描述类似,只是多了一个虚函数表,多了一次拷贝和替换的过程。

虚函数的调用过程,与前面描述基本类似,区别在于基类指针指向的位置可能不是派生类对象的起始位置,以如下面的程序为例:

C++ 类的存储方式以及虚函数表第7张

免责声明:文章转载自《C++ 类的存储方式以及虚函数表》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Firefox浏览器怎么安装adobe flash player插件eMbedded Visual C++ 应用程序迁移到 Visual Studio 2005下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

javascript“数组”的常用方法---以及冒泡排序

首先,数组是数据的集合。 可以通过构造函数(var arr = new Array('0','1','hello'))和字面量(var arr = ['0','1'])的方式来创建, 需要注意的是,构造函数创建时,如果值只有一个,而且还是数字,那么他代表数组的长度(var arr = new Array(5),创建一个长度为5的数组)。 数组的索引是从0开...

js object 常用方法总结

Object.assign(target,source1,source2,...) 该方法主要用于对象的合并,将源对象source的所有可枚举属性合并到目标对象target上,此方法只拷贝源对象的自身属性,不拷贝继承的属性。 Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象...

windows下的shellcode剖析浅谈[转自看雪]

标 题: 【原创】windows下的shellcode剖析浅谈作 者:snowdbg时 间: 2009-10-06,11:12链 接: http://bbs.pediy.com/showthread.php?t=99007  今天是中秋节,正好我的文章在今天基本完成,作为中秋礼物送给大家,由于本人水平有限希望大家多多批评指正!学习了好些日子了,思路总是乱...

Java开发命名规范总结

类命名规范: 类名使用 UpperCamelCase 风格(首字母都大写),必须遵从驼峰形式。 例如: MessageUtil 抽象类命名使用 Abstract 或 Base 开头 ; 异常类命名使用 Exception 结尾 ; 测试类命名以它要测试的类的名称开始,以 Test 结尾。 对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来...

iOS 响应链

首先要明确的是:在IOS中,有响应者链对事件进行响应,所有的响应类都是UIResponder的子类,响应者链是一个由不同对象组成的层次结构,其中的每个对象将依次获得响应事件消息的机会。 响应链的过程: 当事件发生的时候,响应链首先被发送给第一个响应者(往往是事件发生的视图,也就是用户触摸屏幕的地方)。事件将沿着响应者链一直向下传递,知道被接受并作出处理。一...

HyperLedger/Fabric JAVA-SDK with 1.1

HyperLedger/Fabric JAVA-SDK with 1.1 该项目可直接在github上访问。 该项目介绍如何使用fabric-sdk-java框架,基于fabric-sdk-java v1.1正式版开发,可向下兼容1.0版本。 该项目没有对原JAVA-SDK做修改,主要是结合HyperLedger Fabric与fabric-sdk-jav...