六一的部落格


关关难过关关过,前路漫漫亦灿灿。



通常情况, 如果未使用某个函数, 可以只有该函数的声明, 无定义

必须为每一个虚函数提供定义


通过引用和指针调用虚函数时, 在运行时才被解析

当某个虚函数通过指针或引用调用时, 编译器产生的代码直到运行时才能确定应该调用哪个版本的函数

当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时, 在编译时就可以确定调用的虚函数版本

对非虚函数的调用在编译时进行绑定; 通过对象进行的函数调用(包括虚函数)也在编译时绑定

归根结底在于动态类型与静态类型是否相同: 执行动态类型的虚函数版本


C++的多态性

polymorphism

我们把具有继承关系的多个类型成为多态类型, 我们能使用这些类型某些接口(虚函数)的多种形式, 而无需在意他们的差异

引用和指针的静态类型不同于动态类型这一事实是C++语言支持多态性的根本所在


派生类中的虚函数

  1. 要求形参类型与基类函数完全一致
  2. 返回类型
    • 与基类函数一致
    • 如果基类虚函数的返回类型为类本身的指针或引用, 派生类的返回类型可以是派生类的指针和引用, 要求存在派生类到基类的类型转换

      即要求派生为public
 1struct B
 2{
 3    virtual void f1(int) const;
 4    virtual void f2();
 5    void f3();
 6};
 7
 8struct D1 : B
 9{
10    void f1(int) const override;            // 正确
11    void f2(int) override;                  // 错误:形参列表不一致
12    void f3() override;                     // 错误:f3不是虚函数
13    void f4() override;                     // 错误:f4不是虚函数
14};

可以在派生类中定义与基类虚函数同名的函数, 但形参列表不同的成员函数

可能有时候我们就是想覆写基类虚函数, 但形参列表写错了, override 关键字的存在保证了程序员的意图: 如果我们使用 override 标记了某个函数, 但该函数并不是虚函数, 编译器将报错


使用final标记虚函数

  1. 基类B中f1为虚函数
  2. 在派生类D2中使用final标记f1
  3. D3派生自D2, D2无法覆写f1
 1struct B
 2{
 3    virtual void f1(int) const;
 4    virtual void f2();
 5    void f3();
 6};
 7
 8struct D2 : B
 9{
10    void f1(int) const final;           // 覆写虚函数f1, 未使用override关键字; 使用final标记
11};
12
13struct D3 : D2
14{
15    void f2();
16    void f1(int) const;                 // 错误:欲覆写虚函数f1, 未使用关键字; 但由于D3派生自D2, 而D2已将f1标记为final, 即不允许D2的派生类覆写f1
17};

覆写虚函数时未使用 override 关键字, 为隐式覆写


final与override关键字的位置

  1. 在形参列表之后给出 override 关键字
  2. 如果虚函数为const成员函数, 关键字在 const 之后
  3. 如果虚函数为引用, 关键字在引用限定符之后
  4. 如果使用尾置返回类型, 关键字在尾置返回类型之后

虚函数与默认实参

虚函数可以拥有默认实参

如果某次函数调用使用了默认实参, 实参时由本次调用的静态类型决定:

  • 使用基类的指针和引用调用函数时, 使用基类中定义的默认实参
  • 如果虚函数使用默认实参, 则基类与派生类中定义的默认实参最好一致

回避虚函数

使用指针和引用调用虚函数时, 不希望发生动态绑定

使用作用域运算符 :: 给出虚函数的某个特定版本, 在编译时就能确定绑定

1double undiscounted = baseP->Quote::net_price(42);

通常情况下, 只有成员函数或友元中的代码才需要使用作用域运算符来回避虚函数机制

派生类的虚函数需要调用基类的虚函数版本, 此时, 基类的虚函数版本通常完成了继承层次中所有类型都要做的共同任务, 而派生类的虚函数版本负责与派生类本身相关的操作

如果没有作用域运算符, 会发生无限递归


虚函数


通常情况, 如果未使用某个函数, 可以只有该函数的声明, 无定义

必须为每一个虚函数提供定义


通过引用和指针调用虚函数时, 在运行时才被解析

当某个虚函数通过指针或引用调用时, 编译器产生的代码直到运行时才能确定应该调用哪个版本的函数

当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时, 在编译时就可以确定调用的虚函数版本

对非虚函数的调用在编译时进行绑定; 通过对象进行的函数调用(包括虚函数)也在编译时绑定

归根结底在于动态类型与静态类型是否相同: 执行动态类型的虚函数版本


C++的多态性

polymorphism

我们把具有继承关系的多个类型成为多态类型, 我们能使用这些类型某些接口(虚函数)的多种形式, 而无需在意他们的差异

引用和指针的静态类型不同于动态类型这一事实是C++语言支持多态性的根本所在


派生类中的虚函数

  1. 要求形参类型与基类函数完全一致
  2. 返回类型
    • 与基类函数一致
    • 如果基类虚函数的返回类型为类本身的指针或引用, 派生类的返回类型可以是派生类的指针和引用, 要求存在派生类到基类的类型转换

      即要求派生为public
 1struct B
 2{
 3    virtual void f1(int) const;
 4    virtual void f2();
 5    void f3();
 6};
 7
 8struct D1 : B
 9{
10    void f1(int) const override;            // 正确
11    void f2(int) override;                  // 错误:形参列表不一致
12    void f3() override;                     // 错误:f3不是虚函数
13    void f4() override;                     // 错误:f4不是虚函数
14};

可以在派生类中定义与基类虚函数同名的函数, 但形参列表不同的成员函数

可能有时候我们就是想覆写基类虚函数, 但形参列表写错了, override 关键字的存在保证了程序员的意图: 如果我们使用 override 标记了某个函数, 但该函数并不是虚函数, 编译器将报错


使用final标记虚函数

  1. 基类B中f1为虚函数
  2. 在派生类D2中使用final标记f1
  3. D3派生自D2, D2无法覆写f1
 1struct B
 2{
 3    virtual void f1(int) const;
 4    virtual void f2();
 5    void f3();
 6};
 7
 8struct D2 : B
 9{
10    void f1(int) const final;           // 覆写虚函数f1, 未使用override关键字; 使用final标记
11};
12
13struct D3 : D2
14{
15    void f2();
16    void f1(int) const;                 // 错误:欲覆写虚函数f1, 未使用关键字; 但由于D3派生自D2, 而D2已将f1标记为final, 即不允许D2的派生类覆写f1
17};

覆写虚函数时未使用 override 关键字, 为隐式覆写


final与override关键字的位置

  1. 在形参列表之后给出 override 关键字
  2. 如果虚函数为const成员函数, 关键字在 const 之后
  3. 如果虚函数为引用, 关键字在引用限定符之后
  4. 如果使用尾置返回类型, 关键字在尾置返回类型之后

虚函数与默认实参

虚函数可以拥有默认实参

如果某次函数调用使用了默认实参, 实参时由本次调用的静态类型决定:

  • 使用基类的指针和引用调用函数时, 使用基类中定义的默认实参
  • 如果虚函数使用默认实参, 则基类与派生类中定义的默认实参最好一致

回避虚函数

使用指针和引用调用虚函数时, 不希望发生动态绑定

使用作用域运算符 :: 给出虚函数的某个特定版本, 在编译时就能确定绑定

1double undiscounted = baseP->Quote::net_price(42);

通常情况下, 只有成员函数或友元中的代码才需要使用作用域运算符来回避虚函数机制

派生类的虚函数需要调用基类的虚函数版本, 此时, 基类的虚函数版本通常完成了继承层次中所有类型都要做的共同任务, 而派生类的虚函数版本负责与派生类本身相关的操作

如果没有作用域运算符, 会发生无限递归