5

首先,我搜索了这个问题,发现了很多类似的问题,但我找不到解决我问题的答案。如果这只是我的愚蠢,我很抱歉。

我想做的是让抽象类的构造函数调用一个纯虚函数。在 Java 中,这是可行的,因为子类提供了被调用的抽象方法的实现。但是,在 C++ 中,我收到此链接器错误:

test.o:test.cpp:(.text$_ZN15MyAbstractClassC2Ev[MyAbstractClass::MyAbstractClass
()]+0x16): undefined reference to `MyAbstractClass::initialize()'
collect2: ld returned 1 exit status

这是我的代码:

#include <iostream>

class MyAbstractClass {
protected:
    virtual void initialize() = 0;

public:
    MyAbstractClass() {
        initialize();
    }
};

class MyClass : public MyAbstractClass {
private:
    void initialize() {
        std::cout << "yey!" << std::endl;
    }
};

int main() {
    MyClass *my = new MyClass();
    return 0;
}

作为对我正在尝试做的事情的进一步解释,这里是实现我目标的 Java 代码:

public abstract class MyAbstractClass {

    public MyAbstractClass() {
        initialize();
    }

    protected abstract void initialize();
}

public class MyClass extends MyAbstractClass {

    protected void initialize() {
        System.out.println("Yey!");
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
    }
}

此代码打印“是的!”。非常感谢任何帮助!

4

6 回答 6

4
MyAbstractClass() {
    initialize();
}

这不会对 执行虚拟分派MyClass::initialize(),因为在对象构造的这个阶段,它的MyClass部分还没有被创建。因此,您确实调用MyAbstractClass::initialize(),因此必须对其进行定义。(是的,可以定义纯虚成员函数。)

尽量避免从构造函数中调用虚成员函数,因为这种事情会发生并让你措手不及。这样做很少有意义。

另外,尽量避免initialize()使用函数;你已经有构造函数可以玩了。


更新

实际上,尽管您可以将上述内容视为任何其他虚成员函数的内容,但从构造函数调用虚成员函数会产生未定义的行为。所以不要尝试!

于 2012-02-27T23:25:22.097 回答
2

在 C++ 中,您不能从构造函数或析构函数调用纯虚函数(即使它有定义)。如果你调用一个非纯的,那么它将被分派,就好像对象的类型是正在构建的类,所以你永远无法调用派生类中定义的函数。

在这种情况下,您不需要;派生类的构造函数将在基类之后调用,因此您可以从以下位置获得所需的结果:

#include <iostream>

class MyAbstractClass {
public:
    MyAbstractClass() {
        // don't do anything special to initialise the derived class
    }
};

class MyClass : public MyAbstractClass {
public:
    MyClass() {
        std::cout << "yey!" << std::endl;
    }
};

int main() {
    MyClass my;
    return 0;
}

请注意,我也更改my为自动变量;您应该养成在不需要动态分配时使用它们的习惯,并在确实需要它们时学习如何使用RAII来管理动态资源。

于 2012-02-27T23:27:40.660 回答
1

C++ 方面已在其他答案中处理,但我想在 Java 方面添加备注。从构造函数调用虚函数在所有情况下都是一个问题,而不仅仅是在 C++ 中。基本上,代码试图做的是在尚未创建的对象上执行一个方法,这是一个错误。

用不同语言实现的两种解决方案在试图理解您的代码试图做什么方面有所不同。在 C++ 中,决定是在基对象的构造过程中,直到派生对象的构造开始,对象的实际类型是base,这意味着不会有动态分派。也就是说,任何时候对象的类型都是正在执行的构造函数的类型[*]。虽然这让一些人(包括你在内)感到惊讶,但它为问题提供了一个明智的解决方案。

[*]反之,析构函数。随着派生最多的构造函数完成,类型也会发生变化。

Java 中的另一种选择是对象从一开始就是最终类型,甚至构造完成之前。正如您所演示的,在 Java 中,调用将被分派到最终的覆盖器(我在这里使用 C++ 俚语:执行链中虚函数的最后一个实现),这可能会导致不需要的行为。例如,考虑以下实现initialize()

public class MyClass extends MyAbstractClass {
   final int k1 = 1;
   final int k2;
   MyClass() {
      k2 = 2;
   }
   void initialize() {
      System.out.println( "Constant 1 is " + k1 + " and constant 2 is " + k2 );
   }
}

上一个程序的输出是什么?(答案在底部)

不仅仅是一个玩具示例,考虑它MyClass提供了一些在构造时设置并在对象的整个生命周期内保持不变的不变量。也许它包含对可以转储数据的记录器的引用。通过查看该类,您可以看到在构造函数中设置了记录器,并假设它不能在代码中的任何位置重置:

public class MyClass extends MyAbstractClass {
   Logger logger;
   MyClass() {
      logger = new Logger( System.out );
   }
   void initialize() {
      logger.debug( "Starting initialization" );
   }
}

您现在可能会看到这是怎么回事。通过查看执行,MyClass似乎根本没有任何问题。logger在构造函数中设置,因此可以在类的所有方法中使用。现在的问题是,如果MyAbstractClass调用一个被分派的虚函数,那么应用程序将因 NullPointerException 而崩溃。

到目前为止,我希望您理解并重视不执行动态调度的 C++ 决定,从而避免在尚未完全初始化的对象上执行函数(或者相反,如果虚拟调用在析构函数中,则已经被销毁)。

答案:这可能取决于编译器/JVM,但是当我很久以前尝试过这个时,打印出来的行Constant 1 is 1 and constant 2 is 0。如果你对此感到满意,我很好,但我发现令人惊讶...编译器中的1/0的原因是初始化过程首先设置变量定义中的值,然后调用构造函数。这意味着构造的第一步将k1 在之前设置调用MyAbstractBase构造函数,它将在构造函数运行initialize() 之前 MyBase调用并设置第二个常量的值)。

于 2012-02-27T23:45:00.093 回答
1

让我在这里引用 Scott Meyers(请参阅从不调用虚拟函数在构造或破坏期间):

第 9 条:永远不要在构造或销毁过程中调用虚函数。

我将从回顾开始:在构造或销毁期间不应调用虚函数,因为调用不会按照您的想法进行,如果他们这样做了,您仍然会不高兴。如果你是一个正在恢复的 Java 或 C# 程序员,请密切关注这个 Item,因为这是那些语言曲折而 C++ 曲折的地方。

问题:在对象构建期间,虚函数表可能还没有准备好。想象一下,你的班级是例如。第四顺位继承。构造函数是按继承顺序调用的,所以在调用这个纯虚拟(或者即使它是非纯的)时,您希望基类调用initialize一个尚未完成的对象!

于 2012-02-27T23:22:50.023 回答
0

在构造和销毁期间,虚拟表为正在构造或销毁的基础子对象适当地设置。这是理论上正确的做法,因为更多派生类不存在(它的生命周期尚未开始或已经结束)。

于 2012-02-27T23:28:11.553 回答
-1

正如@Seth 已经解释的那样,您不能在构造函数中调用虚函数。更具体地说,虚拟调度机制在构建和销毁期间被禁用。要么使您的initialize成员函数非虚拟并在基类中实现它,要么让用户显式调用它。

于 2012-02-27T23:17:04.733 回答