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。

相关推荐
Jackson_GJH13 分钟前
Qt 右键自定义菜单的实现
开发语言·c++·qt
张小姐的猫16 分钟前
【AI大模型接入SDK】 —— Gemini接入封装
android·数据结构·数据库·c++·人工智能·python
孫治AllenSun23 分钟前
【LangChain4J-09】开发学习知识点
spring boot·后端·学习
CoderYanger36 分钟前
前端基础——JavaScript(WebAPI)(下篇)
java·开发语言·前端·javascript·程序人生·面试·职场和发展
一木 之林1 小时前
七、一-AI 工程实践、插件化调试与软件交付
java·linux·c++
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day38|Spring AI Alibaba2 思维导图复盘
java·学习·spring
小木_.2 小时前
Python 离线识别滑块缺口距离,项目推荐
开发语言·python·滑块识别·人机验证·滑块缺口·缺口识别
自小吃多2 小时前
高云FPGA ModelSim中的仿真
笔记·嵌入式硬件·fpga开发
摘星星的屋顶2 小时前
2026年8月31日~2026年9月13日周报
人工智能·学习
liliangcsdn2 小时前
因子权重矩阵处理-因子权重收缩Shrinkage算法的探索
开发语言·python·机器学习