这是类的构造和析构方式。
首先构造Base,然后构造Derived。因此,在Base的构造函数中,Derived尚未创建。因此,不能调用其任何成员函数。因此,如果Base的构造函数调用虚函数,则它不能是来自Derived的实现,而必须是来自Base的实现。但是,Base中的函数是纯虚函数,并没有可以调用的内容。
在销毁时,首先销毁Derived,然后销毁Base。因此,在Base的析构函数中,没有Derived对象可以调用该函数,只有Base。
顺便提一下,仅在函数仍然是纯虚函数的情况下才是未定义的。因此,这是明确定义的:
struct Base
{
virtual ~Base() { }
virtual void foo() = 0;
};
struct Derived : public Base
{
~Derived() { foo(); }
virtual void foo() { }
};
讨论已经转向建议以下替代方案:
- 它可能会产生编译错误,就像尝试创建抽象类的实例一样。
示例代码无疑会是这样的:
class Base
{
// 其他内容
virtual void init() = 0;
virtual void cleanup() = 0;
};
Base::Base()
{
init();
}
Base::~Base()
{
cleanup();
}
很明显,你正在做的事情会让你陷入麻烦。一个好的编译器可能会发出警告。
另一种选择是寻找 Base::init()
和 Base::cleanup()
的定义,并在存在时调用它,否则将调用链接错误,即将清除操作视为构造函数和析构函数的非虚拟函数。
问题在于,如果你有一个非虚拟函数调用虚拟函数,这种方法就行不通了。
class Base
{
void init();
void cleanup();
virtual ~Base();
virtual void doinit() = 0;
virtual void docleanup() = 0;
};
Base::Base()
{
init();
}
Base::~Base()
{
cleanup();
}
void Base::init()
{
doinit();
}
void Base::cleanup()
{
docleanup();
}
这种情况看起来超出了编译器和链接器的能力范围。请记住,这些定义可以在任何编译单元中。除非你知道它们要做什么,否则构造函数和析构函数调用 init() 或 cleanup() 并不违法,并且 init() 和 cleanup() 调用纯虚函数也并不违法,除非你知道它们从哪里被调用。
编译器或链接器完全无法做到这一点。
因此,标准必须允许编译和链接,并将其标记为“未定义行为”。
当然,如果实现存在,那么编译器就可以使用它(如果有能力)。“未定义行为”并不意味着必须崩溃。只是标准没有规定必须使用它。
请注意,这种情况下析构函数调用一个调用纯虚函数的成员函数,但是您怎么知道它会做这个操作呢?它可能会调用来自完全不同库中的某些内容来调用纯虚函数(假设有访问权限)。
Base::~Base()
{
someCollection.removeMe( this );
}
void CollectionType::removeMe( Base* base )
{
base->cleanup();
}
如果CollectionType存在于完全不同的库中,这里就不可能发生任何链接错误。简单的事实是这些调用的组合是有问题的(但是它们中的任何一个都不是有故障的)。如果removeMe将要调用纯虚拟的cleanup(),它就不能从Base的析构函数中被调用,反之亦然。
最后你必须记住的一件事是,即使Base::init()和Base::cleanup()有实现,它们也永远不会通过虚函数机制(v-table)被调用。它们只会被显式地调用(使用完整的类名限定),这意味着实际上它们并不真正是虚拟的。你被允许给它们实现,这可能是误导人的,可能并不是一个好主意,如果你想要这样一个可以通过派生类调用的函数,也许更好的方法是将其保护并且非虚拟的。
本质上:如果你想让函数具有非纯虚拟函数的行为,这样你就可以给它一个实现,并在构造函数和析构函数阶段调用它,那么就不要定义它为纯虚拟的。为什么要将它定义为你不想要的东西呢?
如果你想做的只是防止实例被创建,你可以用其他方法来做到这一点,例如:
- 使析构函数为纯虚拟的。
- 使所有构造函数都受保护。