前言
下面的工具查看对象的内存模型:
简单类的内存模型
首先,明白一个类的组成及其内存模型分布。如下图所示,类的成员中除了普通成员变量 之外,其他的均不存储在对象的存储空间中,只存储在静态区。
也就是说,一个类可以实例化n个对象,除了普通成员变量分别存储在n个对象的内存空间中,其他的成员在内存中只有一份。即,一个类的所有对象共享这份数据(静态成员函数 、普通成员函数 、虚函数 、静态成员变量 )。
理解静态成员属性和方法的特点
很重要的一点技巧:
- static的静态成员属性等能通过直接用类加上静态成员属性的方式调用,因为它属于类,不属于对象。
- 而成员方法只能声名一个对象(实例化)去利用对象调用(与this指针有关)
只有对象才会有内存模型!!
一定注意:只有对象才会有内存模型,类是没有对象模型的。类是实例化对象的一种方法,是告诉编译器如何去在内存中分配内存、初始化,从而构造出一个对象。
类没有独立的内存空间,类的成员函数和其他类的成员函数都存放在一起,这个地方叫做代码区。这些成员函数在 name mangling过程重新命名,然后通过类名来索引函数。
所以,类不存在于内存中,只存在于我们的代码和编译器中,是告诉编译器如何去构造对象的一份说明书。
成员函数如何获取对象数据??
疑问:对象的成员变量有很多份,但成员函数只有一份,那么成员函数在执行的时候,怎么知道是谁调用的呢?
举例:
class Test{
public:
static void test1();
int test2() {return m_a + m_b;}
private:
int m_a;
int m_b;
static int s_c;
};
int Test::test2(){
return m_a + m_b;
}
int main(){
Test t1;
Test t2;
int ret1 = t1.test2();
int ret2 = t2.test2();
}
你看,成员函数只有一份,但是对象的成员变量有很多份。由于不是每个对象都保存一份函数,那么成员函数在执行的时候,他怎么知道该取哪个对象的成员呢?
答案:this每个成员函数都有一个隐藏参数 this,存在于每个成员函数的第一个参数,this是对象的内存地址。
int Test::test2(Test* this){
return this->m_a + this->m_b;
}
总结,对象调用成员函数的流程是:
1. 对象调用成员函数
2.通过对象获取所属的类名;
3. 根据类名找到对应的成员函数;
4. 将对象的内存空间的地址通过this指针传给成员函数的this指针;
5. 成员函数就可以通过这个指针找到对应对象的成员。
基类与派生类的内存分配
派生类继承基类
内存分配时,是在于基类对象不同的内存地址处,按基类的成员变量类型,开辟一个同样的类型空间,但注意开辟后派生对象的空间,不是复制基类的成员的值,而是仅仅开辟那种成员类型的空间,未初始化时,里面存在的数是不确定的。
针对未初始化的问题可以使用下面两种方法:
- 在继承的基类中定义默认无参构造函数去初始化,
- 在子类中使用初始化列表对基类内存空间进行相应的参数初始化。
然后派生类自己定义的成员变量是排在继承的A类成员下面。
- 如果派生类定义的变量名与基类相同,则此变量覆盖掉继承的基类同名变量,
注意,覆盖不是删除,也 就是派生类中继承自基类的成员变量依然存在,而且值也不发生变化。如果想用此继承自基类的成员变量,则要加:: ,
- 在成员函数中访问时,直接用
base::i ,即可, - 用派生类的对象a访问时,如果此继承自基类的成员变量是对象可访问的(Public类型),则用
a.base::i 访问。
#include "head.h"
using namespace std;
class Base{
public:
int i;
int j;
Base(){
i = 1;
j = 1;
}
Base(int a,int b):i(a),j(b){}
};
class Sub:public Base{
public:
int m;
int n;
int i;
int j;
Sub():m(1),n(1),i(1),j(1){}
Sub(int a,int s,int d,int f):m(a),n(s),i(d),j(f){}
Sub(int a,int s,int d,int f,int g,int h):m(a),n(s),i(d),j(f),Base(g,h){}
};
int main(){
Base b(1,2);
Sub s(1,2,3,4,5,6);
cout << s.m << " "<<s.n <<" "<<s.i << " " << s.j <<\
" " << s.Base::i << " "<< s.Base::j << endl;
}
输出结果为:
1 2 3 4 5 6
从派生类对象继承的两个基类变量的值和及基类对象两个成员变量的值得比较看,足以验证上述结论:
子类继承的基类的成员,只是在另一个内存空间内开辟一个这种类型的成员变量,它的值并不是基类的值,编译器只是负责把这一部分空间类型设置为与基类的类型相同
类的内存占用情况分析
类所占内存的大小是由成员变量(静态变量除外)决定的,成员函数(这是笼统的说,后面会细说)是不计算在内的。
成员函数还是以一般的函数一样的存在。a.fun()是通过fun(a.this)来调用的。所谓成员函数只是在名义上是类里的。其实成员函数的大小不在类的对象里面,同一个类的多个对象共享函数代码。而我们访问类的成员函数是通过类里面的一个指针实现,而这个指针指向的是一个table,table里面记录的各个成员函数的地址(当然不同的编译可能略有不同的实现)。所以我们访问成员函数是间接获得地址的。所以这样也就增加了一定的时间开销,这也就是为什么我们提倡把一些简短的,调用频率高的函数声明为inline形式(内联函数)。
(一)
class CBase
{
};
sizeof(CBase)=1;
为什么空的什么都没有是1呢? c++要求每个实例在内存中都有独一无二的地址。//注意这句话!!!!!!!!!! 空类也会被实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof为1 。
(二)
class CBase
{
int a;
char p;
};
sizeof(CBase)=8;
记得对齐的问题。int 占4字节//注意这点和struct的对齐原则很像!!!!! char占一字节,补齐3字节
(三)
class CBase
{
public:
CBase(void);
virtual ~CBase(void);
private:
int a;
char *p;
};
再运行:sizeof(CBase)=12
C++ 类中有虚函数的时候有一个指向虚函数的指针(vptr),在32位系统分配指针大小为4字节。无论多少个虚函数,只有这一个指针,4字节 。//注意一般的函数是没有这个指针的,而且也不占类的内存。
(四)
class CChild : public CBase
{
public:
CChild(void);
~CChild(void);
virtual void test();
private:
int b;
};
输出:sizeof(CChild)=16; 可见子类的大小是本身成员变量的大小加上父类的大小。//其中有一部分是虚拟函数表的原因,一定要知道父类子类共享一个虚函数指针。
(五)
#include<iostream.h>
class a {};
class b{};
class c:public a{
virtual void fun()=0;
};
class d:public b,public c{};
int main()
{
cout<<"sizeof(a)"<<sizeof(a)<<endl;
cout<<"sizeof(b)"<<sizeof(b)<<endl;
cout<<"sizeof(c)"<<sizeof(c)<<endl;
cout<<"sizeof(d)"<<sizeof(d)<<endl;
return 0;}
程序执行的输出结果为:
sizeof(a) =1
sizeof(b)=1
sizeof(c)=4
sizeof(d)=8
前三种情况比较常见,注意第四种情况。类d的大小更让初学者疑惑吧,类d是由类b,c派生迩来的,它的大小应该为二者之和5,为什么却是8 呢?这是因为为了提高实例在内存中的存取效率.类的大小往往被调整到系统的整数倍.并采取就近的法则,里哪个最近的倍数,就是该类的大小,所以类d的大小为8个字节.
总结:
空的类是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。 (一)类内部的成员变量: 普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。 static修饰的静态变量:不占用内容,原因是编译器将其放在全局变量区。 (二)类内部的成员函数: 普通函数:不占用内存。 虚函数:要占用4个字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系的。
|