在基没有虚拟方法的派生类中声明虚拟方法是错误的吗

Is it an error to declare a virtual method in a derived class whose base does not have a virtual method?

本文关键字:方法 虚拟 声明 错误 派生      更新时间:2023-10-16

例如:

// No virtual methods in Base
class Base
{
public:
  Base() {}
  ~Base() {}
  void Foo();
};
// Derived class containing virtual methods -- will this cause problems??
class Derived : public Base
{
public:
  Derived() {}
  virtual ~Derived() {}
  virtual void Bar() {}
};

我读过在基类中声明至少一个虚拟(和/或纯虚拟)函数会隐式地导致所有派生类将virtual关键字应用于在基类中定义的那些相同(虚拟)方法。这是有道理的。

昨天,我读到一条回复的评论,@Aaron McDaid表示:

如果你有一个非虚拟基类,并且你的Derived类中有一些虚拟方法,那么Base*b=new Derived();删除b;将是未定义的行为,并可能使您的程序崩溃。它看起来很安全,但事实并非如此。(这是因为b不会指向Derived对象的"开始"-它将被vtable所需的空间所偏移。然后,删除将不会在与新地址完全相同的地址上操作,因此它不是一个可释放的有效地址。如果你要在任何地方使用虚拟方法,那么在基中放置一个虚拟d’tor。

这在我看来似乎是合理的——有人确切知道它是否准确吗?如果它是准确的,为什么C++不隐式地使一个基本方法(例如析构函数)成为虚拟的,以保持vtable在前面并防止未定义的行为?

有没有一个例子不希望这种情况发生?

编辑:如果基类不包含虚拟方法,那么在派生类中声明(和使用)虚拟方法是否安全?

在基没有虚拟方法的派生类中声明虚拟方法是错误的吗?

不,这不是错误。这是安全的,只要你不使用错误的地址delete对象,这可能发生在Aaron讨论的场景中。。。

有人确切地知道[亚伦·麦克达德的断言]是否准确吗?

是和否。确实,"Base *b = new Derived(); delete b;将是未定义的行为,并可能使您的程序崩溃"

为什么的解释在某些系统上是正确的,但虚拟调度的实现机制在标准中没有具体说明,该标准只规定了编译器编写器必须协调的行为。没有特别的理由认为指向虚拟调度表的指针将位于对象的前面。尽管如此,你还是可以写一个程序来看看它是否在你自己的系统上:

#include <iostream>
struct Base
{
    int b_, b2_, b3_;
    Base() { std::cout << "Base(this " << (void*)this << ")n"; }
};
struct Derived : Base
{
    int d_, d2_, d3_;
    Derived() { std::cout << "Derived(this " << (void*)this << ")n"; }
    virtual ~Derived() { }
};
int main()
{
    Base* p = new Derived();
    std::cout << "p " << (void*)p << 'n';
}

有了GCC/Linux,我看到。。。

Base(this 0x12eda018)
Derived(this 0x12eda010)
p 0x12eda018

它显示了Base子对象向Derived偏移8字节:这小于3个ints的大小,这意味着它是其他东西:正如Aaron所描述的,假设虚拟调度指针在那里是合理的。

另外,"如果你要在任何地方都有虚拟方法,那么就在基中放一个虚拟d’tor。"对于...; delete b;代码中引入的特定场景是必要的建议,但不应被误认为是所有具有virtual函数的派生类的必要:只有那些可能通过Base*进行delete操作的派生类。

为什么C++不隐式地使一个基本方法(例如析构函数)成为虚拟的,以保持vtable在前面并防止未定义的行为?

将虚拟函数添加到类中可能会产生不希望出现的后果,概括地说:

  • 额外内存使用
  • 较慢的性能
  • 可以限制/阻止安全使用来自不同进程的共享内存中的对象
  • 折衷封装,因为有人可以避免基类析构函数中的操作
  • 可能会妨碍内存布局与协议或硬件的兼容性

基类存在的原因有很多,包括作为"混合体"提供额外的功能,[模板]"策略"控制某些功能方面,并可能公开一些API用于运行时控制和/或查询,对派生类的实现支持等。在这些情况下,通常不需要在基类中使用虚拟析构函数,不加区别地加一个会带来上述缺点。

在非虚拟析构函数的情况下,只会调用基类的析构函数,如果派生类中有基类的成员未定义/未设置,则会导致崩溃(尽管情况并非总是如此)。

引用中提到的情况在多重继承的情况下是可能的,因为给定基类可能有不同的偏移量。从内存结构来看,继承的类包含在类中。

 _____________________________
| Base class 1                |
 _____________________________
| Base class 2                |
|    .......                  |
 -----------------------------

在上面的情况下,如果您试图使用基类2指针来删除或执行任何操作,那么您可能会面临这个问题,除非您从对象地址获取偏移量。对于基类1指针,您不应该遇到任何问题。

C++并没有使它隐式化,因为继承可能有不同的目的。此外,虚拟函数还具有性能惩罚。有时,人们不需要虚拟析构函数,因为基类只用于取出一些常见的东西,而基类不用于引用对象(根据OOP的概念,这可能是不正确的)。与其他语言不同,C++还支持私有和受保护的继承。使用派生对象作为基类没有任何意义。

不想使用虚拟函数的例子是私有继承,这意味着派生类是根据基类而不是基类实现的。例如,Stack可以使用数组来实现。或者,您可以在Stack中拥有数组的对象,也可以私下派生数组。

class Stack: private Array {
  //Implement function using Array
}

显然,在这种情况下,Stack不再是Array。因此,在Array中不需要使用虚拟析构函数。即使使用,由于私有继承,也无法释放Stack。还有许多其他可能的情况。

在您的情况下,您可能不会面临崩溃,但派生类的成员将无法获得自由,并导致资源泄漏。