[GeekBand] C++继承关系下虚函数内存分布
本文参考文献: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++继承关系下虚函数内存分布的更多相关文章
- C++类虚函数内存分布(这个 你必须懂)
转自:http://www.cnblogs.com/jerry19880126/p/3616999.html C++类内存分布 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来 ...
- 谈谈c++中继承中的虚函数
c++继承中的虚函数 c++是一种面向对象的编程语言的一个很明显的体现就是对继承机制的支持,c++中继承分很多种,按不同的分类有不同分类方法,比如可以按照基类的个数分为多继承和单继承,可以按照访问 ...
- Jaskson精讲第7篇-类继承关系下的JSON序列化与反序列化JsonTypeInfo
Jackson是Spring Boot(SpringBoot)默认的JSON数据处理框架,但是其并不依赖于任何的Spring 库.有的小伙伴以为Jackson只能在Spring框架内使用,其实不是的, ...
- 继承关系下的this关键字
继承关系下的this关键字 在继承关系下,父类中的this关键字并不总是表示父类中的变量和方法.this关键字的四种用法如前文所述,列举如下. 1) this(paras…); 访问其他的构造方法 2 ...
- C++学习 之 类的继承中的虚函数(笔记)
1.多态行为 多态是面向对象语言的一种特征,让我们能够以类似的方式处理不同类型的对象.在C++中我们可以通过继承层次结构实现子类型多态. 我们可以通过下面的代码进一步了解多态: #include< ...
- C++ 子类继承父类纯虚函数、虚函数和普通函数的区别
C++三大特性:封装.继承.多态,今天给大家好好说说继承的奥妙 1.虚函数: C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现.子类可以重写父类的虚函数实现子类 ...
- C++继承-重载-多态-虚函数
C++ 继承 基类 & 派生类 一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数.定义一个派生类,我们使用一个类派生列表来指定基类.类派生列表以一个或多个基类命名,形式如下: ...
- c++继承关系中成员函数的重载、重写、重定义之间的区别
1.Override.Overload.Redefine Overload 重载只能发生在类内部,不能发生在子类和父类的继承中.具体来说,如果子类中有父类同名.同返回值类型,但是不同参数列表,这两个在 ...
- [c++] C++多态(虚函数和虚继承)
转自:https://www.jianshu.com/p/02183498a2c2 面向对象的三大特性是封装.继承和多态.多态是非常重要的一个特性,C++多态基于虚函数和虚继承实现,本文将完整挖掘C+ ...
随机推荐
- axios封装http请求
import axios from 'axios' const HTTP_TIMEOUT = 15000; export function httpPost(url, params = {},head ...
- 接口如何使用(以笑话大全api为例)
接口如何使用(以笑话大全api为例) 一.总结 一句话总结:直接用ajax,或者post,get方式向接口网址请求数据,然后接收网站传过来的数据就好,和我们写网站的时候前台向后台请求数据的方式一样. ...
- WebApp调用手机相册或摄像头、拨打电话
WebApp调用手机相册或摄像头.拨打电话 一.总结 一句话总结:input标签,指定type为file,选择好对应的accept即可.camera——相机,相应的accept为image : cam ...
- RAC RMAN 备份 RMAN-03009 ORA-19504 ORA-27040 RMAN-06012 channel c3 not allocated 错误分析
备份Shell 脚本如下: ######################################################################## ## RAC_hot_da ...
- 在Java中,return null 是否安全, 为什么?
Java代码中return value 为null 是不是在任何情况下都可以,为什么不会throw NullPointerException? Java语言层面:null值自身是不会引起任何问题的.它 ...
- /bin/bash^M: bad interpreter: 没有那个文件或文件夹
执行脚本时出现了这样一个错误,打开之后并没有找到所谓的^M,查了之后才知道原来是文件格式的问题,也就是linux和windows之间的不全然兼容... 详细细节无论,假设验证: vim test.sh ...
- $_SERVER['DOCUMENT_ROOT']
$_SERVER['DOCUMENT_ROOT'] 一.总结 $_SERVER 是一个包含了诸如头信息(header).路径(path).以及脚本位置(script locations)等等信息的数组 ...
- matlab 程序发布
将matlab程序发布为可执行程序包 说明,这种可执行程序包可以在没有安装matlab的计算机上运行. 1. 打开Applicaiton Compler 如果下拉列表中没有这个APPLICATIOND ...
- AE内置Command控件使用
樱木 原文 AE内置Command控件使用 直接使用AE内置的Command控件来完成功能 1.拉框放大 /// <summary> /// 放大 /// </summary> ...
- Android自定义组件系列【10】——随ViewPager滑动的导航条
昨天在用到ViewPager实现滑动导航的时候发现微信的导航条效果是跟随ViewPager的滑动而动的,刚开始想了一下,感觉可以使用动画实现,但是这个滑动是随手指时时变化的,貌似不可行,后来再网上搜了 ...