空虚基类与派生类占用的内存
class X{
};
class Y:public virtual X{
};
class Z:public virtual X{
};
class A:public Y,public Z{
};
上述类X,Y,Z,A中都没有显式定义的数据,只表示了它们之间的继承关系,它们的大小都不为0:
书中给出的大小 | VS2017测试的大小 |
---|---|
sizeof X:1 | sizeof X:1 |
sizeof Y:8 | sizeof Y:4 |
sizeof Z:8 | sizeof Z:4 |
sizeof A:12 | sizeof A:8 |
这跟编译器的实现有关,下面阐述原因(先解释书中的):
一个空的类如X,实际并不是空的,它有一个隐晦的1 byte,是编译器安插进去的一个char。这样使得类X的两个对象都有一个char的成员,使得这两个对象在内存中的地址不一样,用来区别这两个对象。
Y和Z的大小由三部分组成:
1、指向虚基类X的指针(编译器为了兼容,32位和64位系统基本都是4 bytes)
2、虚基类X subobject的1个char也出现在了Y,Z里,因为Y,Z都为空
3、数据对齐填充(alignment)。Y,Z目前是5 bytes,在大部分机器上,群聚的结构体大小会受到alignment的限制,使得它们能够更有效率地在内存中被存取。通常是需要填充到4 bytes的倍数,所以5 bytes会被填充3 bytes,最终为8 bytes
A的大小并不是简单的sizeof Y + sizeof Z,如果这样的话,A里面不就存了两份X吗?虚基类在每个派生类中只会有一份实体,class A的组成:
1、被大家共享的唯一一个class X实体,大小为1bytes
2、基类Y的大小减去“因虚基类X而配置”的大小,结果是4 bytes,同理Z也是4 bytes,总共8 bytes
3、class A自己的大小:0 byte
4、数据对齐填充(alignment),目前是9 bytes,经过填充后变为12 bytes
编译器的另一种策略(VS2017,VC++)把Y,Z中对空的虚基类视为成员,既然有了成员,Y,Z就不为空,也就不需要额外安插1 char,少了这个1 char,也少了数据对齐填充(alignment)的3 bytes,所以Y,Z为4 bytes。同理,类A中class X的实体1 byte被拿掉,于是数据对齐填充(alignment)的3 bytes也不需要了,所以A为8 bytes。
c++对象模型尽量以空间优化和存取速度优化的考虑来表现non-static data member,并且保持和C语言的struct兼容。它把数据直接存放在每一个class object之中,对于继承而来的non-static data member(不管是virtual或non-virtual base class)也是如此。
对于static的数据成员,则被放置在程序的一个全局数据段里(global data segement),它们并不属于某个对象,它们属于整个类。不管产生多少个对象,static的数据成员只有一份实体(即使没有任何object,static的数据成员也已经存在),但是一个template class的static数据成员的行为稍有不同,在7.1节有讨论。
每一个对象必须要有足够的空间容纳它所有的non-static数据成员,此外还要加上:
1、由编译器自动加上的额外数据成员,用来支持某些语言特性(主要是各种virtual特性,比如虚函数表指针vptr,还有上面提到过的指向虚基类的指针等)
2、数据对齐填充(alignment)的需要
3.1数据成员(Data Member)的绑定
//某个foo.h头文件
extern float x; //x在别处被定义,此处被引用。这里x是声明不是定义,定义是要分配存储空间的。
//Point3d.h文件
class Point3d{
public:
Point3d(float,float,float);
//问题:被回传和设定的x是哪一个x呢?
float X()const {
return x;}
void X(float new_x)const {
x=new_x; }
//...
private:
float x,y,z;
};
Point3d::X()返回的是类内部的x。
一个类对成员的分析,是直到整个class的声明都出现了才开始的(例如上述对Point3d::X()的分析会延迟至class声明的右大括号出现才开始)。因此,在一个inline member function躯体之内的一个data member绑定操作,会在整个class声明完成之后才发生。
3.2数据成员的布局
class Point3d{
public:
//...
private:
float x;
static List<Point3d*> *freeList;
float y;
static const int chunkSize=250;
float z;
};
non-static数据成员在对象中的排列顺序和其被声明的顺序一样,任何中间介入的static数据成员例如freeList和chunkSize都不会被放进对象的布局之中。在上述例子中,每一个Point3d的对象由3个float组成,次序是x,y,z,static数据成员存放在程序的数据段中,属于整个类,不属于某个对象。
c++ Standard要求,在同一个acess section(也就是private,public,protected等区段)中,数据成员的排列只需要满足“较晚出现的数据成员在对象中有较高的地址”即可,即在同一个acess section中,次序按照声明的次序,但是不一定连续,可能因为边界调整(alignment)或者编译器自动合成的一些内部使用的数据成员如虚函数表指针vptr介入这些数据成员到中间。(传统的编译器会把vptr放到所有明确声明的数据成员最后,当然也有编译器放在对象的最前端)
那不同的acess section的情况呢?
c++ Standard允许将多个acess section中的数据成员自由排列,不必在乎他们在类中出现的顺序。
class Point3d{
public:
//...
private: //一个acess section
float x;
static List<Point3d*> *freeList;
private: //另一个acess section
float y;
static const int chunkSize=250;
private //另一个acess section
float z;
};
上述类的对象的大小和组成和3.2节开头的那个一样,只是不同acess section之间的成员顺序由编译器的实现决定,例如y可以放在x前面,z可以放在y前面。但是当前大多数编译器会把多个acess section按照声明的次序合成成一个acess section。
3.3 数据成员的存取
我自己的理解:所谓存,就是member=xxx的形式,取,就是xxx=member的形式。
static成员
前面讲过,每一个static成员只存在一个实体,存放在程序的data segment数据段中,被视为一个global变量(只在class生命范围内可见)
当通过对象调用static成员时,会进行转化
Point3d origin,*pt=&origin;
//chunkSize是一个static成员,
//origin.chunkSize=250; //这样调用,内部转化为:
Point3d::chunkSize=250;
//pt->chunkSize=250; //这样调用,内部转化为:
Point3d::chunkSize=250;
//foobar().chunkSize=250; 这样调用,内部转化为:
(void)foobar();
Point3d::chunkSize=250;
下面是我在VS2017测试的代码:
#include "pch.h"
#include <iostream>
using namespace std;
class test {
public:
static const int t1=1; //t1是const,在类内初始化
static int t2;
};
int test::t2 = 0; //t2非const,在类外初始化
int main() {
test t;
cout << t.t1 <<" "<<t.t2<< endl; //合法
cout <<test::t1 <<endl;
cout << test::t2 << endl;
}
若获取一个static成员的地址,会得到一个指向其数据类型的指针,而不是指向其类成员的指针,因为static成员并不内含在一个类对象中。例如:
&Point3d::chunkSize会得到一个const int*的地址而不是Point3d::*类型的地址(指向类对象成员的指针)。
如果一个程序里定义了两个类,两个类都声明了一个static成员,且两个static成员同名,那么都存放在程序的数据段中时会引起同名冲突,编译器会暗中对每一个static成员编码,得到一个独一无二的程序识别代码,这种手法叫name-mangling,主要做两件事:
1、一种算法,推导出独一无二的名称
2、推导出的名称能够还原(万一编译系统(或环境工具)必须与使用者交谈,那么那些独一无二的名称可以轻易被推导回原来的名称)
non-static成员
non-static成员存在于每一个对象中,必须通过显示的或者隐式的对象才能对non-static成员进行存取。只要程序员在成员函数里直接处理non-static成员,隐式的对象就会出现,例如:
Point3d::translate(const Point3d &pt){
x+=pt.