类对象内存分布

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

如何计算类对象占用的字节数?

一个空类的话1个字节。这是为了保证n个连续空类声明时,变量地址有偏移,防止变量覆盖。 
非空类的话用关键字sizeof计算。
如果手工计算就相当麻烦,光padding就一堆规则了。而且有些额外信息比如虚函数(多个虚函数也只产生一个vptr指针)等等。
一个类成员 ,当有虚函数时,有以下成分:各个数据成员,数据对齐产生的间隙,一个虚函数表的 "指针"(无虚不存在)。
构造函数不能用 memset(this, 0, sizeof(*this))) 初始化。原因就是每个类里面除了数据成员之外 ,还有一个虚函数表指针 。memcpy另一个同类型类的实例内容过去倒是可以 ,这种情况下该函数表指针可以正确复制过去。
注意虚函数表只在类有虚函数的情况下才存在, 没有虚函数不存在。 构造函数悄悄地帮你设置虚函数表的内容,并把正确的指针存放在对象中。

类在内存中分布

首先请看程序:

#include <iostream>
using namespace std;
class base{ 
public: 
virtual void fun1() 
//void fun1()

cout << "fun1 called !" << endl; 

void fun2() 

cout << "fun2 called !" << endl; 

}; 
int main() 

base s; 
cout << sizeof(s)<< endl; 
return 0; 

代码的结果为4,这是由于虚指针的存在。 但是如果去掉virtual 代码的结果为1。

为什么呢?
这个涉及到类和结构体,在C++内部的排列方式。 
我也不是很了解,只能就自己了解的一点知识做点回答,欢迎大家指正。 
我们知道,C和C++虽然都支持结构体,但是,实际上表现是不一样的。C++的结构体,可以认为是类的一种变体,二者的差异性,类中成员,如果不声明,默认是Private的,结构体中成员,如果不声明,则默认是Public的。 
但是,在C++里面,二者内部都可以内置成员函数,而C的结构体,内部只允许存在成员变量,如果需要内置成员函数,需要程序员显式声明函数指针变量,换句话说,就是C在结构体中管理成员函数,是程序员自己来管理,C++则是编译器代为管理。 
这意味着什么呢? 
在C++中,成员函数和成员变量,都是类和结构体的成员,但二者有所差异。 
编译器在编译每个类时,不管这个类以后会实例化几个对象,首先,它会提取这些类的共性,放到一起,做成一个表。 
比如类里面的非虚函数,这类函数,所有的对象共享一段函数代码,自然没有必要每个对象内部都设置一个函数指针,这太浪费内存了。 
因此,一个类,所有的非虚函数,会被编译器排成一个符号表,放置在特定的编译期基础变量区。这实际表现看,是放在exe文件里面的,在调用一个程序时,是直接从文件中读出,并经过地址修订,准备使用,这部分连基栈都算不上,算是常量区了,所有的常量也是放在这个区。 
嗯,函数内部的静态变量,类中的静态变量,静态函数,都是这个区。 
那,除掉这些,类里面还有什么呢? 
还 有虚函数,我们知道,虚函数表示可能继承,事实上,多次(不是多重)继承后,一个类的虚函数内部会有一个栈,每个虚函数都有一个栈,每次调用该函数,会从 栈顶开始call,当然,如果程序员愿意,也可以在继承的虚函数内部,通过调用父类的同名虚函数,逐级向下call,直至call完所有的虚函数为止。 
这就说明,虚函数和普通成员函数不同,每个对象都有可能变化,因此,编译器就不敢把这个函数的指针,放在常量区,必须跟着对象走,注意,不是类,类是没有实体的,因此,不存在sizeof,只有对象存在大小。 
还有就是普通成员变量,这些内容,每个对象也是不一样的,因此,每个对象必须自己建立一个表来管理,否则大家就混了。 
因此,我们知道了,每个类,实例化对象之后,其实对象的实体在内存中的存储,就只包含虚函数和普通成员变量,这是C++编译器为了节约内存做得优化。 
我们回到你的代码看,你的代码中,fun2是普通函数,被编译器放到常量区去了,因此,不占用对象空间,虚函数fun1,则需要占用,我们知道,32位操作系统,一个指针是4Bytes,函数指针也是指针,因此,你的结果是4Bytes。
取消了virtual 之后,fun1也变成了普通函数,因此和fun2等同处理,就不再占用对象空间,因此,对象空间为0了。 
不过,我隐隐约约听谁说过,C++语言不允许对象空间为0,这样的话,对象指针就没有落点了,因此,一个对象的空间,至少占用1Byte,这就是你的结果为1的原因。

虚函数调用的几点补充说明:

类的虚函数,实际上内部存储上,表现为一个函数指针栈,栈底,是基类这个函数的指针,往上,实际上是继承类,该虚函数的继承函数的指针,一个类,被继承几次,比如说3次,最后一次继承,这个栈就有3层。有点绕。

举个例子吧 
class A 

virtual void Func(void) 
}; 
class B : public A 

virtual void Func(void) 
}; 
class C : public B 

virtual void Func(void) 
}; 
这个A类,里面的Func指针就是它自己 
B就是一个栈了,栈底是A::Func,栈顶是B::Func 
而C就是三层的栈了,在B的基础上,栈顶又压入了C::Func 
基本上就是这个管理关系。 
我的话的意思是,在任何一层继承函数,都可以去手动去call父类的对应函数,完成对整个栈链上所有函数的调用。
因为我们知道,一个类的虚函数,一旦被继承,原来的父类函数指针就被压倒栈下面去了,从栈顶看,只有最后一层的函数指针。 
比如C这个类看,我们看它的Func,只要它继承并实现了,那么,调用Func一定只能调用C::Func,B和A的由于看不到,因此是不会被调用的。 
当然,如果C没有实现这个虚函数,则Func的栈上,没有C::Func,因此,直接Call会Call到B::Func,以此类推,如果B没有实现这个虚函数,表示未继承,则Call会Call到A::Func,这就是虚函数继承中,后实现的覆盖前实现的原理。 
当然,如果A内没有实现Func的实体,做了一个纯虚函数,而B和C这些继承类也不实现,那么,编译器在构造符号表的时候,就会找不到任何一个Func的实体,该虚函数栈为空,无法连接,因此会报连接失败的错误,编译不能通过。
这种栈式管理,有好有坏,好处是后面的继承类,可以选择实现虚函数,也可以选择不实现,偷个懒。程序不会出错,下次调用该函数,会自动沿着它的继承关系,寻找父类以及更往前的爷爷类的函数实体,至少能找到一个执行其功能,简化开发。 
但是,也有一 个坏处,就是一个虚函数,一旦被继承类实现了,则父类的必然被覆盖,如果父类有什么内置的功能,就没有办法执行了,这很麻烦,由于面向对象的继承关系,我 们总是希望,继承类的对应函数,只要完成它相对于父类增加的那部分功能就够了,父类的功能,还能继续执行,免得写重复的代码。

这个例子在MFC开发中很多,很多时候,我们的一个窗口类,是从CDialog这个类继承的,而CDialog,又是CWnd这个类继承的。针对一个虚函数方法,比如说CWnd::Create这个方法。 
virtual BOOL Create( LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect...
我们知道,创建一个窗口有一大堆事情要做,这些事情,MFC已经在CWnd的Create这个函数里面实现好了,但好死不死,它把这个函数方法设置为虚函数了,就是说,后续继承类可以自己来实现这个方法。 
我们这么来假 设,如果我们那个工程的窗口,继承自CDialog,然后,我们自己实现了这个Create方法,那完蛋了,由于C++这个覆盖特性,执行的时候,就只执 行我们这个Create了,下面的CDailog::Create和CWnd::Create都执行不了,除非我们把那两个函数内部所有的代码抄一遍,否 则,这个Create根本没有办法完成我们希望完成的功能。他失去了创建窗口的功能。 
因此,为了解决这个问题,C++允许继承类的虚函数,显式调用父类的虚函数,以实现父类的基础功能,最后,才是我们自己新增加的代码。 
这个意思主要是说,虚函数的继承,看似省事,但他不是想当然会先实现父类功能,后调用新增代码,需要我们手动call。 
再看看这个例子,我们以VC建立一个MFC的对话框工程,就叫test。
// CtestDlg 对话框 
class CtestDlg : public CDialog 

// 构造 
public: 
CtestDlg(CWnd* pParent = NULL); // 标准构造函数
// 对话框数据 
enum { IDD = IDD_TEST_DIALOG };
protected: 
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现 
protected: 
HICON m_hIcon;
// 生成的消息映射函数 
virtual BOOL OnInitDialog(); //看好这一句啊,虚函数 
afx_msg void OnSysCommand(UINT nID, LPARAM lParam); 
afx_msg void OnPaint(); 
afx_msg HCURSOR OnQueryDragIcon(); 
DECLARE_MESSAGE_MAP() 
}; 
注意其中OnInitDialog,好,我们来看看VC自动为我们生成的这个函数是怎么写的: 
BOOL CtestDlg::OnInitDialog() 

CDialog::OnInitDialog(); //看这句,在干吗?
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。 
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); 
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE); 
if (pSysMenu != NULL) 

CString strAboutMenu; 
strAboutMenu.LoadString(IDS_ABOUTBOX); 
if (!strAboutMenu.IsEmpty()) 

pSysMenu->AppendMenu(MF_SEPARATOR); 
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); 

}
// 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动 
// 执行此操作 
SetIcon(m_hIcon, TRUE); // 设置大图标 
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE 
}
注意到没,由于继承类的虚函数一旦实现,父类的虚函数就被自动屏蔽,VC也必须手动实现对父类虚函数的层级调用,才能完成基本功能。 
很多时候,我们的同学,手动继承一个类之后,玩虚函数老是忘了这个手动调用父类,结果发现,虚函数功能越继承越少,甚至继承到功能没有了,就是搞忘了这点。 
但是,上述代码是VC的向导自动添加的,VC并没有对此作显式说明,结果,大家在只用IDE开发的过程中,老是关注不到这个细节,自己做的时候就出错。这类问题还很多。

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

时间: 2024-10-18 14:15:23

类对象内存分布的相关文章

C++类的内存分布

使用Visual Studio工具来看是类的内存分布 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局.近期的VS版本都支持这样配置. 下面可以定义一个类,像下面这样: class Base { int a; int b; public: void CommonFunction(); };

《C++反编译与逆向分析技术揭秘》之学习笔记02--结构体和类之内存分布

※结构体和类之内存分布 1.空类的大小空类:其实空类至少会占用1个字节的长度. 2.字节对齐在为结构体和类中的数据成员分配内存时,结构体中的当前数据成员类型长度为M,指定对齐值为N,那么实际对齐值位q=min(M,N),其成员的地址安排在q的倍数上. vc6.0缺省对齐8个字节sShort占用2个字节,所以安排的地址0x0012FF70为2的倍数.nInt占用4个字节,所以安排的地址0x0012FF74为4的倍数.因为结构体中最大的字段长度为4,所以对齐值调整为4个字节.因为test对象为8个字

C++对象内存分布(3) - 菱形继承(virtual)

1.前言 本篇文章的所有代码例子,如果是windows上编译运行,则使用的是visual studio 2013.如果是RHEL6.5平台(linux kernal: 2.6.32-431.el6.i686)上编译运行,则其gcc版本为4.4.7,如下所示: [[email protected] ~]# gcc --version gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-4) 2.菱形继承类的内存分布 本篇文章主要讨论的是虚继承(virtual)下的内存分

面向对象--多继承&amp;派生类对象内存布局分析&amp;各基类指针所指向的位置分析

背景 原文链接:ordeder  http://blog.csdn.net/ordeder/article/details/25477363 关于非虚函数的成员函数的调用机制,可以参考: http://blog.csdn.net/yuanyirui/article/details/4594805 成员函数的调用涉及到面向对象语言的反射机制. 虚函数表机制可以查看下面这个blog: http://blog.csdn.net/haoel/article/details/1948051 总结为: 其一

C++对象内存分布(2) - 菱形继承(non virtual)

1.前言 本篇文章的所有代码例子,如果是windows上编译运行,则使用的是visual studio 2013.如果是RHEL6.5平台(linux kernal: 2.6.32-431.el6.i686)上编译运行,则其gcc版本为4.4.7,如下所示: [[email protected] ~]# gcc --version gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-4) 2.菱形继承类的内存分布 2.1.类的结构 菱形继承 - 重复继承 2.2.实现

关于虚函数,类的内存分布以及类的成员函数调用原理

1.类的内存分布 空类为了占位,空间占一个字节 成员函数,静态函数,静态变量并不占内存(不和类放在一起) 所有的虚函数也并不和类放在一起,而是将所有的虚函数构造成一个虚函数表,用一个指针指向这个虚函数表,类仅仅存储这个指针,一个指针在32位的机器上占四个字节 所有的非静态成员变量占内存 因此,类的内存分布=所有的非静态成员变量+虚指针(自创的名词:即指向虚函数表的指针) 2.虚函数的原理 一个非继承的类:一个虚指针(指向他的虚函数表). 一个单继承的类:一个虚指针(指向他的虚函数表,这个虚函数表

VS中的类的内存分布(上)

0.序 目前正在学习C++中,对于C++的类及其类的实现原理也挺感兴趣.于是打算通过观察类在内存中的分布更好地理解类的实现.因为其实类的分布是由编译器决定的,而本次试验使用的编译器为VS2015 RC,所以此处的标题为<VS中的类的内存分布>. 1.对无继承类的探索 1.1 空类 我们先一步一步慢慢来,从一个空的类开始. //空类 class test { }; int main(int argc, char *argv[]) { test ts; cout << sizeof(t

多继承(虚继承)派生类对象内存结构

在这里谈一下虚继承.前面写过派生类对象的内存结构,都是基于VS2010编译器的,不同的编译器对于继承的处理不同,但本质都是一样的. 虚继承是解决共享基类问题的.例如在菱形继承中 如果不使用虚继承,基类A在D中会有两个,这不仅浪费内存,还会造成歧义.使用虚继承就可以解决基类共享的问题. 要想在派生类中共享基类(例如在D对象中只有一个A对象,这时候D对象中的B对象和C对象都可以查找到A,而不是在B对象和C对象中各含有一个A对象). 先看下面一个例子: #include<iostream> usin

C++基类指针指向的派生类对象内存的释放

C++由于基类指针可以指向不同的派生类对象,因此当赋予基类指针不同的地址时,要注意之前的派生类对象的内存释放. int main(){ Parent* ptr = new Child1; Child2 myChild2; Child3 myChild3; ptr->show(); delete ptr; //位置1 ptr = &myChild2; ptr->show(); delete ptr; //位置2 ptr = &myChild3; ptr->show(); d