关于C++的类对象,内存分布问题

  1. 云栖社区>
  2. 博客>
  3. 正文

关于C++的类对象,内存分布问题

技术小牛人 2017-11-14 14:36:00 浏览566
展开阅读全文

原问题如下:

#include <iostream> 
using namespace std; 
class base{ 
public: 
 virtual void fun1() 
 { 
 cout <&lt; "fun1 called !" &lt;&lt; endl; 
 } 
 void fun2() 
 { 
 cout &lt;&lt; "fun2 called !" &lt;&lt; endl; 
 } 
}; 
int main() 

    base s; 
    cout &lt;&lt; sizeof(s) &lt;&lt; endl; 
    return 0; 

代码的结果为4。 
这个我觉得是因为那个虚函数表里的一个指针占了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的原因。 
不知道这样能不能帮你解惑,呵呵,一家之言哈,欢迎拍砖。

 

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

肖老师,你的这篇文章解我心中长久以来的疑虑,对菜鸟我极为有用! 
老师你在文章中说:......类里面还有什么呢? 
还有虚函数,我们知道,虚函数表示可能继承,事实上,多次(不是多重)继承后,一个类的虚函数内部会有一个栈,每个虚函数都有一个栈,每次调用该函数,会从栈顶开始call,当然,如果程序员愿意,也可以在继承的虚函数内部,通过调用父类的同名虚函数,逐级向下call,直至call完所有的虚函数为止。 
-------------------------------- 
肖老师,上面这段话菜鸟我看来看去还是不懂,能否请你稍微解释的详细一些?也许对虚函数调用的内部实现机制听了你说的就明白了.不好意思,给你添麻烦了.

 

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

这是说,类的虚函数,实际上内部存储上,表现为一个函数指针栈,栈底,是基类这个函数的指针,往上,实际上是继承类,该虚函数的继承函数的指针,一个类,被继承几次,比如说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 &lt; 0xF000);

CMenu* pSysMenu = GetSystemMenu(FALSE); 
if (pSysMenu != NULL) 

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

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

}

// 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动 
//  执行此操作 
SetIcon(m_hIcon, TRUE); // 设置大图标 
SetIcon(m_hIcon, FALSE); // 设置小图标

// TODO: 在此添加额外的初始化代码

return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE 
}

注意到没,由于继承类的虚函数一旦实现,父类的虚函数就被自动屏蔽,VC也必须手动实现对父类虚函数的层级调用,才能完成基本功能。 
很多时候,我们的同学,手动继承一个类之后,玩虚函数老是忘了这个手动调用父类,结果发现,虚函数功能越继承越少,甚至继承到功能没有了,就是搞忘了这点。 
但是,上述代码是VC的向导自动添加的,VC并没有对此作显式说明,结果,大家在只用IDE开发的过程中,老是关注不到这个细节,自己做的时候就出错。这类问题还很多。 
这在开发中,表现出来的就是,向导的代码永远正确,自己的手工代码一写就错,慢慢弄下来,搞得很多人都不敢手工写代码了。 
我原来写文章建议,初学者不要用IDE,其实就是指,这些细节被IDE自动完成,程序员关注不到,就学不到真东西,搞得哪天一手写,就出错。 
呵呵,扯远了,虚函数的继承关系,我知道的大概就这么点,已经全部兜给你了,再不够,只有请你自己查书了。 
呵呵,好累。

 

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

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

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

void fun2() 

cout &lt;&lt; "fun2 called !" &lt;&lt; endl; 

double    d; 
static int  a; 
}; 
int main() 

    base s; 
    cout &lt;&lt; sizeof(s) &lt;&lt; endl; 
    return 0; 

疑问 1  double 是8个字节 这个程序运行是16个字节 除了虚函数的4个 那多余处理的4个呢? 
疑问 2   static int  a;我将将static int  换成double是24个没错,可我将static 去掉,那么个int应该是4个字节 共 
4+8+4=16 可程序运行是24个 why?

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

晕了,这个问题很难,我够呛能回答出来。 
先做做试验吧。 
#include &lt;iostream> 
using namespace std; 
class base 

public: 
        virtual void fun1() 
        { 
                cout <&lt; "fun1 called !" &lt;&lt; endl; 
        } 
        void fun2() 
        { 
                cout &lt;&lt; "fun2 called !" &lt;&lt; endl; 
        } 
}; 
void TestBase(void) 

        base b; 
        printf("object length=%d\n",sizeof(b)); 
        dbg_bin((char*)&b,sizeof(b)); 

我先打印一下基本类的内容,这个dbg_bin是我自己的工具,二进制输出一段内存区的内容。打印结果如下: 
object length=4 
0013FE8C - E4 58 44 00                                         .XD. 
对象长度4Bytes,内部只有一个指针,应该就是Fun1的指针,0x004458E4

ok,我们再来打印你的例子,这里先说明一点,在你的例子中,由于a从头到尾都没有被使用过,因此实际上编译器根本就没有为其分配单元,不信,你到main中试图访问一下a,会失败,正解是应该在类声明完毕后,马上做一次显式的静态变量赋值动作,以强迫编译器为a分配内存单元。

#include &lt;iostream> 
using namespace std; 
class base 

public: 
        virtual void fun1() 
        { 
                cout <&lt; "fun1 called !" &lt;&lt; endl; 
        } 
        void fun2() 
        { 
                cout &lt;&lt; "fun2 called !" &lt;&lt; endl; 
        } 
        double m_dDouble;                //d 
        static int m_nStaticInt;        //a 
}; 
int base::m_nStaticInt=15; //看好这里,一次赋值动作,强迫编译器分配 
void TestBase(void) 

        base b; 
        b.m_dDouble=0.0; 
        printf("object length=%d\n",sizeof(b)); 
        printf("b.m_dDouble=%f\n",b.m_dDouble); 
        printf("base::m_nStaticInt=%d\n",b.m_nStaticInt); 
        dbg_bin((char*)&b,sizeof(b)); 

我们来看看输出结果。 
object length=16 
b.m_dDouble=0.000000  //这应该是8Bytes的0 
base::m_nStaticInt=15   //赋值成功,但是,注意下面,没有它的踪迹 
0013FE80 - 90 38 44 00  CC CC CC CC  00 00 00 00  00 00 00 00  .8D............. 
我们看看哈,第一个4Bytes,应该是虚函数fun1的指针,0x00443890 
第二个,4个cc,这是vc编译器在debug模式下默认的占位数字,没有意义,只是表示从进程空间创建以来,还没有为其填充过数字,这几个单元,我猜测是没有使用,属于占位符。 
后面8个00,显然是double的空间,a不见了,因为静态变量,不随对象走,是在基栈中,所有这个类的对象都看得见,大家共用一个变量内存区。

ok,我们再把a的static修饰去掉看看。 
#include &lt;iostream> 
using namespace std; 
class base 

public: 
        virtual void fun1() 
        { 
                cout <&lt; "fun1 called !" &lt;&lt; endl; 
        } 
        void fun2() 
        { 
                cout &lt;&lt; "fun2 called !" &lt;&lt; endl; 
        } 
        double m_dDouble;        //d 
        int m_nStaticInt;        //a 
}; 
void TestBase(void) 

        base b; 
        b.m_dDouble=0.0; 
        b.m_nStaticInt=15;  //赋初值,好辨认 
        printf("object length=%d\n",sizeof(b)); 
        printf("b.m_dDouble=%f\n",b.m_dDouble); 
        printf("b.m_nStaticInt=%d\n",b.m_nStaticInt); 
        dbg_bin((char*)&b,sizeof(b)); 

好,打印出来看。 
object length=24 
b.m_dDouble=0.000000 
b.m_nStaticInt=15 
0013FE78 - 90 38 44 00  CC CC CC CC  00 00 00 00  00 00 00 00  .8D............. 
0013FE88 - 0F 00 00 00  CC CC CC CC                            ........ 
前面都差不多,不过,a放进对象里面了,值是15(0xF),后面又多了四个占位Bytes。

ok,再做个实验,我们把a换成double 
#include &lt;iostream> 
using namespace std; 
class base 

public: 
        virtual void fun1() 
        { 
                cout &lt;&lt; "fun1 called !" &lt;&lt; endl; 
        } 
        void fun2() 
        { 
                cout &lt;&lt; "fun2 called !" &lt;&lt; endl; 
        } 
        double m_dDouble;                        //d 
        double m_dDouble2;                        //a 
}; 
void TestBase(void) 

        base b; 
        b.m_dDouble=0.0; 
        b.m_dDouble2=1.0; 
        printf("object length=%d\n",sizeof(b)); 
        printf("b.m_dDouble=%f, size=%d\n",b.m_dDouble,sizeof(b.m_dDouble)); 
        printf("b.m_dDouble2=%f, size=%d\n",b.m_dDouble2,sizeof(b.m_dDouble2)); 
        dbg_bin((char*)&b,sizeof(b)); 

来看看结果: 
object length=24 
b.m_dDouble=0.000000, size=8 
b.m_dDouble2=1.000000, size=8 
0013FE78 - 90 38 44 00  CC CC CC CC  00 00 00 00  00 00 00 00  .8D............. 
0013FE88 - 00 00 00 00  00 00 F0 3F                            .......?

具体原因我也解释不清楚啦,能看到的就这些,貌似这个C++对象是以8Bytes取整的,即不够8Bytes,会填充占位符,这个我没找到相关理论依据,只能说这是编译器的特性了。

在很多时候,32位编译器为了优化,可能会以4Bytes的整数倍,做向上取整,这是常见手法,不过,这8Bytes取整,我也第一回遇到,我也搞不懂。看有没有哪位高人帮忙解释一下?

本文转自  tonyxiaohome  51CTO博客,原文链接:http://blog.51cto.com/tonyxiaohome/198762 ,如需转载请自行联系原作者

网友评论

登录后评论
0/500
评论
技术小牛人
+ 关注