方法覆盖
virtual关键字
只有在基类中声明为 virtual 的方法才能被派生类正确覆盖。关键字位于方法声明的开头,如下面的 Base 的修改版本所示:
class Base {
public:
virtual void someMethod() {}
protected:
int m_protectedInt { 0 };
private:
int m_privateInt { 0 };
};
override关键字
要覆盖一个方法,你可以在派生类定义中重新声明它,就像它在基类中声明的那样,不同的是添加了 override 关键字并删除了 virtual 关键字。
class Derived : public Base {
public:
void someMethod() override; // Overrides Base's someMethod()
virtual void someOtherMethod();
};
override 关键字的使用是可选的,但强烈建议使用。如果没有关键字,可能会意外地创建一个新的虚方法,而不是覆盖基类中的方法。
virtual是如何实现的
要了解如何避免方法隐藏,需要更多地了解 virtual 关键字的实际作用。在 C++ 中编译类时,会创建一个二进制对象,其中包含该类的所有方法。在非virtual的情况下,将控制转移到适当方法的代码直接硬编码在基于编译时类型调用方法的位置。这称为静态绑定,也称为早期绑定。
如果该方法被声明为virtual,则通过使用称为 vtable 或“虚表”的特殊内存区域调用正确的实现。每个具有一个或多个虚方法的类都有一个虚表,并且此类的每个对象都包含一个指向该虚表的指针。这个 vtable 包含指向虚方法实现的指针。这样,当在对象上调用方法时,指针跟随进入vtable,并在运行时根据对象的实际类型执行适当版本的方法。这称为动态绑定,也称为后期绑定。
为了更好地理解 vtables 如何使方法覆盖成为可能,以下面的 Base 和 Derived 类为例:
class Base {
public:
virtual void func1();
virtual void func2();
void nonVirtualFunc();
};
class Derived : public Base {
public:
void func2() override;
void nonVirtualFunc();
};
对于此示例,假设有以下两个实例:
Base myBase;
Derived myDerived;
图显示了两个实例的 vtable 外观的高级视图。 myBase 对象包含一个指向它的虚表的指针。该 vtable 有两个条目,一个用于 func1(),一个用于 func2()。这些条目指向 Base::func1() 和 Base::func2() 的实现。
myDerived 也包含指向其 vtable 的指针,该 vtable 也有两个条目,一个用于 func1(),一个用于 func2()。它的 func1() 入口指向 Base::func1(),因为 Derived 不会覆盖 func1()。另一方面,它的 func2() 入口指向 Derived::func2()。
请注意,两个 vtable 都不包含 nonVirtualFunc() 方法的任何条目,因为该方法不是virtual。
virtual的合理性
在某些语言中,例如 Java,所有方法都是自动virtual的,因此可以正确地覆盖它们。在 C++ 中,情况并非如此。反对在 C++ 中使一切virtual的论点,以及首先创建关键字的原因,与 vtable 的开销有关。要调用虚方法,程序需要通过解引用指向要执行的适当代码的指针来执行额外的操作。在大多数情况下,这是一个很小的性能损失,但 C++ 的设计者认为让程序员决定是否需要性能损失会更好,至少在当时是这样。如果该方法永远不会被覆盖,则无需将其虚拟化并降低性能。然而,对于今天的 CPU,性能影响是以纳秒的分数来衡量的,而且未来的 CPU 会越来越小。在大多数应用程序中,使用虚方法和避免使用虚方法之间不会有可衡量的性能差异。
尽管如此,在某些特定的用例中,性能开销可能过于昂贵,你可能需要有一个选项来避免这种情况。例如,假设你有一个具有虚方法的 Point 类。如果你有另一个存储数百万甚至数十亿个点的数据结构,则在每个点上调用虚拟方法会产生巨大的开销。在这种情况下,避免在 Point 类中使用任何虚方法可能是明智的。
每个对象的内存使用量也会受到轻微影响。除了方法的实现之外,每个对象还需要一个指向它的 vtable 的指针,它占用的空间很小。在大多数情况下这不是问题。但是,有时它确实很重要。再次以 Point 类和存储数十亿点的容器为例。在这种情况下,所需的额外内存变得很重要。
虚析构函数
除非你有特殊原因不这样做或类被标记为final,否则析构函数应标记为virtual。构造函数不能也不需要是virtual的,因为你总是在创建对象时指定要构造的确切类。
阻止覆盖
除了将整个类标记为 final 之外,C++ 还允许将单个方法标记为 final。此类方法不能在进一步的派生类中被覆盖。例如,从以下派生类覆盖 someMethod() 会导致编译错误:
class Base {
public:
virtual ~Base() = default;
virtual void someMethod();
};
class Derived : public Base {
public:
void someMethod() override final;
};
class DerivedDerived : public Derived {
public:
void someMethod() override; // Compilation error.
};