1 基类:相对的。某个类可能既是基类(base)又是派生类(derived)。
- 要知道这个原则:基类可以保存派生类地址,比如
CerealPack breakfast; //派生类
Carton* pCarton {&breakfast};//直接基类保存了派生类
Box* pBox {&breakfast};//间接基类保存了派生类
Contents* pContents {&breakfast};
Carton carton;
pBox = &carton;
pCarton* p= new Box() 这是不对的,向下塑性。如果确实
2. 声明成员函数时没有用virtual,那就没有多态,即使函数在基类和派生类申明一样,也是两个实现。是静态绑定。
static binding or early binding
情形一,基类无法调用派生类的覆盖(其实没有virtual就没有多态的覆盖。基类和派生类中相同的函数会实现两遍)
class Box
{
protected:
double length {1.0};
double width {1.0};
double height {1.0};
public:
Box() = default;
Box(double lv, double wv, double hv) : length {lv}, width {wv}, height {hv} {}
// Function to show the volume of an object
void showVolume() const
{ std::cout << "Box usable volume is " << volume() << std::endl; }
// Function to calculate the volume of a Box object
double volume() const { return length * width * height; }
};
class ToughPack : public Box
{
public:
// Constructor
ToughPack(double lv, double wv, double hv) : Box {lv, wv, hv} {}
// Function to calculate volume of a ToughPack allowing 15% for packing
double volume() const { return 0.85 * length * width * height; }
};
int main()
{
Box box {20.0, 30.0, 40.0}; // Define a box
ToughPack hardcase {20.0, 30.0, 40.0}; // Declare tough box - same size
box.showVolume(); // Display volume of base box
hardcase.showVolume(); // Display volume of derived box
std::cout << "hardcase volume is " << hardcase.volume() << std::endl;//相当于 hardcase.ToughPack::volume()
Box *pBox {&hardcase};
std::cout << "hardcase volume through pBox is " << pBox->volume() << std::endl;//这是由于静态绑定,相当于 pBox->Box::volume()
}
输出:
Box usable volume is 24000
Box usable volume is 24000
hardcase volume is 20400
hardcase volume through pBox is 24000
情形二,基类隐藏了派生类
ToughPack hardcase {20.0, 30.0, 40.0};
Box& box=hardcase;
box.volume();//这里不会调用ToughPack的volume().
因为没有用virtual,就没有虚指针表。本来就是两个volume实现(box和toughpack的)。当声明是box引用时,是静态绑定,即在编译期间就已经确定是Box类了,运行期间也只能是Box了。
声明时加上virtual实现动态绑定
所有的方法(虚方法和非虚的)都真正的实现放在在code的text区域。但是虚方法都会出现在虚表中。指针指向真正的实现函数。
只要定义了大于一个的virtual的方法,会在类对象的内存中增加一个vtable,指向一个虚表。这个表里每一项指向真正的实现函数。只有virtual的才会出现在虚表中。
运行期可以动态设定,所有事动态绑定。
注意,虚表不包含非虚函数。
- 虚函数只要在基类加上virtual,派生类中会自动继承,都是虚函数。即使不写virtual.
- 注意必须使用相同签名,否则会等于是派生类中新加个函数,而不是重写虚函数了。使用override避免这种情况。
- 如果不希望以后的派生类中重写虚函数,用final。放在申明的后面。和override顺序无所谓。
如:void xxx() const override final
多态只有在指针或者引用时才能体现。
- 如果是按值传递,就会导致对象切片出现。动态类型被转换为基类类型。即静态绑定,失去动态属性。
比如将对象放到vector中:
vectorboxes;
boxes.push_back(Box());
boxes.push_back(ToughPack());//这时只能访问到基类了,由于静态绑定。参见14.1.6
指针的话,还需要管释放。所以用智能指针。
参考:
1. c++对象内存模型,注意不是类的布局,普通方法都是放在类的code 正文段数据区的
https://www.cnblogs.com/QG-whz/p/4909359.html
当布局时,子类与父类有各自的虚函数表。类的内存布局和对象内存布局不一样,本文主要讲的都是对象的内存布局,所贴的示例代码也是通过对象指针来进行内存访问而描述对象的内存布局的。
对于非继承下的对象模型:nonstatic 数据成员被置于每一个类对象中,而static数据成员被置于类对象之外。static与nonstatic函数也都放在类对象之外,而对于virtual 函数,则通过虚函数表+虚指针来支持。
虚函数存在时,会多一个虚指针指向虚表和type_info,做RTTI支持。
不考虑虚函数与虚继承,当数据都在同一个访问标识符下,C++的类与C语言的结构体在对象大小和内存布局上是一致的,C++的封装并没有带来空间时间上的影响。
- 如果不是虚继承,我们称作一般继承。
注意子类与父类拥有各自的一个虚函数表。在C++对象模型中,对于一般继承(这个一般是相对于虚拟继承而言),若子类重写(overwrite)了父类的虚函数,则子类虚函数将覆盖虚表中对应的父类虚函数。 - 对于虚继承,若子类overwrite父类虚函数,同样地将覆盖父类子物体中的虚函数表对应位置,而若子类声明了自己新的虚函数,则编译器将为子类增加一个新的虚表指针vptr,这与一般继承不同。示例:class B1 : virtual public B