—— 《21天学通C++》
1、没有继承关系的类,分配完内存后,优先给虚表指针赋值,再列表初始化,再构造函数。有继承关系的类,分配完内存后,先基类构造,后续同上。
2、将函数声明(只能是类中的)为虚函数。放在函数声明最前。告诉编译器,调用复函的版本。将子类对象视为父类,并执行子类的函数实现。
3、使用new在堆中实例化的派生类对象,如果赋值给基类指针,并通过该指针调用delete,将不会调用派生类的析构函数。—— 内存泄露 (析构函数声明为虚函数)
4、假设基类有虚函数、子类覆盖了某些虚函数:编译器为实现了虚函数的基类和派生类,分别创建各自的虚函数表(Virtual Function Table, VFT)。实例化这些对象时,将创建一个隐藏的指针,指向虚函数表。VFT可以看做是一个包含函数指针的静态数组,每个指针都指向一个相应的虚函数(派生类:有指向父类的虚函数的;也有指向自己虚函数的指针,包括覆盖基类的函数)。VFT在编译阶段建立。VFT指针(虚表指针)放在对象的内存空间中最前面的位置。
有了虚函数表,通过指针、引用传递(指向派生类的基类)传递的指针依然实实在在的是指向派生类,因此可以调用派生类虚函数表内存的函数指针。
5、可以通过类型转换运算符dynamic_cast确定Base指针指向的是基类还是派生类(RTTI, RunTime Type Identification)
6、不能被实例化的基类(不能作为函数参数、不能创建对象、不能作为返回类型) —— 抽象基类(ABC)。需要声明至少一个纯虚函数。可以理解为抽象接口。
class AbstractBase {
public:
virtual void DoSth() = 0; // 告诉编译器,子类必须实现这个方法
};
可以声明抽象类指针、引用
7、虚继承解决菱形问题。例:《21天学通C++》例子
Platypus具有多个Animal实例,占用多份内存。假设Animal里有一个int age成员变量,则鸭嘴兽存在三个不同的age。所以,Mammal、Bird、Reptile应该虚继承Animal
class Mammal : public virtual Animal {
...
};
class Platypus final : public Mammal, public Bird, public Reptile {
...
};
从Base使用virtual派生出Derived1、 Derived2类时,意味着从Derived1、 Derived2再派生出新的类Derived3时,只包含一个Base。
8、override —— 表明覆盖意图的限定符
class Fish {
public:
virtual void Swim() {
...
}
};
class Tuna : public Fish {
void Swim() const {
...
}
};
此时子类函数并不会覆盖基类的虚函数。const 导致两个函数的特征不同。使用override来核实是否真的覆盖住了父类的虚函数,而不是重新写了一个不同的函数。
class Tuna : public Fish {
void Swim() const override { // 此时编译出错,删除const可通过编译
...
}
};
override让编译器做的检查:
a. 基类函数是否有虚函数?
b. 基类中虚函数特征与派生类override的函数是否相同?
9、被声明为final的虚函数,在子类中不能再进行覆盖
class Tuna : public Fish {
void Swim() override final {
...
}
};
class BluefinTuna final : public Tuna {
public:
void Swim() { // 编译出错
...
}
};
class Failed : public BluefinTuna { // 编译出错
};
10、可将赋值构造函数声明为虚函数吗 —— 不能。多态是运行时状态,而构造函数只能创建固定类型对象,不具备多态性。
class Fish {
public:
virtual Fish *Clone() const = 0;
};
class Tuna : public Fish {
public:
Tuna *Clone() const { // TODO: 这里返回值写Tuna* 还是Fish *???
return new Tuna(*this);
}
};
11、基类总应该包含一个虚析构函数
12、虚函数表与虚基类表 虚表和虚基表到底有哪些区别?