一、不含多态的继承
即基类里没有虚函数。这时候派生类成员的存取时间并没有增加,但是空间上会有浪费。比如有如下两个类
class X {
int a;
char b;
};
class Y : public X {
char c;
};
一个X对象占用4+1+3(padding, 假设4字节对齐) = 8字节。而一个Y对象并不是占用 4+1+1+2(padding)=8个字节,而是占用4+1+3(padding)+1+3(padding)=12个字节。编译器之所以如此设计,是为了使派生类对象中基类对象的内存结构和原始的基类对象一致,这种一致性保证了通过指针复制对象时不出错。如果按照前一种内存布局,会产生错误,比如:
X *px = new X(), *py = new Y();
*py = *px;
将一个基类指针的内容赋给一个派生类对象,基类对象8字节大,前4个字节是int a,第5个字节为char b,后面全是padding(无效数据),但是派生类对象的第六个字节是char c,所以这样的复制会将py->c给抹掉,造成数据丢失。
所以,编译器总是把派生类新加入的成员变量直接附到基类后面。由于内存对齐,每多家一层继承最多浪费3个字节,也不算很大的开销。
二、加上多态
多态是OO的一个重要概念,在C++里则表现为函数重载、虚函数和虚继承。
如果基类里加了虚函数,那么存取时间和存储空间都会有额外的开销。以一下两个类为例:
class Point2d {
public:
virtual int z() { return 0; } //预留接口, 2d的点z轴坐标为0是合理的
protected:
int _x, _y;
};
class Point3d : Public Point2d {
public:
int z() { return z; }
protected:
int _z;
};
Point3d的额外开销具体如下:
1.导入一个和Point2d有关的”虚函数表(virtual table)”,用来存放它所声明的所有虚成员函数的地址;再加上一两个slot,用于支持运行时“运行时类型信息(RTTI)”。
2.在每一个类对象中导入一个vptr,提供执行期链接,使每一个对象都能找到相应的虚函数表。
3.加强每一个构造函数,使他能够初始化vptr指向对应的虚函数表。
4.加强析够函数,释放vptr。
对于一个简单的Point类来说,这个开销显得非常大了。如
果程序中有数量可观的Point对象,那么这个虚函数的存在将严重拖累程序执行的效率。
所以,在使用虚函数之前,一定要现评估一下为虚函数所带来的设计上便利付出相应的效率代价是否值得。