class X{};
class Y : virtual public X{};
class Z : virtual public X{};
class A : public Y, public Z{};
对每一个类进行大小计算的结果:
sizeof X yielded 1
sizeof Y yielded 8
sizeof Z yielded 8
sizeof A yielded 12
几个结论:
- 类大小的计算,遵循结构体的对齐原则。
- 类的大小只与普通数据成员有关,与成员函数和静态成员无关。静态数据成员之所以不计算在类的对象大小内,是因为它属于这个类的所有对象,被它们共享;静态数据存储于内存的全局区。
- 由于虚函数表的指针,所以虚函数对类的大小有影响。
- 由于虚基表指针,所以虚继承对类的大小有影响。
空类的大小
空类对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而,C++标准规定,凡是一个独立的(非附属)对象都必须具有非零大小。所以空类对象的大小为1。因为new
需要分配不同的内存地址,不能分配内存大小为 0 的空间
注意如果发生空类的继承,那么空类的一个字节不会计入派生类的大小中。
如果空类对象是一个类的 data member ,那么一个字节会记入类的大小,并且要考虑对齐。
含有虚函数成员的类
虚函数是通过一张虚函数表实现的。编译器保证虚函数表的指针存在对象实例中最前面的位置。(这是为了保证正确取得虚函数的偏移量)
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
int a;
};
Base b;
Base 的实例对象 b 的大小是 4+8 对齐得16。
基类含有虚函数的继承
class Derived: public Base
{
public:
virtual void f1() { cout << "Derived::f1" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
Derived d;
d 的成员存放如下:
- 虚函数按照声明顺序存在虚函数表中;
- 基类的虚函数在派生类的虚函数之前。
当发生虚函数override时:
class Derived: public Base
{
public:
virtual void f() { cout << "Derived::f" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
Derived d;
d的成员存放如下:
- 覆盖的
f()
函数被放到了虚函数表中原来基类虚函数的位置; - 没有被覆盖的函数保持不变;
- 派生类大小仍是基类和派生类非静态数据成员+一个vptr指针的大小。
虚继承
虚继承得来的派生类都有指向基类的指针,而派生类和基类对象共享虚表指针。
class A {
int a;
virtual void myfuncA(){}
};
class B:virtual public A{
virtual void myfunB(){}
};
class C:virtual public A{
virtual void myfunC(){}
};
class D:public B,public C{
virtual void myfunD(){}
};
sizeof(A)=16 , sizeof(B)=24 , sizeof(C)=24 , sizeof(D)=32
A对象的大小是int
+ vptr对齐后得16。
B、C因为是虚继承,所以大小是 sizeof(A)+指向虚基类的指针。B、C对象虽然加入自己的虚函数,但是虚表指针是和基类共享的,没有自己的虚表指针。
D对象由于B、C都是虚继承的,所以D只包含一个A的副本,所以D大小为A + B指向虚基类的指针 + C指向虚基类的指针。
整个继承链中只有一个 a 的实例——基类 A 中的,其余都是依靠bptr去指示的。
3.1 数据成员的绑定
一个类内声明并定义的成员函数,在整个 class 声明未被完全看见之前,是不会评估求值的。
extern int x;
class Point3d{
public:
...
//对函数本体的分析将延迟,直到class声明的右大括号出现才开始
//因此 此处的 x 是 extern 的
float X()const {return x;}
private:
float x;
};//分析在这里开始
但是对于参数不适用,参数在函数声明的时候就发生绑定。
3.2 数据成员的布局
非静态数据成员在类对象中的排列顺序和被声明的顺序一致。任何中间介入的 static 成员变量都不会放进对象的布局中,而是放在程序的数据段中,和类对象无关。
3.4 继承与数据成员
C++保证出现在派生类中的 base class subobject 有其完整原样性。
class Concrete1{
private:
int val;
char bit1;
};
class Concrete2:public Concrete1{
private:
char bit2;
};
class Concrete3:public Concrete2{
private:
char bit3;
};
继承后的数据不会填补在基类的尾巴后面,而是对齐后的基类大小后面。
HINT:在自己编写代码的时候发现这三种对象的大小都是8……..可能是编译器优化结果
如果使用基类指针绑定派生类对象,那么派生类数据就会和基类子对象捆绑在一起填补空间,造成空间错误。
多重继承
多重继承关系图如下:
class Point2d{
public:
//virtual function
protected:
float _x,_y;
};
class Point3d:public Point2d{
protected:
float _z;
};
class Vertex{
public:
//virtual function
protected:
Vertex *next;
};
class Vertex3d:public Point3d,public Vertex{
protected:
float mumble;
};
对一个派生类对象,将其地址指定给一个最左端 base class 指针,情况和单一继承一样。因为二者指向相同的地址,因为起始基类的资源在后续派生类中,都是处于最开始的位置,所以后续派生类和第一个基类的地址是一样的。
对于第二个和后继的基类指针绑定到派生类对象,需要修改地址:加上或减去基类 subobject 的大小。
e.g.
Vertex3d v3d;
Vertex *pv;
pv = &v3d;
//需要内部转化
pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
然而如果有如下绑定,那么操作就不能只有简单转换:
Vertex3d *pv3d;
Vertex *pv;
pv = pv3d;
//需要考虑如果pv3d为0,那么pv将直接获得sizeof(Point3d)的值,所以要有条件测试
pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d) : 0;
上面二者的区别在于:一个是Vertex3d
的对象,一个是指针(指针可以为0)。
虚拟继承
虚拟继承的挑战在于:将 istream 和 ostream 各自维护的一个 ios 子对象折叠为一个 iostream 维护的单一子对象。
一般策略:如果一个类内含一个或多个虚基类子对象,那么将被分割为两部分:一个不变区域和一个共享区域。不变区域总是拥有固定的偏移量,可以被直接存取;共享区域表现的是虚基类子对象,这部分数据的位置会因为每次派生操作而变化。
为了存取类间共享部分的数据,编译器在每个派生类中安插指向虚基类的指针。
3.6 指向数据成员的指针
取数据成员地址的话,传回的值总是比偏移量多1(我自己的编译器没有这种行为)。因为需要区分没有指向任何数据成员的指针和指向第一个数据成员的指针。
float Point3d::*p1=0;
float Point3d::*p2 = &Point3d::_x;
为了区分p1和p2,每一个真正的成员偏移量都要加上1。