本文参考文献:GeekBand课堂内容,授课老师:侯捷

:深度探索C++对象模型(侯捷译)

:网络资料,如:http://blog.csdn.net/sanfengshou/article/details/4574604

说明:由于条件限制,仅测试了Windows平台下的VS2013 IDE。其余平台结果可能不同,但原理都类似。建议读者自己在其他平台进行测试。

1、什么是虚函数?

虚函数是类的非静态成员函数,在类中的基本形式如下:virtual 函数返回值类型 虚函数名(形参表)

如:virtual void process()

2、虚函数的作用,为什么采用虚函数?

虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义(形式也是:virtual 函数返回值类型 虚函数名(形参表){ 函数体 }),在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。

3、正文

  首先,假设有一个Fruit类,如下所示:

//基类
class Fruit
{
public:
//构造函数
Fruit(const int no_,const double weight_,const char key_) :no(no_), weight(weight_), key(key_){};
//打印变量内存地址
void print()
{
cout << "no :" << no << " | " << " Memory Address no: " << (void*)&no << endl;
cout << "weight :" << weight << " | " << " Memory Address weight: " << (void*)&weight << endl;
cout << "key :" << key << " | " << " Memory Address key: " << (void*)&key << endl;
}
//虚函数的影响
virtual void process()
{
cout << "the Process function of Base Fruit is called!" << endl;
}
private:
int no;
double weight;
char key;
};

我们知道,int类型为4个字节,double类型为8个字节,char类型为1个字节,又知道Class中存在着字节对齐的说法。这里有流传比较广的三原则:

1、偏移地址和成员占用大小均需要对齐;

2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)

3、结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐.

由此可以推断出,此时数据段大小应为24字节

如果数据更换位置了呢?比如,感兴趣的朋友自己验证下:

private:
char key;
int no;
double weight;

最后,相信大家都知道 #pragma pack(),这个函数。这里面又有着效率等问题,以后再详细的分析它,由于与此标题无关,就不展开说了。

测试代码如下所示:

#include "stdafx.h"
#include"Object.h"
#include"iostream"
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
//基类(水果):测试数据
cout << "------------------------------------------------------------------" << endl;
cout << "The Memory Test Data of Fruit Class !"<< endl;
cout << "------------------------------------------------------------------" << endl;
Fruit TestFruit(,0.0,'b');
cout << "Fruit Class Size : " << sizeof(Fruit) << endl; //类的大小
cout << "Fruit Memory Layout FirstAddress: " << (void*)&TestFruit << endl; //类的首地址
//虚函数表的地址存在在前四个字节中
cout << "Fruit Virtual Function Table Address:" << (int*)(&TestFruit) << endl; //虚函数表地址
//打印类的信息
TestFruit.print(); //类中成员的地址信息
cout << "Fruit Virtual process Function Address: " << (int*)*(int*)&TestFruit + << endl; //虚函数表中函数process函数首地址
// 通过函数指针调用函数,验证正确性
typedef void(*func_pointer)(void);
func_pointer fptr = NULL;
fptr = (func_pointer)*((int*)*(int*)&TestFruit + ); // v_func1()
fptr(); //process
}

结果如下所示:

根据显示结果,所以我们可以大致的画出内存分布图:

其中

1、Fruit类为类,大小为 sizeof(Fruit) = 32,

2、vptr 为虚指针,指向了虚函数表

3、int类型为4字节,为了对齐,所以填充了4个字节。同理,char填充了7个字节。

如果派生类和基类有共同的虚函数时内存如何分布呢?

子类代码如下,注意此时子类和基类都有virtual void process()函数


#ifndef  _OBJECT_H_
#define _OBJECT_H_
#include"iostream"
using namespace std;
//基类
class Fruit
{
public:
//构造函数
Fruit(const int no_,const double weight_,const char key_) :no(no_), weight(weight_), key(key_){};
//打印变量内存地址
void print()
{
cout << "no :" << no << " | " << " Memory Address no: " << (void*)&no << endl;
cout << "weight :" << weight << " | " << " Memory Address weight: " << (void*)&weight << endl;
cout << "key :" << key << " | " << " Memory Address key: " << (void*)&key << endl;
}
//虚函数的影响
virtual void process()
{
cout << "the Process function of Base Fruit is called!" << endl;
}
private:
int no;
double weight;
char key;
}; //派生类
//这里考虑自己本身的虚函数,及基类的虚函数
class Apple : public Fruit
{
public:
//构造函数
Apple(const Fruit& fruit_,const int size_,const char type_) :size(size_), type(type_),Fruit(fruit_){};
//打印成员数据
void save()
{
cout << "size :" << size << " | " << " Apple Memory Address no: " << (void*)&size << endl;
cout << "type :" << type << " | " << " Apple Memory Address weight: " << (void*)&type << endl;
}
virtual void process()
{
cout << "the Process function of Derived Apple is called!" << endl;
}
private:
int size;
char type;
}; #endif

完整测试代码如下:

// TestObjectSize.cpp : 定义控制台应用程序的入口点。
// #include "stdafx.h"
#include"Object.h"
#include"iostream"
using namespace std; int _tmain(int argc, _TCHAR* argv[])
{
//基类(水果):测试数据
cout << "------------------------------------------------------------------" << endl;
cout << "The Memory Test Data of Fruit Class !"<< endl;
cout << "------------------------------------------------------------------" << endl;
Fruit TestFruit(,0.0,'b');
cout << "Fruit Class Size : " << sizeof(Fruit) << endl; //类的大小
cout << "Fruit Memory Layout FirstAddress: " << (void*)&TestFruit << endl; //类的首地址
//虚函数表的地址存在在前四个字节中
cout << "Fruit Virtual Function Table Address:" << (int*)(&TestFruit) << endl; //虚函数表地址
//打印类的信息
TestFruit.print(); //类中成员的地址信息
cout << "Fruit Virtual process Function Address: " << (int*)*(int*)&TestFruit + << endl; //虚函数表中函数process函数首地址
// 通过函数指针调用函数,验证正确性
typedef void(*func_pointer)(void);
func_pointer fptr = NULL;
fptr = (func_pointer)*((int*)*(int*)&TestFruit + ); // v_func1()
fptr(); //process //派生类(苹果):测试数据
cout << "------------------------------------------------------------------" << endl;
cout << "The Memory Test Data of Apple Class !" << endl;
cout << "------------------------------------------------------------------" << endl;
Apple TestApple(TestFruit, , 't');
cout << "Apple Class Size : " << sizeof(Apple) << endl; //类的大小
cout << "Apple Memory Layout FirstAddress: " << (void*)&TestApple << endl; //类的首地址
//虚函数表的地址存在在前四个字节中
cout << "Apple Virtual Function Table Address:" << (int*)(&TestApple) << endl; //虚函数表地址
//查看基类Fruit类的信息
TestApple.print();
//打印Apple类的信息
TestApple.save(); //类中成员的地址信息
cout << "Apple Virtual process Function Address: " << (int*)*(int*)&TestApple + << endl; //虚函数表中函数process函数首地址
// 通过函数指针调用函数,验证正确性
typedef void(*func_pointer2)(void);
func_pointer fptr2 = NULL;
fptr2 = (func_pointer2)*((int*)*(int*)&TestApple + ); // v_func1()
fptr2(); //process system("pause");
return ;
}

我们先看下实际结果:

通过结果,我们可以画出此时子类的内存分布图。

根据显示结果,我们可以进一步分析!

其中

1、Apple类为派生类,大小为 sizeof(Apple) = 40,

2、vptr 为虚指针,指向了虚函数表.同时,先对基类进行了内存分配,然后再对子类进行内存分配。

3、派生类中int类型为4字节,char 为1字节。为了对齐,char 类型填充了3个字节、

根据以上分析:整体框架如图:

4.1、进一步的思考

1、 通过以上分析,我们基本上知道了系统如何进行内存分布的,我们这里对虚函数表内存进一步的观察与思考:

打印出来的信息 : Fruit Virtual process Function Address : 001AEC78

  Apple Virtual process Function Address :001AEDB0

                        观察得到,从内存角度出发,基类的地址(001AEC78)要小于子类(001AEDB0),这说明基类的虚函数在子类的前面。

2、分别在Fruit类、Apple类中添加虚函数:

    //测试用,非本题范围
virtual void process_b0()
{
cout << "This is Base Fruit class's process_b0" << endl;
}

及Apple类中添加虚函数

    //测试用,非本题范围
virtual void process_b1()
{
cout << "This is Derived Apple_class's process_b1" << endl;
}

3、然后修改测试代码段,将检验一次,改为三次

    // 通过函数指针调用函数,验证正确性
//typedef void(*func_pointer2)(void);
//func_pointer fptr2 = NULL;
//fptr2 = (func_pointer2)*((int*)*(int*)&TestApple + 0); // v_func1()
//fptr2(); //process // 通过函数指针调用函数,验证调用问题
typedef void(*func_pointer)(void);
func_pointer fp = NULL;
for (int i = ; i<; i++) {
fp = (func_pointer)*((int*)*(int*)&TestApple + i);
fp();

4、运行后结果如图所示:

通过结果进一步的思考与分析:

1、显示:The Progress function of Deviced Apple is called !同名的process,运行的是子类的process()。说明此时用子类的虚函数process()代替了父类的虚函数process()!

2、先显示:this is Base Fruit class proccess_b0 ,后显示:This is Deviced Apple class process_b1.进一步说明了基类的虚函数内存分布在子类的之前!

基本关系如图所示:

4.2、调试技巧

在VS2013 DE中有很多方便的工具,可以清晰的观察出变量等各种信息,如图通过debug模式下即可查看出很多关键信息!

2、通过反汇编观察,这点还不是很熟,不过以后要加强调试经验。

以上就是我个人的一些不成熟的学习笔记,望各位批评指正。谢

[GeekBand] C++继承关系下虚函数内存分布的更多相关文章

  1. C++类虚函数内存分布(这个 你必须懂)

    转自:http://www.cnblogs.com/jerry19880126/p/3616999.html C++类内存分布 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来 ...

  2. 谈谈c++中继承中的虚函数

      c++继承中的虚函数 c++是一种面向对象的编程语言的一个很明显的体现就是对继承机制的支持,c++中继承分很多种,按不同的分类有不同分类方法,比如可以按照基类的个数分为多继承和单继承,可以按照访问 ...

  3. Jaskson精讲第7篇-类继承关系下的JSON序列化与反序列化JsonTypeInfo

    Jackson是Spring Boot(SpringBoot)默认的JSON数据处理框架,但是其并不依赖于任何的Spring 库.有的小伙伴以为Jackson只能在Spring框架内使用,其实不是的, ...

  4. 继承关系下的this关键字

    继承关系下的this关键字 在继承关系下,父类中的this关键字并不总是表示父类中的变量和方法.this关键字的四种用法如前文所述,列举如下. 1) this(paras…); 访问其他的构造方法 2 ...

  5. C++学习 之 类的继承中的虚函数(笔记)

    1.多态行为 多态是面向对象语言的一种特征,让我们能够以类似的方式处理不同类型的对象.在C++中我们可以通过继承层次结构实现子类型多态. 我们可以通过下面的代码进一步了解多态: #include< ...

  6. C++ 子类继承父类纯虚函数、虚函数和普通函数的区别

    C++三大特性:封装.继承.多态,今天给大家好好说说继承的奥妙 1.虚函数: C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现.子类可以重写父类的虚函数实现子类 ...

  7. C++继承-重载-多态-虚函数

    C++ 继承 基类 & 派生类 一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数.定义一个派生类,我们使用一个类派生列表来指定基类.类派生列表以一个或多个基类命名,形式如下: ...

  8. c++继承关系中成员函数的重载、重写、重定义之间的区别

    1.Override.Overload.Redefine Overload 重载只能发生在类内部,不能发生在子类和父类的继承中.具体来说,如果子类中有父类同名.同返回值类型,但是不同参数列表,这两个在 ...

  9. [c++] C++多态(虚函数和虚继承)

    转自:https://www.jianshu.com/p/02183498a2c2 面向对象的三大特性是封装.继承和多态.多态是非常重要的一个特性,C++多态基于虚函数和虚继承实现,本文将完整挖掘C+ ...

随机推荐

  1. NHibernate之旅(3):探索查询之NHibernate查询语言(HQL)

    本节内容 NHibernate中的查询方法 NHibernate查询语言(HQL) 1.from子句 2.select子句 3.where子句 4.order by子句 5.group by子句 实例 ...

  2. JS学习笔记 - 自定义右键菜单、文本框只能输入数字

    <script> // 事件总共有2个部分, //1.点击鼠标右键的表现 oncontextmenu 2.点击鼠标左键的表现(即普通点击onclick) // 点击右键,div位置定位到鼠 ...

  3. C#使用wkhtmltopdf.exe,HTML页面转化为PDF文档

    此文用来记录使用wkhtmltopdf.exe在C#代码中将html转换为PDF的过程: 1,在http://wkhtmltopdf.org/downloads.html 下载wkhtmltopdf. ...

  4. JVM学习:方法重载的优先级

    重载:方法名一致,参数长度或者类型不一致. 先放总结,下面为例子 参数具有继承.实现关系,优先考虑子类: 在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长 ...

  5. 【77.78%】【codeforces 625C】K-special Tables

    time limit per test 2 seconds memory limit per test 256 megabytes input standard input output standa ...

  6. 从Lua调用C

    从Lua调用C: 方式:C函数从栈中获取函数參数(第一个參数总是局部栈的索引1),将结果压入栈中,C函数须要返回结果数量. 每一个函数都有自己的局部私有栈 样例: static int l_sin(l ...

  7. 仿凤凰时时彩代购平台源代码[ASP+MSSQL]完整下载

    源代码简单介绍 : 适用范围:  时时彩源代码,时时彩程序,开奖平台源代码,投注平台源代码,仿凤凰时时彩源代码 执行环境:  ASP+MSSQL 其它说明:仿凤凰时时彩代购平台源代码.网上售价8000 ...

  8. php实现求字符串第一个只出现一次的字符

    php实现求字符串第一个只出现一次的字符 一.总结 很简单的逻辑 1.两个数组,一个存字母,一个存字母出现的次数 二.php实现求字符串第一个只出现一次的字符 题目描述 在一个字符串(1<=字符 ...

  9. js进阶 11-9/10/11 jquery创建和插入节点

    js进阶 11-9/10/11 jquery创建和插入节点 一.总结 一句话总结: 1.jquery插入节点8个方法? 内部之前,内部之后,之前,之后:各两个 append()和appendTo() ...

  10. 小强的HTML5移动开发之路(52)——jquerymobile中的触控交互

    当使用移动设备进行触控操作时,最常用的就是轻击.按住屏幕或者手势操作,jQuery Mobile可以通过绑定的触控事件来响应使用者的特定触控行为. 一.轻击与按住 直接上代码(一切皆在代码中,细细品吧 ...