【问题标题】:Pure virtual invocation from constructor and destructor来自构造函数和析构函数的纯虚拟调用
【发布时间】:2012-01-28 22:17:52
【问题描述】:

C++ 标准规定禁止从构造函数或析构函数调用纯虚函数。这是什么原因?标准为什么要设置这样的限制?

【问题讨论】:

  • 它没有说禁止。它说未定义。原因是为编译器提供最大的灵活性。

标签: c++ constructor destructor pure-virtual


【解决方案1】:

在运行类析构函数时,所有子类析构函数已经运行。调用子类定义的虚方法是无效的,其析构函数已经运行。

在构造函数中调用虚方法也存在类似的限制。您不能为尚未运行构造函数的子类调用虚方法。

【讨论】:

  • 我关于虚函数的cmets也适用于纯虚函数。
  • @eharvest:这不是真的,可以从任何非构造函数/析构函数方法调用纯虚函数。子类必须提供实现。
  • @eharvest:当从基类调用虚函数时,实际调用的函数取决于调用该函数的实例的类型。它不取决于有多少子类。
  • C++ 中有很多未定义的事情,编译器无法检测到。
  • 您可以从构造函数和析构函数调用虚方法,这在 C++ 中具有明确定义的语义:'调用的函数是构造函数或析构函数类中的最终覆盖器,而不是在更多-派生类”。这就是为什么调用纯虚函数会导致未定义行为的原因。
【解决方案2】:

这与您在浇筑地基或拆除地基时不能住在房子里的原因相同。在构造函数完成之前,对象只是部分构造。并且一旦析构函数启动,对象就会被部分销毁。纯虚函数只能在处于正常状态的对象上调用,否则可能不存在确定调用哪个函数实现所需的结构。

【讨论】:

  • 这与非纯虚函数不同......如何?
【解决方案3】:

C++ 标准规定,禁止从构造函数或析构函数调用纯虚函数。这是什么原因?标准为什么要设置这样的限制?

来自公认的旧 C++ 标准草案,但我将得出的相关区别仍然相关:

10.4-6 可以从抽象类的构造函数(或析构函数)调用成员函数;对于从这样的构造函数(或析构函数)创建(或销毁)的对象,直接或间接地对纯虚函数进行虚拟调用(class.virtual)的效果是未定义的。

这与您所断言的内容略有不同,因为分号前短语的上下文与后置词隐含相关。改写:

undefined behaviour happens when an abstract class's constructor or destructor calls one of its own member functions that is (still) pure virtual.

我包括限定符“(仍然)”,因为在类层次结构的某个点上,纯虚函数被赋予定义 - 并且不再是“纯”的。这有点奇怪,但请考虑标准的这一部分:

一个类是抽象的,如果它至少有一个纯虚函数。 [注意:这样的功能可能会被继承:见下文。 ]

显然,在基类中定义了纯虚函数的派生类本身不一定是抽象的。只有当“纯粹性”的含义不是普遍适用于虚函数本身,而是适用于保持纯粹性的类层次结构的级别时,上述陈述才能成立。

结果:如果函数 - 从层次结构中调用构造函数/析构函数级别的角度来看 - 已经定义,则可以以明确定义的行为调用它。

了解了标准中未定义的内容后,我们可以回到您的问题:“这是什么原因?为什么标准要设置这样的限制?”

原因的关键在于,必须在派生类构造函数开始之前完全构造基类,因为派生类的构造可能在基对象上运行。类似地,基析构函数必须在派生析构函数之后运行,因为后者可能仍希望访问基对象。鉴于这种必要的顺序,编译器无法安全地分派给派生类的虚函数,因为派生类的构造函数尚未运行以建立派生类成员状态,或者派生类的析构函数已经为附加数据调用析构函数成员和对象状态不再保证可用。

对于非纯虚函数,编译器可以并且确实依赖于调用该函数的最专业定义,该函数已知为层次结构中已经构造但尚未销毁的最派生类。但是根据定义,纯虚函数是那些尚未指定实现的函数,并且在类层次结构中纯函数的级别上没有可调用的实现。

虚拟调度机制的典型实现可以通过在虚拟调度表中为基类提供一个指针来表示这一点,该基类包含设置为 0、未初始化或指向某个“引发警报”函数的纯虚函数.随着派生类构造函数的连续层启动,指向虚拟调度表的指针将被它们自己的 VDT 的地址覆盖。那些覆盖实现的派生类将指向它们自己的函数定义,这将成为任何本身不指定新实现的派生类的默认值。当派生类析构函数完成时,这个关键的指向 VDT 成员的隐式指针将向后移动通过相同的 VDT 列表,确保任何虚拟调用都是对类层次结构中未破坏层上的函数。但是,当定义了虚函数的第一个类的析构函数运行时,未来的 VDT 将再次缺少对实际实现的任何引用。

【讨论】:

    【解决方案4】:

    回想一下,从构造函数/析构函数调用“非纯”虚函数会忽略该函数是虚函数的事实,并且 总是 在您的类中调用实现,而不是在派生类中调用建。这就是为什么你不能从构造函数或析构函数调用纯虚函数:就它们而言,你的纯虚函数没有实现。

    【讨论】:

      【解决方案5】:

      该函数只是一个要在子类中实现的原型,它实际上并不存在于类中......因此在构造函数或析构函数中都不能调用它)。

      没有函数的实现,所以简单地说,没有代码可以调用:)

      调用构造函数/析构函数时,实现纯虚的子类不存在。

      【讨论】:

        【解决方案6】:

        假设一个典型的虚函数在其抽象基类中没有显式实现。因此:

        class Base
        {
            public:
            virtual void area() = 0; 
            Base() { area();  }; 
            ~Base() { area(); };
        };
        void Base::area(){ print("Base::area()"); }
        
        class Derived: public Base
        {
            public:
            void area() override { print("Derived::area()"); };
            Derived() {  };
            ~Derived() {  };
        };
        
        int main() {
            Derived d;
        }
        

        一旦你实例化了对象d,执行控制将进入构造函数Base::Base(),并执行它的主体,然后点击area(),由于没有实现area()尚未编译器,编译器将发出警告/错误,因为实际上尚未创建 Derived(其中包含 area() 的覆盖版本)。由于编译器知道(根据标准)纯虚函数必须在所有派生类中实现,而那些派生类不是由编译器创建的;此刻的编译器不知道它应该去哪里。因此,当您的编译器卡住时,就会出现未定义的行为。

        【讨论】:

          猜你喜欢
          • 2016-01-22
          • 2012-11-23
          • 2012-01-28
          • 2021-10-20
          • 2012-04-13
          • 2013-09-06
          • 2012-01-27
          相关资源
          最近更新 更多