派生类对象的内存布局
派生类对象的内存布局需要满足的要求是,一个基类指针,无论其指向基类对象还是派生类对象,通过它来访问一个基类中定义的数据成员,都可以使用相同的步骤。
单继承情况
考虑下面的情况:
class Base{...}
class Derive : public Base {...}
那么在Derive类的对象中,从Base继承来的数据成员,全部放在前面,与这些数据成员在Base类对象中放置的顺序保持一致,Derive类的新增数据成员,全部放在后面。如下图所示。如果出现了从Derive指针到Base指针的转换,例如:
Base* pba = new Base();
Derive* pd = new Derive();
Base* pbb = pd;
在pd赋值给pbb的过程中,指针值不需要改变。pba和pbb这两个Base类型的指针,虽然指向的对象具有不同的类型,但任何一个Base成员到该对象首地址都具有同的偏移量,因此使用Base指针的pba和pbb访问Base类中定义的数据成员时,可以采用相同的方式,而无需考虑具体的对象类型。
多继承情况
将pd赋值给pb1b指针时,与单继承时的情况相似,只需要把地址复制一遍即可。但将pd赋值给pb2b指针时,则不能简单地执行地址复制操作,而应当在原地址的基础上加一个偏移量,使pb2b指针指向Derive对象中Base2成员的首地址。这样,对于同为Base2类型指针的pb2a和pb2b来说,它们都指向Base2中定义的,以相同方式分布的数据成员。
通过上而的介绍,我们应该认识到一个与直观不符的结论:指针转换并非都保持原先的地址不变,地址的算述运算可能在指针转换时发生。但这又不是简单的地址算术运算,因为如果Derive指针的值为0,则转换后Base2指针值也应为0。因此,在将Derive指针转换为 Base2类型指针,执行的操作是,先判断原指针是否为0,如果是0,则以0作为转换后的指针值,否则以原地址加上一个偏移量后得到转换后的指针值。
多继承的情况比单继承稍微复杂一些,考虑下面的情况:
class Base1 {...};
class Base2 {...};
class Derive : public Base1, public Base2 {...};
Derived类继承了Base1和Base2类,在Derive类的对象中,前面依次存放的是从Base1类和Base2类继承而来的数据成员,其顺序与它们在Base1类和Base2类的对象中放置的顺序一致,Derive类新增的数据放在它们的后面,如下图所示。如果出现了从Derive指针到Base1或Base2指针的隐含转换,例如:
Base1 * pb1a = new Base1();
Base2 * pb2a = new Base2();
Derive* pd = new Derive();
Base1* pb1b = pd;
Base2* pb2b = pd;
虚拟继承情况
虚继承的情况更加复杂。考虑下面的继承关系:
class Base0 {...};
class Base1: virtual public Base0{...};
class Base2: virtual public Base0{...};
class Derive: public Base1, public Base2{...};
一种比较容易理解的内存布局是,在Base1类和Base2类对象中都增加一个隐含的Base0指针,它指向Base0中定义的数据的首地址。Derived类同时继承了Base1和Base2类数据成员,因此要把两个类中的隐含指针分别继承下来,但由于Derive类中Base0的类数据成员只有一份,因此Derive类对象中中的这两个隐含指针指向相同的地址。通过Base1和Base2类型指针访问Base0类的数据成员时,都通过指针来间接访问 。
字节对齐
编译器用'N'来设置数据的对齐方式。默认32位OS对齐字节是4,64位对齐字节是8。'N'有可能影响结构体内部成员的对齐位置,以及结构体整体大小。 对齐规则:
如果类包含虚函数,则对象最前面会有占8字节的地址,指向一个虚表,该表中包含该类每个虚函数的地址。
每个成员变量在其结构体内的偏移量都是“MIN(对齐字节,成员变量类型的大小)”的倍数。
如果有嵌套结构体,那么内嵌结构体的第一个成员变量在外结构体中的偏移量,是“MIN(对齐字节,内嵌结构体中那个数据类型大小最大的成员变量)”的倍数。
整个结构体的大小要是“MIN(对齐字节,这个结构体内数据类型大小最大的成员变量)”的倍数。如果有内嵌结构体,那么取“MIN(对齐字节,内嵌结构体中数据类型大小最大的成员变量)”作为计算外结构体整体大小的依据。
C++中多态实现的三个条件
必须存在一个继承体系结构 继承体系结构中的一些类必须具有同名的virtual成员函数(virtual是关键字) 至少有一个基类类型的指针或基类类型的引用。这个指针和引用可以对virtual成员函数进行调用
包含多态
指针实现多态
class Shape {
public:
virtual float getArea() = 0;
virtual ~Shape(){
printf("Shape is destroyed!\n");
}
virtual void show(){
printf("I am a Shape!\n");
}
};
class Rectangle: public Shape {
public:
Rectangle(int width, int height):width(width),height(height){}
float getArea(){
return width * height;
}
void show(){
printf("I am a Rectangle!\n");
}
protected:
int width, height;
};
class Triangle: public Shape {
public:
Triangle(int base, int height):base(base),height(height){}
float getArea(){
return base * height / 2;
}
protected:
int base, height;
};
class Circle: public Shape {
public:
Circle(int radius):radius(radius){}
float getArea(){
return M_PI * radius * radius;
}
~Circle(){
printf("Circle is destroyed!\n");
}
protected:
int radius;
};
void test(Shape* sp){ //此处,必须是指针或引用,否则不能实现多态
printf("area=%f\n", sp->getArea());
sp->show(); //ap->Shape::show();仍可访问基类成员
}
int main(){
Shape* rp = new Rectangle(3, 3);
Shape* tp = new Triangle(2, 5);
Shape* cp = new Circle(1);
test(rp);
test(tp);
test(cp);
delete rp;
delete tp;
delete cp;
}
纯虚函数和抽象类
有时,在基类中某些函数,无法给出有意义的实现。对于这种在基类中无法实现的函数,能否在基类中只说明函数原型,用来统一接口,而在派生类中再给出具体实现呢?在C++中提供了纯虚函数来实现这一功能。
它与一般虚函数的原型在书写格式上,就在于后面多个“= 0”。声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。纯虚函数的函数体由派生类给出
纯虚函数不同于函数体为空的虚函数;纯虚函数根本就没有函数体。前者所在的类是抽象类,不能直接实例化,而后者所在的类是可以实例化的。它们的共同特点是都可以派生出新类,然后在新类中给出虚函数的实现,可以实现多态。
带有纯虚函数的类是抽象类,抽象类的主要作用是建立一组公共的接口,而本身并不去实现,由派生类实现。
抽象类派生出新类后,如果派生类给出所有纯虚函数的实现,这个派生类就可以实例化,否则仍然是抽象类。抽象类不能实例化。