zoukankan      html  css  js  c++  java
  • C++的内存分布(一)转

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

    一个空类的话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开发的过程中,老是关注不到这个细节,自己做的时候就出错。这类问题还很多。 

  • 相关阅读:
    MongoDB Replica Set 选举过程
    转 Warning:MongoDB Replica Sets配置注意事项
    mongodb与内存
    mongodb的监控与性能优化
    php 5.5.1 编译安装过程
    java知识点积累(一)
    java内存及数据区
    static及静态方法
    接口和抽象类
    Java SE自学阶段的笔记整理
  • 原文地址:https://www.cnblogs.com/OneDream/p/3367660.html
Copyright © 2011-2022 走看看