单继承与Data Members
在C++的继承模型中,base class members和derived class members的排列顺序并为强制规定。不同的编译器可能有不同的布局安排。大部分情况下,base class members会安排在derived class members的前面,但base class是virtual base class(base class存在virtual function)除外。
只有继承没有多态
考虑如下程序:
class Point2d
{
public:
Point2d(float x = 0.0, float y = 0.0)
:x(x),y(y){}
void setX(float newX)
{
x = newX;
}
void setY(float newY)
{
y = newY;
}
float getX()
{
return x;
}
float getY()
{
return y;
}
void operator+=(const Point2d& rhs)
{
x += rhs.getX();
y += rhs.getY();
}
protected:
float x,y;
};
class Point3d : public Point2d
{
public:
Point3d(float x = 0.0,float y = 0.0,float z = 0.0)
:Point2d(x,y),z(z){}
void setZ(float newZ)
{
z = newZ;
}
float getZ()
{
return z;
}
void operator+=(const Point3d& rhs)
{
Point2d::operator +=(rhs);
z += rhs.getZ();
}
protected:
float z;
};
这样单一继承且没有virtual function的数据布局如下:
加上多态(加上虚函数)
对于Point2d而言,它只是特殊的Point3d,其中的z等于0。这样Point2d,Point3d的重新设计如下,新增部分以红色标出
class Point2d
{
public:
Point2d(float x = 0.0, float y = 0.0)
:x(x),y(y){}
void setX(float newX)
{
x = newX;
}
void setY(float newY)
{
y = newY;
}
float getX()
{
return x;
}
float getY()
{
return y;
}
virtual void setZ(float){}
virtual float getZ()
{
return 0.0;
}
virtual void operator+=(const Point2d& rhs)
{
x += rhs.getX();
y += rhs.getY();
}
protected:
float x,y;
};
class Point3d : public Point2d
{
public:
Point3d(float x = 0.0,float y = 0.0,float z = 0.0)
:Point2d(x,y),z(z){}
void setZ(float newZ)
{
z = newZ;
}
float getZ()
{
return z;
}
void operator+=(const Point2d& rhs)
{
Point2d::operator +=(rhs);
z += rhs.getZ();
}
protected:
float z;
};
说下虚函数的实现,在大部分编译器中虚函数是通过virtual table和virtual table pointer实现的,二者可以简写为vtbl和vptrs。vtbl通常以函数指针实现。在程序中凡是声明(或者继承)了虚函数者,都有自己的一个vtbl,而其中的值就是该class的各个虚函数实现体的指针。而vptrs的作用就是提供执行期的链接,使每一个object能够找到相应的vtbl。关于vptrs到底放在class object的哪里好?(一般是在class object的最前面或者最后面),不同的编译器有不同的安排。此时class object的布局如下(此图是把vptr放在base class的尾端):
由上图可见base class与derived class之间的转换可以很自然的进行,因为base class和derived
class的object都是从相同的地址开始,唯一的差异只是derived
object比较大,用以容纳自己的non-static data members。如进行一下操作:
Point3d p3d;
Point2d *p = &p3d;
把一个derived class object指定给base class的指针或者reference,该操作并不需要编译器去修改地址,它可以自然地发生。
多重继承
加如一个新的类Vertex如下:
class Vertex
{
public:
//拥有若干virtual接口,所以Vertex对象中会有vptr
protected:
Vertex*
next;
}
class Vertex3d: public Point3d.public Vertex
{
public:
// ….
protected:
float
mumble;
}
现在Point2d,Point3d,Vertex,Vertex3d的继承关系如下
多重继承的问题主要发生于derived class objects和其第二或后继的base class
objects(如本例中的Vertex3d到Vertex之间的转换就属于这中情况)之间的转换,而不是像单重继承那样的转换或是经由其支持的virtual
function做的转换(暂且可以不考虑)。
对一个多重派生对象,将其地址指定给第一个base class的指针情况和单一继承时相同,因为二者都是指向相同的起始地址,需要付出的成本只有地址的指定操作。至于第二个或后继的base class的地址指定操作,需要将地址修改为加上(或减去)介于中间的base class
subobject的大小。
上例的数据布局如下图:
如下例:
Vertex3d v3d;
Vertex* pv;
Point2d* p2d;
Point3d* p3d;
则下面的操作
pv = &v3d; //相当于单重继承的转换
编译器内部的转换可能是这样的:
pv =(Vertex*)( ((char*)&v3d) +
sizeof(Point3d) ); //加上sizeof(Point3d的原因是Vertex3d继承了Point3d
而下面的指定操作
p2d = &v3d;
p3d = &v3d;
都只需要简单地拷贝其地址就行了。
C++ Standard 中并未要求base class
Point3d,和Vertex有特定的排列次序。
虚拟继承
多重继承中会碰到如下图的继承关系(钻石型继承),C++中的ios,istream,ostream,iostream就是典型的钻石型继承。
这样的继承出现时, base class 的data members会在derived
class object中都出现,这样derived class object就会出现同样的数据或者function。此时,让base class成为virtual(虚基类),可以消除这样的冗余现象,唯一需要付出的成本是derived
class object内有两指针指向虚基类(如:本例的类A)。
若类A没有任何虚函数,D对象的内存布局可能如下:
D object
B data members |
Point to virtual base class |
C data members |
Point to virtual base class |
D data members |
A data members |
若类A存在任何虚函数,D对象的内存布局可能如下:
B data members |
vptr |
Point to virtual base class |
C data members |
vptr |
Point to virtual base class |
D data members |
vptr |
A data members |
Virtual
Destructor
最后想说下virtual destructor,我们经常看到基类会把析构函数写成虚函数,这样写还是有道可循的。
如下程序
class Base {
public:
virtual Base() {};
};
class Derived : public
Base {};
Base* pd = new
Derived;
但是,当你写下delete pd;时,如果base class 的destructor不是virtual的,其结果是未定义的。实际上执行时通常会发生的是derived对象的成分没有被销毁,于是会造成诡异的“局部销毁”对象,这会形成资源泄露。
上面如有不足不对的地方,望各路大神补充指正。