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

  • 相关阅读:
    hdoj2187:悼念512汶川大地震遇难同胞 (贪心)
    2.0其它之Transform详解,以及UIElement和FrameworkElement的常用属性
    2.0外观之样式, 模板, 视觉状态和视觉状态管理器
    2.0图形之Ellipse, Line, Path, Polygon, Polyline, Rectangle
    2.0控件之ListBox, MediaElement, MultiScaleImage, PasswordBox, ProgressBar, RadioButton
    2.0画笔之SolidColorBrush, ImageBrush, VideoBrush, LinearGradientBrush, RadialGradientBrush
    2.0图形之基类System.Windows.Shapes.Shape
    2.0交互之鼠标事件和键盘事件
    2.0控件之ScrollViewer, Slider, StackPanel, TabControl, TextBlock, TextBox, ToggleButton
    2.0交互之InkPresenter(涂鸦板)
  • 原文地址:https://www.cnblogs.com/OneDream/p/3367660.html
Copyright © 2011-2022 走看看