一、数据成员绑定时机
1 | typedef string mytype; |
(1)编译器对成员函数的解析,是整个类定义完毕后才开始的。因为只有整个类定义完毕后,编译器才能看到类中的成员变量,才能根据实际的需要把出现该成员变量的场合做适当的解释(成员函数中解析成类中的变量类型,全局函数中解析成全局的变量类型)。
(2)对于成员函数参数,是在编译器第一次遇到整个类型mytype的时候被决定的。所以,mytype第一次遇到的时候,编译器只看到了typedef string mytype,没有看到类中的typedef int mytype。
(3)为了在类中尽早的看到类型mytype,所以这种类型定义语句typedef,一定要挪到类的最开头定义。(即上述代码中应该把typedef int mytype移到class A的开头处)当后边的成员函数第一次遇到这个类型mytype的时候,它就本着最近碰到的类型的原则来应用最近碰到的类型。
二、进程内存空间布局
不同的数据在内存中会有不同的保存时机,保存位置。当运行一个可执行文件时,操作系统就会把这个可执行文件加载到内存,此时进程有一个虚拟的地址空间(内存空间),分为:堆栈段、数据段、代码段等。从下面代码中可以看出些许规律。
1 | int *ptest = new int(120); |
从上述代码的打印结果来看,不同类型的数据在内存中的存储位置是不同的,同一类型的数据是连续存储的。
三、类中成员变量的布局
(1)普通成员变量的存储顺序是按照在类中的定义顺序从上到下来的。比较晚出现的成员变量在内存中有更高的地址。类定义中pubic,private,protected的数量,不影响类对象的sizeof。
(2)某些因素会导致成员变量之间排列不连续,就是边界调整(字节对齐),调整的目的是提高效率,编译器自动调整。调整方式:往成员之间填补一些字节,使用类对象的sizoef字节数凑成一个4的整数倍,8的整数倍。
(3)为了统一字节对齐问题,引入一个概念叫一字节对齐(不对齐):#pragma pack(1)。把这个语句放到程序最开始处即可。
(4)有虚函数时,编译器往类定义中增加vptr虚函数表指针(内部的数据成员)。
(5)成员变量偏移值,就是这个成员变量的地址,离对象首地址偏移多少。
四、类中成员变量的存取
(1)静态成员变量,可以当做一个全局量,但是它只在类的空间内可见。引用时用类名::静态成员变量名。静态成员变量只有一个实体,保存在可执行文件的数据段。
(2)非静态成员变量(普通的成员变量)存放在类的对象中。存取通过类对象(类对象指针)来操作。例如:对于普通成员的访问,编译器是把类对象的首地址加上成员变量的偏移值。
五、单一继承下的数据成员布局
(1)一个子类对象,所包含的内容,是他自己的成员,加上他父类的成员的总和。从偏移值看,父类成员先出现,然后才是孩子类成员。
(2)引入继承关系后,可能会带来内存空间的额外增加(字节对齐)。所以不能用memcpy内存拷贝把父类对象的内容直接往子类对象里拷贝。
六、带有虚函数的类中数据成员布局
单个类带虚函数的数据成员布局
类中引入虚函数时,会有额外的成本付出:
1)编译的时候,编译器会产生虚函数表。
2)对象中会产生虚函数表指针vptr,用以指向虚函数表。
3)增加或者扩展构造函数,增加给虚函数表指针vptr赋值的代码,让vptr指向虚函数表。
4)如果多重继承,比如你继承了2个父类,每个父类都有虚函数的话,每个父类都会有vptr,那继承时,子类就会把这两根vptr都继承过来,如果子类还有自己额外的虚函数的话,子类与第一个基类共用一个vptr。
5)析构函数中也被扩展增加了虚函数表指针vptr相关的赋值代码,这个赋值代码似乎和构造函数中代码相同。
单一继承父类带虚函数的数据成员布局
代码如下:
1 | class Base |
从打印结果可以看出,当父类中有虚函数的时候,指向父类虚函数表的虚函数表指针会占用内存开头的四个字节,紧接着是父类的成员变量,最后才是子类的成员变量。
单一继承父类不带虚函数的数据成员布局
1 | class Base |
从打印结果可以看出,如果父类中没有虚函数,则父类的成员变量就放到了内存开头处,之后是子类的成员变量。
但是如果子类中有虚函数而父类中没有,内存布局又会发生变化,即数据在内存空间中的分布分别是:父类成员、子类的虚函数表指针、子类成员。代码及打印结果如下:
1 | class Base |
如果子类和父类中都有虚函数,则子类和父类共用一个虚函数表指针,即它俩的地址一样,都在内存的开头处,代码如下:
1 | class Base |
总结:不管是父类还是子类,只要含有虚函数,则虚函数指针就位于对应的所有数据成员的前面(父类指针对应父类成员,子类指针对应子类成员),如果父类和子类都含有虚函数,则它俩的虚函数表指针共用一块内存,位于内存空间的开头处。
多重继承且父类都带虚函数的数据成员布局
1 | class Base1 |
(1)通过this指针打印,我们看到访问Base1成员不用跳 ,访问Base2成员要this指针要偏移(跳过)8字节。
(2)我们看到偏移值,m_bi和m_b2i偏移都是4。
(3)this指针,加上偏移值就的能够访问对应的成员变量,比如m_b2i = this指针+偏移值。
结论:我们要访问一个类对象中的成员,成员的定位是通过:this指针(编译器会自动调整)以及该成员的偏移值,这两个因素来定义。这种this指针偏移的调整都需要编译器介入来处理完成。
虚基类相关
1 | class Grand //爷爷类 |
(1)虚基类就是让Grand类只被继承一次,防止二义性问题。
(2)有虚基类,就有虚基类表vbtable(virtual base table)、虚基类表指针vbptr(virtual base table pointer)。
(3)空类sizeof(Grand)=1好理解。虚继承之后,A1,A2里就会被编译器插入一个虚基类表指针,这个指针,有点成员变量的感觉。A1,A2里因为有了虚基类表指针,因此占用了4个字节。
1 | class Grand //爷爷类 |
根据内存分析可以看出:1-4字节和9-12字节应该存放的就是两个虚基类表指针,其中1-4字节是vbptr1(继承自A1),9-12字节是vbptr2(继承自A2)。其余的数据分布如上述程序中所示,需要注意的是:爷爷类里的成员分布在内存的最后。
1 | class Grand //爷爷类 |
由此,可以判断出,一个类同时虚继承多个父类,只会产生一个虚基类指针(A1)。1-4字节放的是A1的虚基类指针,9-12字节放的是A2的虚基类指针。其他的数据分布如程序所示。