25-3 覆盖与最终限定符,以及协变返回类型
为解决继承中的常见问题,C++提供了两个与继承相关的标识符:override(重写)和final(最终)。请注意这些标识符并非关键字——它们只是普通词汇,仅在特定上下文中才具有特殊含义。C++标准将其称为“具有特殊含义的标识符”,但通常被称为“限定符specifiers”。
尽管 final 应用较少,override 却是极具价值的补充特性,建议您经常使用。本节将同时探讨这两种标识符,并介绍虚拟函数重写返回类型必须匹配规则的一个例外情况。
覆盖限定符
正如我们在上一课中提到的,派生类的虚函数只有在签名和返回类型完全匹配时才会被视为覆盖。这可能导致意外问题:本应被覆盖的函数实际上并未被覆盖。
请看以下示例:
#include <iostream>
#include <string_view>
class A
{
public:
virtual std::string_view getName1(int x) { return "A"; }
virtual std::string_view getName2(int x) { return "A"; }
};
class B : public A
{
public:
virtual std::string_view getName1(short x) { return "B"; } // note: parameter is a short
virtual std::string_view getName2(int x) const { return "B"; } // note: function is const
};
int main()
{
B b{};
A& rBase{ b };
std::cout << rBase.getName1(1) << '\n';
std::cout << rBase.getName2(2) << '\n';
return 0;
}
由于 rBase 是指向 B 对象的 A 引用,此处的意图是通过虚函数访问 B::getName1() 和 B::getName2()。然而,由于 B::getName1() 接受不同参数类型(short 而不是 int),它不被视为对 A::getName1() 的重写。更隐蔽的是,由于 B::getName2() 为 const 成员而 A::getName2() 不是,B::getName2() 同样不被视为对 A::getName2() 的重写。
因此程序输出:

在此特定情况下,由于A和B仅打印其名称,我们很容易发现重写操作出了问题,导致调用了错误的虚函数。然而在更复杂的程序中,当函数的行为或返回值并未被打印时,此类问题往往难以调试。
为解决函数本应重写却未被正确重写的问题,可将重写修饰符应用于任意虚函数,以此告知编译器强制执行该函数的重写机制。重写修饰符应置于成员函数声明末尾(与函数级const修饰符相同位置)。若成员函数同时具有const和重写属性,const必须位于override之前。
若标记为覆盖的函数未覆盖基类函数(或应用于非虚函数),编译器将标记该函数为错误。
#include <string_view>
class A
{
public:
virtual std::string_view getName1(int x) { return "A"; }
virtual std::string_view getName2(int x) { return "A"; }
virtual std::string_view getName3(int x) { return "A"; }
};
class B : public A
{
public:
std::string_view getName1(short int x) override { return "B"; } // compile error, function is not an override
std::string_view getName2(int x) const override { return "B"; } // compile error, function is not an override
std::string_view getName3(int x) override { return "B"; } // okay, function is an override of A::getName3(int)
};
int main()
{
return 0;
}

上述程序会产生两个编译错误:一个针对 B::getName1(),另一个针对 B::getName2(),因为这两个函数均未覆盖先前定义的函数。而 B::getName3() 确实覆盖了 A::getName3(),因此该行不会报错。
由于使用覆盖标记不会带来性能开销,且有助于确保实际覆盖了预期函数,所有虚拟覆盖函数都应使用覆盖标记进行标注。此外,由于覆盖标记隐含虚拟特性,使用覆盖标记标注的函数无需额外添加virtual关键字。
最佳实践:
在基类中的虚函数上使用 virtual 关键字。
在派生类中的重写函数上使用 override 限定符(而非 virtual 关键字)。这包括虚析构函数。
规则:
若成员函数同时具有 const 和重写属性,const 必须列在首位。const override 是正确的写法,而 override const 则不正确。
最终限定符
有时您可能不希望他人重写虚函数或继承类。最终限定符可用于指示编译器强制执行此限制。若用户试图重写被标记为最终的函数或继承最终类,编译器将报错。
当需要限制用户重写函数时,final 限定符final specifier的使用位置与 override 限定符相同,例如:
#include <string_view>
class A
{
public:
virtual std::string_view getName() const { return "A"; }
};
class B : public A
{
public:
// note use of final specifier on following line -- that makes this function not able to be overridden in derived classes
std::string_view getName() const override final { return "B"; } // okay, overrides A::getName()
};
class C : public B
{
public:
std::string_view getName() const override { return "C"; } // compile error: overrides B::getName(), which is final
};

在上面的代码中,B::getName() 重写了 A::getName(),这没有问题。但 B::getName() 带有 final 修饰符,这意味着对该函数的任何进一步重写都应视为错误。事实上,当 C::getName() 试图覆盖 B::getName() 时(此处的覆盖修饰符无关紧要,仅为遵循良好实践而保留),编译器将报出编译错误。
若需禁止从某个类继承,应在类名后添加 final 修饰符:
#include <string_view>
class A
{
public:
virtual std::string_view getName() const { return "A"; }
};
class B final : public A // note use of final specifier here
{
public:
std::string_view getName() const override { return "B"; }
};
class C : public B // compile error: cannot inherit from final class
{
public:
std::string_view getName() const override { return "C"; }
};

在上例中,类 B 被声明为 final。因此,当 C 试图从 B 继承时,编译器将报出编译错误。
协变返回类型
存在一种特殊情况:派生类的虚函数重写即使返回类型与基类不同,仍可视为匹配的重写。当虚函数的返回类型是某个类的指针或引用时,重写函数可返回派生类的指针或引用。此类返回类型称为协变返回类型covariant return types。示例如下:
#include <iostream>
#include <string_view>
class Base
{
public:
// This version of getThis() returns a pointer to a Base class
virtual 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::printType
b->getThis()->printType(); // calls Derived::getThis(), returns a Base*, calls Base::printType
return 0;
}
这将输出:

关于协变返回类型的一个有趣说明:C++ 无法动态选择类型,因此你始终会得到与实际调用函数版本匹配的类型。
在上例中,我们首先调用 d.getThis()。由于 d 是 Derived 类型,此调用将执行 Derived::getThis(),返回 Derived* 指针。随后该 Derived* 指针被用于调用非虚函数 Derived::printType()。
现在来看关键情况:接着调用 b->getThis()。变量 b 是指向 Derived 对象的 Base 指针。Base::getThis()是虚函数,因此调用Derived::getThis()。尽管Derived::getThis()返回Derived,但由于基类版本的函数返回Base,返回的Derived会被向上转换为Base。由于Base::printType()是非虚函数,最终调用Base::printType()。
换言之,在上例中,只有当调用 getThis() 的对象本身类型为 Derived* 时,才会得到 Derived*。
需注意:若 printType() 为虚函数而非实函数,则 b->getThis() 的结果(类型为 Base* 的对象)将经历虚函数解析,最终调用 Derived::printType()。
当虚成员函数返回指向包含该成员函数的类的指针或引用时(例如 Base::getThis() 返回 Base,而 Derived::getThis() 返回 Derived),共变返回类型常被使用。但这并非绝对必要。当重写成员函数的返回类型可由基类虚成员函数的返回类型派生时,协变返回类型即可适用。
测验时间
问题 #1
下列程序的输出结果是什么?
#include <iostream>
class A
{
public:
void print()
{
std::cout << "A";
}
virtual void vprint()
{
std::cout << "A";
}
};
class B : public A
{
public:
void print()
{
std::cout << "B";
}
void vprint() override
{
std::cout << "B";
}
};
class C
{
private:
A m_a{};
public:
virtual A& get()
{
return m_a;
}
};
class D : public C
{
private:
B m_b{};
public:
B& get() override // covariant return type
{
return m_b;
}
};
int main()
{
// case 1
D d {};
d.get().print();
d.get().vprint();
std::cout << '\n';
// case 2
C c {};
c.get().print();
c.get().vprint();
std::cout << '\n';
// case 3
C& ref{ d };
ref.get().print();
ref.get().vprint();
std::cout << '\n';
return 0;
}
显示方案
BB
AA
AB
在所有情况下,由于 get() 具有协变返回类型,get() 的返回类型将与隐式对象的 get() 成员函数的返回类型一致。
情况 1 较为简单。两条语句中 d.get() 均调用 D::get(),该函数返回 m_b。由于 get() 在类型为 D 的对象 d 上调用,故采用 D::get() 的返回类型 B&。对 print() 和 vprint() 的调用分别解析为 B::print() 和 B::vprint()。
情况 2 同样简单明了。两条语句中 c.get() 均调用 C::get(),该函数返回 m_a。由于 get() 在类型为 C 的 c 上调用,故采用 C::get() 的返回类型 A&。对 print() 和 vprint() 的调用分别解析为 A::print() 和 A::vprint()。
案例三最为有趣。ref 是指向 D 对象的 C&。由于 ref.get() 是虚函数,其虚解析为 D::get() 并返回 m_b。但 get() 具有协变返回类型,因此其返回类型由 get() 被调用的隐式对象类型决定。由于 ref 是 C&,故采用 C::get() 的返回类型,这意味着 ref.get() 的返回类型为 A&(引用对象 m_b,其类型为 B)。
由于 ref.get() 的返回类型为 A&,非虚函数调用 ref.get().print() 将解析为 A::print()。
当调用虚函数 ref.get().vprint() 时,将进行虚函数解析。尽管 ref.get() 的返回类型为 A&,但被引用的对象实际是 B。因此最终调用 B::vprint()。
问题 #2
何时使用函数重载function overloading ,何时使用函数重写function overriding?
函数重载用于当我们需要成员函数或非成员函数在接收不同类型参数时表现不同。
函数重写用于当我们需要成员函数在隐式对象为派生类时表现不同。

浙公网安备 33010602011771号