代码之家  ›  专栏  ›  技术社区  ›  David Coufal

在构造函数内调用虚拟函数

  •  200
  • David Coufal  · 技术社区  · 17 年前

    假设我有两个C++类:

    class A
    {
    public:
      A() { fn(); }
    
      virtual void fn() { _n = 1; }
      int getn() { return _n; }
    
    protected:
      int _n;
    };
    
    class B : public A
    {
    public:
      B() : A() {}
    
      virtual void fn() { _n = 2; }
    };
    

    如果我写下以下代码:

    int main()
    {
      B b;
      int n = b.getn();
    }
    

    有人可能会想到 n 设置为2。

    结果是 n 设置为1。为什么?

    13 回复  |  直到 8 年前
        1
  •  185
  •   melpomene    8 年前

    从构造函数或析构函数调用虚拟函数是危险的,应该尽可能避免。所有C++实现都应该调用在当前构造函数中层次结构定义的函数的版本,而不再调用。

    这个 C++ FAQ Lite 在第23.7节中详细介绍了这一点。我建议阅读这篇文章(以及其他常见问题解答),以便后续跟进。

    Excerpt:

    […]在构造函数中,虚拟调用机制被禁用,因为尚未发生派生类的重写。对象是从基向上构造的,基在派生之前。

    […]

    销毁是在基类__之前完成的_derived class,因此虚拟函数的行为与构造函数中的一样:只使用本地定义__,不调用重写函数以避免触及对象的(现在已销毁的)派生类部分。

    编辑 最正确的(谢谢李特)

        2
  •  78
  •   David Rodríguez - dribeas    17 年前

    在大多数OO语言中,从构造函数调用多态函数是一个灾难的秘诀。当遇到这种情况时,不同的语言将执行不同的操作。

    基本问题是,在所有语言中,必须在派生类型之前构造基类型。现在,问题是从构造函数调用多态方法意味着什么。你希望它表现得怎么样?有两种方法:在基类(C++风格)调用方法,或者调用层次结构底部的未构造对象(Java方式)的多态方法。

    在C++中,基类将在进入自己的构造之前构建其虚拟方法表的版本。此时,对虚拟方法的调用将最终调用该方法的基本版本或生成 纯虚拟方法调用 如果它在层次结构的那个级别上没有实现。在基完全构建之后,编译器将开始构建派生类,并且它将重写方法指针以指向层次结构的下一级中的实现。

    class Base {
    public:
       Base() { f(); }
       virtual void f() { std::cout << "Base" << std::endl; } 
    };
    class Derived : public Base
    {
    public:
       Derived() : Base() {}
       virtual void f() { std::cout << "Derived" << std::endl; }
    };
    int main() {
       Derived d;
    }
    // outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run
    

    在Java中,编译器将在构建第一个步骤之前构建虚拟表,在进入基构造函数或派生构造函数之前。含义是不同的(对我来说更危险)。如果基类构造函数调用了在派生类中重写的方法,则实际将在派生级别处理调用,并对未构造的对象调用方法,从而产生意外的结果。在构造函数块内初始化的派生类的所有属性尚未初始化,包括“final”属性。具有在类级别定义的默认值的元素将具有该值。

    public class Base {
       public Base() { polymorphic(); }
       public void polymorphic() { 
          System.out.println( "Base" );
       }
    }
    public class Derived extends Base
    {
       final int x;
       public Derived( int value ) {
          x = value;
          polymorphic();
       }
       public void polymorphic() {
          System.out.println( "Derived: " + x ); 
       }
       public static void main( String args[] ) {
          Derived d = new Derived( 5 );
       }
    }
    // outputs: Derived 0
    //          Derived 5
    // ... so much for final attributes never changing :P
    

    如你所见,调用多态性( 事实上的 在C++术语中,方法是常见的错误来源。在C++中,至少你有保证它永远不会调用一个尚未构建的对象的方法…

        3
  •  53
  •   David Coufal    17 年前

    原因是C++对象是像洋葱一样从内向外构造的。在派生类之前构造超级类。所以,在制造B之前,必须先制造A。当A的构造函数被调用时,它还不是B,所以虚函数表仍然有一个条目来获取FN()的副本。

        4
  •  22
  •   Cheers and hth. - Alf    8 年前

    这个 C++ FAQ Lite 很好地涵盖了这一点:

    本质上,在调用基类构造函数的过程中,对象还不是派生类型,因此调用虚拟函数的基类型实现,而不是派生类型的实现。

        5
  •  13
  •   davidA    14 年前

    解决问题的一个方法是使用工厂方法创建对象。

    • 为包含虚方法afterconstruction()的类层次结构定义公共基类:
    class Object
    {
    public:
      virtual void afterConstruction() {}
      // ...
    };
    
    • 定义工厂方法:
    template< class C >
    C* factoryNew()
    {
      C* pObject = new C();
      pObject->afterConstruction();
    
      return pObject;
    }
    
    • 这样使用:
    class MyClass : public Object 
    {
    public:
      virtual void afterConstruction()
      {
        // do something.
      }
      // ...
    };
    
    MyClass* pMyObject = factoryNew();
    
    
        6
  •  1
  •   TimW    17 年前

    你知道Windows资源管理器的崩溃错误吗?! “纯虚拟函数调用…”
    同样的问题…

    class AbstractClass 
    {
    public:
        AbstractClass( ){
            //if you call pureVitualFunction I will crash...
        }
        virtual void pureVitualFunction() = 0;
    };
    

    因为函数pureVitualFunction()没有实现,并且函数是在构造函数中调用的,所以程序将崩溃。

        7
  •  1
  •   Yogesh    13 年前

    vtables由编译器创建。 类对象有指向其vtable的指针。当它启动时,vtable指针指向vtable 基本类的。在构造函数代码的末尾,编译器生成代码来重新指向vtable指针。 到类的实际vtable。这确保调用虚拟函数的构造函数代码调用 这些函数的基类实现,而不是类中的重写。

        8
  •  1
  •   msc    9 年前

    这个 C++ Standard (ISO/IEC 14882-2014) 赛义德:

    可以调用成员函数,包括虚拟函数(10.3) 施工或破坏期间(12.6.2)。当虚函数 直接或间接从构造函数或从 销毁器,包括在施工或销毁 类的非静态数据成员以及调用的对象 适用的对象(称之为X)正在构造或破坏中, 调用的函数是构造函数中的最后一个重写器,或者 析构函数的类,而不是在派生类中重写它的类。 如果虚拟函数调用使用显式类成员访问 (5.2.5)对象表达式是指x的完整对象。 或该对象的基类子对象之一,但不是X或其子对象之一 基类子对象,行为为 未定义 .

    所以,不要调用 virtual 来自构造函数或析构函数的函数,由于构造顺序从 基础衍生 析构函数的顺序从 派生到基类 .

    因此,试图从正在构造的基类调用派生类函数是危险的。类似地,对象在构造过程中按相反的顺序被破坏,因此试图从析构函数调用更派生类中的函数可能会访问已释放的资源。

        9
  •  1
  •   François Andrieux    8 年前

    其他答案已经解释了为什么 virtual 从构造函数调用时,函数调用不能按预期工作。我想提出另一个可能的解决方法,从基类型的构造函数中获得类似多态的行为。

    通过向基类型添加模板构造函数,这样模板参数总是被推断为派生类型,就可以知道派生类型的具体类型。从那里,你可以打电话 static 该派生类型的成员函数。

    此解决方案不允许- 静止的 要调用的成员函数。当执行在基类型的构造函数中时,派生类型的构造函数甚至没有时间检查它的成员初始化列表。正在创建的实例的派生类型部分尚未开始初始化。由于非 静止的 成员函数几乎可以肯定地与数据成员交互,这是不常见的 希望 调用派生类型的非- 静止的 基类型的构造函数中的成员函数。

    下面是一个示例实现:

    #include <iostream>
    #include <string>
    
    struct Base {
    protected:
        template<class T>
        explicit Base(const T*) : class_name(T::Name())
        {
            std::cout << class_name << " created\n";
        }
    
    public:
        Base() : class_name(Name())
        {
            std::cout << class_name << " created\n";
        }
    
    
        virtual ~Base() {
            std::cout << class_name << " destroyed\n";
        }
    
        static std::string Name() {
            return "Base";
        }
    
    private:
        std::string class_name;
    };
    
    
    struct Derived : public Base
    {   
        Derived() : Base(this) {} // `this` is used to allow Base::Base<T> to deduce T
    
        static std::string Name() {
            return "Derived";
        }
    };
    
    int main(int argc, const char *argv[]) {
    
        Derived{};  // Create and destroy a Derived
        Base{};     // Create and destroy a Base
    
        return 0;
    }
    

    此示例应打印

    Derived created
    Derived destroyed
    Base created
    Base destroyed
    

    当A Derived 是建造的, Base 构造函数的行为取决于所构造对象的实际动态类型。

        10
  •  0
  •   Priya    8 年前

    首先,创建对象,然后将其地址分配给指针。在创建对象时调用构造函数,并用于初始化数据成员的值。对象指针在对象创建后进入场景。这就是为什么,C++不允许我们将构造函数设为虚拟的。 。另一个原因是,没有什么比指向构造函数的指针更能指向虚拟构造函数,因为虚拟函数的一个属性是它只能由指针使用。

    1. 虚拟函数用于动态赋值,因为构造函数是静态的,所以我们不能使它们成为虚拟的。
        11
  •  0
  •   stands2reason    8 年前

    正如已经指出的,对象是在构建的基础上创建的。在构造基对象时,派生对象尚不存在,因此虚拟函数重写无法工作。

    然而,这可以用使用 静态多态性 如果getter返回常量,或者可以用静态成员函数表示,则不使用虚函数,而是使用crtp( https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern )

    template<typename DerivedClass>
    class Base
    {
    public:
        inline Base() :
        foo(DerivedClass::getFoo())
        {}
    
        inline int fooSq() {
            return foo * foo;
        }
    
        const int foo;
    };
    
    class A : public Base<A>
    {
    public:
        inline static int getFoo() { return 1; }
    };
    
    class B : public Base<B>
    {
    public:
        inline static int getFoo() { return 2; }
    };
    
    class C : public Base<C>
    {
    public:
        inline static int getFoo() { return 3; }
    };
    
    int main()
    {
        A a;
        B b;
        C c;
    
        std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;
    
        return 0;
    }
    

    通过使用静态多态性,基类知道在编译时提供信息时要调用哪个类的getter。

        12
  •  -3
  •   user2305329    12 年前

    我没有看到虚拟关键字的重要性。B是一个静态类型变量,其类型由编译器在编译时确定。函数调用不会引用vtable。构造B时,调用其父类的构造函数,这就是为什么_n的值设置为1的原因。

        13
  •  -4
  •   terry    17 年前

    在对象的构造函数调用期间,虚拟函数指针表没有完全生成。这样做通常不会给你期望的行为。在这种情况下调用虚拟函数可能会起作用,但不能保证,并且应避免便携且遵循C++标准。

    推荐文章