类的继承
类可以通过继承或者派生来引入“是一个”的关系
struct Base{
};
struct Derive : public Base{
};
关于继承,我们需要关注以下几点
- 通常使用
public
继承(struct
缺省情况下使用public
继承而class
缺省情况下使用private
继承) - 继承部分不是类的声明
- 可以使用基类的指针或者引用来指向派生类的对象
int main() { Derive d; Base * ptr = &d; Base & ref = d; }
- 静态类型VS动态类型
protect
限定符:派生类可以访问
类的派生会形成嵌套域,我们主要需要关注以下几点
- 派生类所在域位于基类内部
- 派生类中的名称定义会覆盖基类
- 使用域操作符显式访问基类成员
- 在派生类中调用基类的构造函数
struct Base{ Base(int){} }; struct Derive : public Base{ Derive(int a): Base(a) { } };
虚函数
我们可以使用虚函数与指针(引用)的配合来实现动态绑定。虚函数使用virtual
关键字引入,非静态、非构造函数可以声明为虚函数,虚函数会引入vtable
结构
> vtable
可以用来实现基类指针(引用)在运行期动态转换为派生类指针(引用)
虚函数可以在基类中定义,相当于引入了缺省的逻辑
struct Base{
virtual void fun(){}
};
struct Derive : Base{
};
它可以在派生类中进行重写(override
)
struct Base{
virtual void fun(){}
};
struct Derive : Base{
void fun(){}
};
在重写过程中函数签名通常来说保持不变(返回类型可以是原始返回指针/引用类型的派生指针/引用类型)
重写过程中虚函数的特性保持不变,也就是依然是虚函数并存储在虚函数表中
我们可以通过=0
来声明纯虚函数,相应地构造抽象基类
struct Base{
virtual void fun() = 0 ;
};
struct Derive : Base{
void fun(){}
};
在C++开始,我们可以使用
override
关键字来表明这个函数为重写函数,编译器会检查对应的被重写的虚函数的存在以保证逻辑的正确性和避免书写错误
有虚函数所引入的动态绑定属于运行期行为,与编译期行为有所区别
- 虚函数的缺省实参只会考虑静态类型
- 虚函数的调用成本要高于非虚函数,在C++11中引入了
final
关键字来告诉编译器对应的函数或者类不会被继续重写或者继承从而允许编译器引入一些特定的优化来提升性能 - 需要使用基类的指针或者引用来进行动态绑定而不能使用单纯的基类的类型
- 在构造函数中调用虚函数要小心(基类的构造函数一定调用的是基类的虚函数而不是派生类的)
- 派生类的析构函数会隐式的调用基类的析构函数
- 通常来说要将基类的析构函数声明为
virtual
的(这是为了确保使用基类的指针删除派生类的对象时会调用派生类的析构函数,但是如果没有这个需求,也可以不将基类的析构函数声明为virtual
) - 在派生类中可以修改虚函数的访问权限
继承与特殊函数
关于一些特殊函数在继承中的应用,我们需要注意以下几点
-
派生类合成的
- 缺省构造函数会隐式调用基类的缺省构造函数
- 拷贝构造函数将隐式调用基类的拷贝构造函数
- 赋值函数将隐式调用基类的赋值函数
-
派生类的析构函数会调用基类的析构函数
-
派生类的其他构造函数将隐式调用基类的缺省构造函数
-
所有特殊成员函数在显式定义时都可能需要显式调用基类相关成员函数
-
构造与销毁顺序
- 基类的构造函数会先调用,之后才涉及到派生类中数据成员的构造
- 派生类中的数据成员会先被销毁,之后才涉及到基类的析构函数的调用