C++进阶: override和final说明符-----继承2中重写的确认官和刹车(制动器)
文章目录
- 引子:
- 指向派生类对象的基类指针(引用)
- 对象分片
- 虚函数----->(在相关函数前面加virtual 关键字)
 
- 重写说明符:override和final
- override :显式说明覆盖:确保我确实重写了我认为已经重写的函数
- final: 显示拒绝覆盖:进一步重写都应该被视为错误
- 特殊的重新:协变返回类型
 
- 总结:
引子:
指向派生类对象的基类指针(引用)
上次我们在虚函数1中说到某些情况下为了防止给每个派生类都重载仅仅传入参数不同,但功能一样的函数, 利用基类指针(引用)可以指向派生类对象这一准则将传入参数为派生类对象的指针(引用) 只统一为一个基类的指针(引用)
 
对象分片
这个设想非常美好, 可走到这里时又一个拦路虎出现了, 它就是对象分片, 什么意思? 我们知道继承基类的派生类不是简单的复制了基类的成员(数据成员和成员函数), 而是它有两部分组成, 一部分是基类的, 另一部分是派生类自己的. 而当基类的指针指向派生类对象时,只能访问到派生类的基类部分, 没法访问到自己派生类的部分. 形象地把一个完整的派生类对象分割成能访问的和不能访问的这两部分了.

 这显然不是我们想要的结果, 好在虚函数解决了这个问题:
虚函数----->(在相关函数前面加virtual 关键字)
它做了两件事情:
- 可以跨域让原本不能访问派生对象的基类指针变得可以访问派生类成员
- 编译器会在派生类的派生部分与基类部分比较, 视优化情况能重写(或者覆盖)基类同签名和返回类型的成员函数
同签名: 函数名称、参数类型以及是否为 const
注意:
- 1构造函数永不为虚函数,
- 2 一旦成员函数添加virtual 变为虚函数, 必须得给析构函数变为虚函数
现在我们可以初步达到目的了, 但如果不了解虚函数设计的全貌, 我们就不能自如地使用它.
接下来我们了解更详细的继承下的虚函数知识:
 
重写说明符:override和final
这是和继承有关的两个"关键字" (加了引号), 说了也奇怪, 其实它们本应该为关键字的, 却很可惜, 为啥不是关键字呢?因为千年以来已有大量代码将它作为了普通标识符, C++考虑向后兼容, 避免与已有的项目命名冲突, 所以只得弃掉这两个言达意表的词成为关键字了, 但是它并未完全舍弃, 就把它俩给予了同等地位 又不失解决命名冲突的方法. 我们应该记住它俩的另一个名字:上下文关键字(contextual keyword).
- contextual : 它的英文意思是"上下文的"、“情境的” 或 “与背景相关的”, 上下文限定了范围(在class内起作用), 解决了与已有的项目命名冲突的问题
- keyword:与C++语言特性的关键字有同等地位.
上节说到继承后的虚函数就如蚊子吸血的单向性一样, 基类中的虚函数的virtual 一直传播继承的派生类当中, 那就问题来了, 我们如果某个派生类的函数有的想要virtual ,有的不需要virtual , 要怎么办? 究竟是不是这些功能, 这些暂且不表, 来个例子尝尝鲜:
#include <iostream>
#include <string_view>class Base
{
public:virtual std::string_view getName1(int x) { return "Base1"; }virtual std::string_view getName2(int x) { return "Base2"; }
};class Derived: public Base
{
public:virtual std::string_view getName1(short x) { return "Derived1"; }virtual std::string_view getName2(int x) const { return "Derived2"; }
};int main()
{Derived derived{};Base& rBase { derived };std::cout << rBase.getName1(1) << " and " << rBase.getName2(1) << '\n';return 0;
}
这里的编译我使用了降低错误级别来编译的.注意错误如下:
1, 析构没加virtual,需加上.
2, 各个参数没有使用, 其变量名前加上[[maybe_unused]](in C++17)就行了.
我们是加了virtual变成虚函数的, 为啥调用的是基类部分, 而不是我们期望的派生部分?其实细细看我们发现我们的基类和派生类有些不一样, 继承基类的派生类的基类部分也自然和派生部分不一样, 我制作了表格, 不一样的地方做了红色区分, 这些就是我们经常忽略的签名(函数名称、参数类型以及是否为 const)和返回类型方面的细节.
| 基类Base | 派生类Derived | 
|---|---|
| virtual std::string_view getName1( int x) { return “Base1”; } | virtual std::string_view getName1( short x) { return “Derived1”; } | 
| virtual std::string_view getName2(int x) { return “Base2”; } | virtual std::string_view getName2(int x) const{ return “Derived2”; } | 
这就用到下面的知识了:
override :显式说明覆盖:确保我确实重写了我认为已经重写的函数
注意:
- override在成员函数的位置:在函数声明后面
- 如果有关键字const 则在其后
- 在基类中,对虚函数使用 virtual 关键字。
- 在派生类中,对重写函数使用 override 说明符(但不要使用 virtual 关键字)。这包括虚析构函数。

#include <iostream>
#include <string_view>class Base
{
public:virtual std::string_view getName1(int x) { return "Base1"; }virtual std::string_view getName2(int x) { return "Base2"; }
};class Derived: public Base
{
public:virtual std::string_view getName1(short x) override { return "Derived1"; }virtual std::string_view getName2(int x) const override{ return "Derived2"; }
};int main()
{Derived derived{};Base& rBase { derived };std::cout << rBase.getName1(1) << " and " << rBase.getName2(1) << '\n';return 0;
}
下面是我在错误等级比较低的Ubuntu上运行的
 
 结论:原来override的功能是有助于确保我们确实重写了我们认为已经重写的函数, 即显式说明覆盖, 这需要在每个虚函数的派生类里面使用.
final: 显示拒绝覆盖:进一步重写都应该被视为错误
有些情况下,您可能不希望用户能够重写虚函数或继承某个类。可以使用 final 限定符来告诉编译器强制执行此操作。
#include <iostream>
#include <string_view>class A
{
public:virtual std::string_view getName() const { return "A"; }virtual ~A() {}
};class B: public  A
{
public:std::string_view getName() const override final {return "B"; }
};class C: public B
{
public:std::string_view getName() const override { return "C"; } //error: declaration of 'getName' overrides a 'final' function
};int main()
{B b{};A& rA{b};std::cout << rA.getName() << '\n';C c{};A& rA2{ c};std::cout << rA2.getName() << '\n';return 0;
}
(只有去掉代码中final就能正常运行, 但这里基于测试final的功能, 需要这个错误)
解释一下, override 和final 的作用方式, 基类是虚函数, 它派生类的同签名和返回类型的函数也因继承而为虚函数, 派生类是建立在基类上的,先有基类, 后有派生类, override要见到派生类那一刻才确认真的重写,它因此放在要等到确认的那个类中, 则它在重新后发挥作用
我们提前就知道那个派生类不能重写, final 就放在那个要派生类的基类中, 它在重新之前就发挥作用, 已明确它后面的成员函数不能重写.
特殊的重新:协变返回类型
有一种特殊情况,派生类的虚函数重写可以具有与基类不同的返回类型,并且仍然被视为匹配的重写。如果虚函数的返回类型是指向某个类的指针或引用,则重写函数可以返回指向派生类的指针或引用。这些被称为协变返回类型。
 先决条件:协变返回类型通常用于虚成员函数返回指向包含该成员函数的类的指针或引用的情况
以下是一个示例:
#include <iostream>
#include <string_view>class Base
{
public:// This version of getThis() returns a pointer to a Base classvirtual Base* getThis() { std::cout << "called Base::getThis()\n"; return this; }void printType() { std::cout << "returned a Base\n"; }
};class Derived : public Base
{
public:// Normally override functions have to return objects of the same type as the base function// However, because Derived is derived from Base, it's okay to return Derived* instead of Base*Derived* getThis() override { std::cout << "called Derived::getThis()\n";  return this; }void printType() { std::cout << "returned a Derived\n"; }
};int main()
{Derived d{};Base* b{ &d };d.getThis()->printType(); // calls Derived::getThis(), returns a Derived*, calls Derived::printTypeb->getThis()->printType(); // calls Derived::getThis(), returns a Base*, calls Base::printTypereturn 0;
}

关于协变返回类型,有一点值得注意:C++ 无法动态选择类型,因此我们总是会得到与被调用函数的实际版本相匹配的类型。
在上面的例子中,我们首先调用 d.getThis()。由于 d 是一个 Derived 类,所以它会调用 Derived::getThis(),该函数返回一个Derived*。然后,这个Derived*被用于调用非虚函数 Derived::printType()。
现在来看一个有趣的例子。我们调用 b->getThis()。变量 b 是指向派生对象的基类指针。Base::getThis() 是一个虚函数,所以它会调用 Derived::getThis()。虽然 Derived::getThis() 返回一个 Derived* ,但由于基类版本的 getThis() 返回的是一个 Derived* ,因此返回的 Derived* 会被向上转型为 Base*。因为 Base::printType() 是非虚函数,所以会调用 Base::printType()。
换句话说,在上面的例子中,只有Derived*当你调用 getThis() 时,如果对象本身就是 Derived 类型的对象,你才能得到一个结果。
请注意,如果 printType() 是虚函数而不是非虚函数,则 b->getThis() 的结果(类型为 的对象Base*)将进行虚函数解析,并且将调用 Derived::printType()。
协变返回类型通常用于虚成员函数返回指向包含该成员函数的类的指针或引用的情况(例如,Base::getThis() 返回一个 Object Base*,而 Derived::getThis() 返回一个Derived*Object)。然而,这并非绝对必要。只要重写成员函数的返回类型源自基类虚成员函数的返回类型,就可以使用协变返回类型。
总结:
今天我们主要学了继承(inheriance) 中虚函数(virtual)的重写(或者覆盖)机制的final(刹车) 和 override(确认官), 还有一个比较特殊的情况(虚函数中包含指向其类的指针)下使用的协变返回类型(编译器在基类指针和派生类指针之间择优),然后选择实际匹配度高的成员.
 



