C++学习笔记----9、发现继承的技巧(一)---- 使用继承构建类(4)

2.4、override关键字

override关键字的使用是可选的,但强烈推荐。没有这个关键字,可能会意外在继承类中而不是重载基类中的成员函数生成一个新的(virtual)成员函数,而有效地隐藏了基类中的成员函数。看一下下面的Base与Derived类,Derived恰当地重载了someFunction(),但是却没有使用override关键字:

cpp 复制代码
class Base
{
public:
    virtual void someFunction(double d);
};

class Derived : public Base
{
public:
    virtual void someFunction(double d);
};

可以通过引用调用someFunction()如下:

cpp 复制代码
Derived myDerived;
Base& ref { myDerived };
ref.someFunction(1.1); // Calls Derived's version of someFunction()

它正确地调用了Derived类的重载的someFunction()。现在,假设你在重载someFunction()时意外地使用了一个整型参数而不是double,如下:

cpp 复制代码
class Derived : public Base
{
public:
    virtual void someFunction(int i);
};

这个代码没有重载Base的someFunction(),但是生成了一个新的virtual成员函数。如果你想如下代码所示通过Base引用来尝试调用someFunction(),是Base的someFunction()被调用而不是Derived的!

cpp 复制代码
Derived myDerived;
Base& ref { myDerived };
ref.someFunction(1.1); // Calls Base's version of someFunction()

这种类型的问题会在你开始修改Base但是忘记了去修改所有的继承类的时候会发生。例如,可能你的Base类的第一个版本有一个叫做someFunction()接受整型数的成员函数。然后呢,你写了一个Derived类重载了这个接受整型数的someFunction()。后来你决定Base中的someFunction()需要一个double而不是整数,所以你更新了Base类中的someFunction()。当时这种情况是可能发生的,你忘记了去更新继承类中的someFunction()的重载,也去接受一个double而浊整数。由于把这个忘了,你现在实际上是生成了一个新的virtual成员函数而不是恰当地重载了基类的成员函数。

可以通过使用override关键字如下来防止这种情况:

cpp 复制代码
class Derived : public Base
{
public:
    void someFunction(int i) override;
};

该Derived的定义生成一个编译错误,因为带上override关键字,你的意思就是someFunction()是要重载基类的一个成员函数,而基类没有someFunction()接受整数,只有一个接受double。

这种问题意外地生成一个新的成员函数而不是恰当地重载一个也可能会发生在重命名基类中的成员函数而忘记重命名继承类中的重载成员函数。

警告:在成员函数上总是要使用override关键字意味着要重载基类的成员函数。

2.5、virtual的真相

到现在为止你知道了如果一个成员函数不是virtual,在继承类中尝试重载它就会隐藏那个成员函数的基类版本。本节为探索编译器是如何 实现virtual成员函数的,它们的性能影响是什么,也会讨论virtual析构函数的重要性。

2.5.1、virtual是如何实现的

要理解如何避免成员函数隐藏,需要知道更多一点virtual关键字实际上干了什么。当类在c++中编译的时候,生成一个二进制文件对象,包含了类的所有的成员函数。在non-virtual情况下,代码将控制权转移给恰当的成员函数是直接在基于编译时类型的成员函数被调用时硬编码的。这叫做静态绑定,也叫做前绑定。

如果成员函数被声明为virtual,正确的实现是通过使用一块叫做vtable,或"virtual table"的内存区域来调用的。每个类有一个或多个virtual成员函数有一个vtable,每一个这样的类对象都包含一个指向对应vtable的指针。该vtable包含指向virtual成员函数的实现的指针。用这种方式,当在一个对应对象的指针或引用上调用一个成员函数时,它的vtable指南就跟随着,恰当版本的成员函数基于运行时对象的实际类型执行。这叫做动态绑定,也叫做后绑定。记住动态绑定只在使用对象的指针或引用时有效是很重要的。如果在一个对象上直接调用一个virtual成员函数,那么该调用会使用编译时解析好了的静态绑定。

为了更好的理解vtable是如何使得重载成员函数成为可能,看下面Base与Derived类的例子:

cpp 复制代码
class Base
{
public:
    virtual void func1();
    virtual void func2();
    void nonVirtualFunc();
};

class Derived : public Base
{
public:
    void func2() override;
    void nonVirtualFunc();
};

在这个例子中,假定你有下面两个实例:

cpp 复制代码
Base myBase;
Derived myDerived;

下面的图展示了两个实例的vtable的高阶视图。myBase对象包含一个指向它的vtable的指针。该vtable有两个入口,一个是func1(),一个是func2()。这些入口指向了Base::func1()与Base::func2()的实现。

myDerived也包含了一个指向它的vtable的指针,也有两个入口,一个是func1(),一个是func2()。它的func1()入口指向了Base::func1(),因为Derived没有重载func1()。另一方面,它的func2()入口指向了Derived::func2()。

注意两个vtable都没有包含任何nonVirtualFunc()成员函数的入口,因为这个成员函数不是virtual。

相关推荐
竹竹零5 分钟前
JAVA队列
java·开发语言·数据结构·个人开发
计算机毕设-小月哥6 分钟前
【Python Django + Vue】酒店在线预订系统:用技术说话!
开发语言·vue.js·后端·python·django·计算机毕设·计算机毕业设计
是jin奥9 分钟前
Golang 逃逸分析(Escape Analysis)理解与实践篇
开发语言·后端·golang
阿星_22 分钟前
VS2022中Qt环境配置步骤
c++·qt
Qfuuu22 分钟前
extern “C“ 的作用、C++ 和 C 编译的不同、C++ 编译过程的五个主要阶段
c++
qq_4663024524 分钟前
Qt窗体ui如何设置中英文翻译?
开发语言·qt·ui
python百炼成钢28 分钟前
day01-Qt5入门
开发语言·qt
llleo081529 分钟前
10.11 Qt
开发语言·qt
内有小猪卖29 分钟前
tcl/perl 脚本命令学习
开发语言·学习·perl
yngsqq37 分钟前
016集——c# 实现CAD类库 与窗体的交互(CAD—C#二次开发入门)
开发语言·c#