为什么对构造函数中虚拟成员函数的调用是非虚拟调用?

Why is a call to a virtual member function in the constructor a non-virtual call?

提问人:David Coufal 提问时间:6/7/2009 最后编辑:Jan SchultkeDavid Coufal 更新时间:9/25/2023 访问量:152239

问:

假设我有两个 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();
}

人们可能会期望将其设置为 2。n

事实证明,它设置为 1。为什么?n

C++ 构造函数 覆盖 virtual-functions member-functions

评论

17赞 David Coufal 6/7/2009
我正在问和回答我自己的问题,因为我想在Stack Overflow中对这种C++深奥的解释。这个问题的一个版本已经两次袭击了我们的开发团队,所以我猜这些信息可能对那里的某个人有用。如果您能以不同/更好的方式解释,请写出答案......
9赞 Zifre 6/7/2009
我想知道为什么这被否决了?当我第一次学习C++时,这真的让我感到困惑。+1
4赞 Craig Reynolds 2/1/2019
令我惊讶的是缺少编译器警告。编译器将对“当前构造函数的类中定义的函数”的调用替换为派生类中“最重写”的函数。如果编译器说“用 Base::foo() 代替对构造函数 foo() 的调用”,那么程序员就会被警告代码不会做他们期望的事情。这比进行无声替换要有用得多,这会导致神秘的行为、大量的调试,并最终前往 stackoverflow 进行启蒙。
3赞 user207421 2/5/2020
@CraigReynolds 不一定。不需要对构造函数中的虚拟调用进行特殊的编译器处理 基类构造函数仅为当前类创建 vtable,因此此时编译器可以像往常一样通过该 vtable 调用 vitrual 函数。但是 vtable 还没有指向任何派生类中的任何函数。在基类构造函数返回后,派生类的构造函数会调整派生类的 vtable,这就是构造派生类后重写的工作方式。

答:

82赞 David Coufal 6/7/2009 #1

原因是C++对象像洋葱一样从内到外构造。基类是在派生类之前构造的。因此,在制作 B 之前,必须制作 A。当 A 的构造函数被调用时,它还不是 B,因此虚函数表仍然具有 A 的 fn() 副本的条目。

评论

20赞 6/7/2009
C++ 通常不使用术语“超类”——它更喜欢“基类”。
0赞 David Rodríguez - dribeas 6/8/2009
这在大多数面向对象语言中都是一样的:如果没有构建基础部分,就不可能构建派生对象。
3赞 M.M 12/15/2015
@DavidRodríguez-dribeas 其他语言确实做到了这一点。例如,在 Pascal 中,首先为整个对象分配内存,然后只调用最派生的构造函数。构造函数必须包含对其父构造函数的显式调用(这不一定是第一个操作 - 它只需要在某个地方),或者如果没有,就好像构造函数的第一行进行了该调用一样。
0赞 user5193682 11/2/2016
感谢您的清晰和避免细节,这些细节不会直接影响结果
0赞 BAKE ZQ 8/14/2020
如果调用仍然使用 vptr(因为 vptr 也设置为当前级别)方式,或者只是静态调用当前级别的版本。
292赞 JaredPar 6/7/2009 #2

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

C++ FAQ Lite 在第 23.7 节中非常详细地介绍了这一点。我建议阅读该内容(以及常见问题解答的其余部分)以进行跟进。

摘录:

[...]在构造函数中,虚拟调用机制处于禁用状态,因为尚未从派生类重写。对象是从基础开始构建的,即“基础后派生”。

[...]

销毁是在“基类之前派生类”完成的,因此虚函数的行为与构造函数中的类似:仅使用本地定义 - 并且不调用重写函数以避免触及对象的(现已销毁的)派生类部分。

编辑最更正了所有(感谢 litb)

评论

70赞 Johannes Schaub - litb 6/8/2009
不是大多数 C++ 实现,但所有 C++ 实现都必须调用当前类的版本。如果有些没有,那么这些就有错误:)。我仍然同意你的观点,从基类调用虚函数是不好的 - 但语义是精确定义的。
27赞 Steven Sudit 6/19/2014
这并不危险,只是非虚拟的。事实上,如果从构造函数调用的方法是虚拟调用的,这将是危险的,因为该方法可以访问未初始化的成员。
6赞 Siyuan Ren 9/11/2014
为什么从析构函数调用虚函数很危险?当析构函数运行时,对象不是仍然完整,只有在析构函数完成后才会销毁吗?
16赞 Cheers and hth. - Alf 8/14/2016
−1“很危险”,不,这在 Java 中很危险,那里可能会发生下调;C++ 规则通过一种相当昂贵的机制消除了危险。
17赞 Lightness Races in Orbit 9/6/2016
从构造函数调用虚函数在哪些方面是“危险的”?这完全是胡说八道。
32赞 Aaron Maenpaa 6/8/2009 #3

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

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

评论

3赞 moodboom 12/11/2016
清晰、直接、最简单的答案。这仍然是我希望看到得到一些爱的功能。我讨厌必须编写所有这些愚蠢的 initializeObject() 函数,用户在构造后被迫立即调用这些函数,这对于一个非常常见的用例来说只是糟糕的形式。不过我理解其中的困难。C'est la vie.
1赞 underscore_d 5/21/2017
@moodboom 你提出什么“爱”?请记住,你不能只是改变目前的工作方式,因为这会严重破坏大量现有代码。那么,你会怎么做呢?不仅要引入什么新语法来允许在构造函数中(实际的、非虚拟化的)虚拟调用,还要说明如何以某种方式修改对象构造/生存期的模型,以便这些调用具有要运行的派生类型的完整对象。这会很有趣。
0赞 moodboom 5/22/2017
@underscore_d我认为不需要任何语法更改。也许在创建对象时,编译器会添加代码来遍历 vtable 并寻找这种情况并修补东西?我从未编写过C++编译器,我很确定我最初给它一些“爱”的评论是幼稚的,这永远不会发生。:-)无论如何,虚拟 initialize() 函数并不是一个非常痛苦的解决方法,您只需要记住在创建对象后调用它。
0赞 moodboom 5/22/2017
@underscore_d 我刚刚注意到您在下面的其他评论,解释说 vtable 在构造函数中不可用,再次强调了这里的困难。
1赞 underscore_d 5/22/2017
@moodboom 我在写 vtable 在构造函数中不可用时傻了。它是可用的,但构造函数只能看到其自己类的 vtable,因为每个派生构造函数都会更新实例的 vptr 以指向当前派生类型的 vtable,而不是进一步。因此,当前的 ctor 看到的是一个只有自己的覆盖的 vtable,因此它不能调用任何虚函数的更多派生实现。
2赞 TimW 6/8/2009 #4

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

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

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

评论

0赞 underscore_d 5/21/2017
很难看出这是同一个问题,因为你没有解释为什么。在 ctor 期间调用非纯虚函数是完全合法的,但它们只是不通过(尚未构造的)虚拟表,因此执行的方法版本是为我们所在的 ctor 类类型定义的方法版本。所以这些不会崩溃。这样做是因为它是纯虚拟的并且未实现(旁注:可以在基础中实现纯虚函数),因此没有要为此类类型调用的方法版本,并且编译器假设您没有编写糟糕的代码,所以繁荣
0赞 underscore_d 5/22/2017
噢。这些调用确实会通过 vtable,但它尚未更新以指向派生最多的类的重写:只有现在正在构造的类。尽管如此,崩溃的结果和原因仍然是一样的。
0赞 user2943111 2/19/2022
@underscore_d “(旁注:可以在基础中实现纯虚函数)”不,你不能,否则该方法不再是虚。您也无法创建抽象类的实例,因此,如果您尝试从构造函数调用纯方法,则 TimW 的示例将无法编译。它现在可以编译,因为构造函数不调用纯虚拟方法,也不包含任何代码,只包含注释。
0赞 Ben Voigt 7/6/2023
@user2943111:是的,你可以。C++ 中的纯虚拟意味着类是抽象的,每个派生类也将是抽象的,除非它覆盖纯虚拟成员函数。没有任何定义的虚拟成员函数必须是纯的,但反之则不然。
111赞 David Rodríguez - dribeas 6/8/2009 #5

在大多数面向对象语言中,从构造函数调用多态函数是灾难的根源。遇到这种情况时,不同的语言会有不同的表现。

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

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

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 中,编译器将在构造的第一步,即输入 Base 构造函数或 Derived 构造函数之前,构建虚拟表等效项。含义是不同的(而且在我看来更危险)。如果基类构造函数调用在派生类中重写的方法,则该调用实际上将在派生级别处理,即在未构造对象上调用方法,从而产生意外结果。在构造函数块中初始化的派生类的所有属性都尚未初始化,包括“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赞 DS. 9/21/2012
很好地解释了为什么替代方案(也)容易出错。
7赞 underscore_d 7/3/2016
一个解释!+1,上级答案恕我直言
4赞 underscore_d 5/21/2017
@VinGarcia什么?在这种情况下,C++ 不会“禁止”任何事情。该调用仅被视为对当前正在执行其构造函数的类的方法的非虚拟调用。这是对象构建时间线的合乎逻辑的结果 - 而不是阻止你做傻事的严厉决定。事实上,它也巧合地实现了后一个目的,这对我来说只是一个奖励。
3赞 underscore_d 6/3/2017
@VinGarcia 对于那些在使用语言之前不费心去了解语言如何工作的人来说,这只是“出乎意料”的,而只是假设他们的期望会反映现实而不进行检查——在这种情况下,我没有同情心:语言,根据定义,是你学习的东西,而不是“期望”。我认为,作为一个设计决策,它被清楚地记录下来,并且非常有意义。而且我不想使用另一种语言,尽管你没有解释为什么它无论如何都是相关的反例。
1赞 user207421 2/5/2020
@underscore_d 它不被视为非虚拟呼叫。它的处理方式与任何其他虚拟呼叫相同。关键是,在构造函数中,vtable 尚未指向任何派生类的重写方法,因此虚拟调用无法“看到”当前类范围之外的内容。
-4赞 terry 6/8/2009 #6

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

评论

5赞 curiousguy 12/24/2011
"在这种情况下调用虚拟函数可能会起作用,但不能保证这是不正确的。行为是有保证的。
1赞 underscore_d 7/3/2016
@curiousguy......保证调用基本版本(如果可用),或者如果 vfunc 是纯虚拟的,则调用 UB。
20赞 Tobias 6/8/2009 #7

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

  • 为包含虚拟方法 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();

1赞 Yogesh 5/7/2013 #8

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

评论

1赞 curiousguy 7/11/2016
vptr 不会在 ctor 结束时更改。在 ctor 的主体中,虚函数调用会覆盖,而不是任何基类版本。C::CC
0赞 curiousguy 8/9/2016
对象的动态类型是在 ctor 调用基类 ctor 之后和构造其成员之前定义的。因此,vptr 不会在 ctor 结束时更改。
0赞 Yogesh 12/4/2017
@curiousguy我说的是同样的事情,vptr 不会在基类的构造函数结束时更改,它将在派生类的构造函数结束时更改。我希望你也这么说。它依赖于编译器/实现。您何时建议 vptr 应该更改。有什么好的理由投反对票吗?
1赞 curiousguy 12/5/2017
vptr 更改的时间与实现无关。它由语言语义规定:当类实例的动态行为发生变化时,vptr 会发生变化。这里没有自由。在 ctor 的主体内部,动态类型是 。vptr 将反映这一点:它将指向 T 的 vtable。你不同意吗?T::T(params)T
0赞 curiousguy 12/5/2017
也许有一个真实的继承例子来谈论会更容易
-2赞 user2305329 10/28/2013 #9

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

评论

1赞 underscore_d 7/3/2016
问题是为什么 的构造函数调用基数,而不是它的派生覆盖。变量的类型与此无关。bf()b
0赞 Lightness Races in Orbit 9/6/2016
“函数调用不会引用 vtable”事实并非如此。如果您认为虚拟调度仅在通过 a 或“B&”访问时启用,那您就错了。B*
0赞 underscore_d 5/22/2017
除了它遵循自己的逻辑得出错误的结论之外......这个答案背后的想法,即已知的静态类型,被误用了。编译器可以去虚拟化,因为它知道真实的类型,并且直接从 中调度到版本。但这只是假设规则的允许。一切仍然必须像使用虚拟表一样运行,并严格遵循。在构造函数中,情况也是如此:即使(可能不可能)它与 ctor 内联,虚拟调用仍然必须像 - 如果它只有基本 vtable 可供使用。b.getN()BABA
0赞 Peter - Reinstate Monica 8/23/2018
@LightnessRacesinOrbit 你能给我举个例子来说明你的断言,即虚拟调度是在不通过引用或指针(包括隐式)调用的情况下发生的吗?this
0赞 Peter - Reinstate Monica 8/23/2018
@user2305329 你说得对,通话是非虚拟的。 是一个静态类型的对象,为其类型定义的任何内容都将被调用。但是成员函数(包括构造函数)内部,所有成员函数调用都是通过隐式指针进行的,因此如果它是多态类,则为虚函数调用。解析对基类实现的虚拟调用的原因和基本原理 - 即使它发生在派生对象的整体构造过程中 - 在其他答案中进行了解释。b.getn()bgetn()thisfn()
3赞 msc 8/23/2017 #10

C++标准(ISO/IEC 14882-2014)说:

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

因此,不要从构造函数或析构函数中调用函数,这些函数试图调用正在构造或销毁的对象,因为构造的顺序从基类开始到派生,析构函数的顺序从派生到基类开始。virtual

因此,尝试从正在构造的基类调用派生类函数是危险的。同样,对象的销毁顺序与构造相反,因此尝试从析构函数调用更派生的类中的函数可能会访问已释放的资源。

0赞 Priya 11/10/2017 #11

首先,创建对象,然后将其地址分配给指针。构造函数在创建对象时调用,用于初始化数据成员的值。指向对象的指针在创建对象后进入方案。这就是为什么C++不允许我们将构造函数设置为虚拟的。 另一个原因是,没有像指向构造函数的指针一样可以指向虚拟构造函数的东西,因为虚函数的一个特性就是它只能被指针使用。

  1. 虚函数是用来动态赋值的,因为构造函数是静态的,所以我们不能让它们虚拟。
5赞 stands2reason 4/11/2018 #12

如前所述,这些对象是在建造时自下而上创建的。在构造基本对象时,派生对象尚不存在,因此虚函数覆盖不起作用。

但是,如果 getter 返回常量,或者可以在静态成员函数中表示,则可以使用使用静态多态性而不是虚函数的多态 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。

评论

1赞 Wang 8/1/2018
我想我会避免这样做。这不再是单个基类。您实际上创建了很多不同的基类。
0赞 curiousguy 11/8/2018
@Wang Exactly:只是一个帮助类,而不是可用于运行时多态性的常见接口类型(例如异构容器)。这些也很有用,只是不适用于相同的任务。某些类既继承自基类,基类是运行时多态性的接口类型,又是编译时模板帮助程序。Base<T>
0赞 keyou 1/29/2021 #13

作为补充,调用尚未完成构造的对象的虚函数将面临同样的问题。

例如,在对象的构造函数中启动一个新线程,并将该对象传递给新线程,如果新线程在对象完成构造之前调用该对象的虚函数会导致意外结果。

例如:

#include <thread>
#include <string>
#include <iostream>
#include <chrono>

class Base
{
public:
  Base()
  {
    std::thread worker([this] {
      // This will print "Base" rather than "Sub".
      this->Print();
    });
    worker.detach();
    // Try comment out this code to see different output.
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  virtual void Print()
  {
    std::cout << "Base" << std::endl;
  }
};

class Sub : public Base
{
public:
  void Print() override
  {
    std::cout << "Sub" << std::endl;
  }
};

int main()
{
  Sub sub;
  sub.Print();
  getchar();
  return 0;
}

这将输出:

Base
Sub

评论

0赞 Tomer Shetah 1/29/2021
您好,欢迎来到SO!请阅读导览,以及我如何写出一个好的答案?例如,添加代码片段可能会有所帮助。
0赞 François Andrieux 11/25/2021
此解决方案具有未定义的行为。 不同步线程,因此在构建和销毁期间都有竞争。其次,这有崩溃的风险,因为 worker 需要仍然存在(它是一个成员函数),但不能保证这一点。如果您没有像实例这样的任意等待,则在线程打印之前很容易到达其生命周期的末尾。依赖的解决方案几乎总是被打破。sleep_forthis->Print()thisgetchar()Subdetach()
0赞 Don Slowik 9/29/2021 #14

为了回答运行该代码时会发生什么/原因,我通过 编译了它,并使用 gdb 逐步完成。g++ -ggdb main.cc

main.cc:

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

  protected:
    int _n;
};


class B: public A {
  public:
    B() {
      // fn();
    }
    void fn() override {
      _n = 2;
    }
};


int main() {
  B b;
}

在 处设置一个断点,然后单步执行 B(),打印 ptr,单步执行 A() (基本构造函数):mainthis

(gdb) step
B::B (this=0x7fffffffde80) at main2.cc:16
16    B() {
(gdb) p this
$27 = (B * const) 0x7fffffffde80
(gdb) p *this
$28 = {<A> = {_vptr.A = 0x7fffffffdf80, _n = 0}, <No data fields>}
(gdb) s
A::A (this=0x7fffffffde80) at main2.cc:3
3     A() {
(gdb) p this
$29 = (A * const) 0x7fffffffde80

显示最初指向在堆栈上构建的派生 B obj 0x7fffffffde80。下一步是进入基 A() ctor 并进入相同的地址,这是有道理的,因为基 A 正好位于 B 对象的开头。但它仍然没有被构建:thisbthisA * const

(gdb) p *this
$30 = {_vptr.A = 0x7fffffffdf80, _n = 0}

再往前一步:

(gdb) s
4       fn();
(gdb) p *this
$31 = {_vptr.A = 0x402038 <vtable for A+16>, _n = 0}

_n 已初始化,其虚函数表指针包含以下地址:virtual void A::fn()

(gdb) p fn
$32 = {void (A * const)} 0x40114a <A::fn()>
(gdb) x/1a 0x402038
0x402038 <_ZTV1A+16>:   0x40114a <_ZN1A2fnEv>

因此,下一步通过 this->fn() 执行 A::fn() 是完全有道理的,给定 active 和 .再往前走一步,我们又回到了 B() ctor:this_vptr.A

(gdb) s
B::B (this=0x7fffffffde80) at main2.cc:18
18    }
(gdb) p this
$34 = (B * const) 0x7fffffffde80
(gdb) p *this
$35 = {<A> = {_vptr.A = 0x402020 <vtable for B+16>, _n = 1}, <No data     fields>}

基地 A 已经建成。请注意,存储在虚拟函数表指针中的地址已更改为派生类 B 的 vtable。因此,对 fn() 的调用将通过 this->fn() 选择派生类覆盖 B::fn(),给定 B() 中的活动和 (取消注释调用 B::fn() 以查看此内容。再次检查存储在_vptr中的 1 个地址。A 显示它现在指向派生类重写:this_vptr.A

(gdb) p fn
$36 = {void (B * const)} 0x401188 <B::fn()>
(gdb) x/1a 0x402020
0x402020 <_ZTV1B+16>:   0x401188 <_ZN1B2fnEv>

通过查看此示例,并查看具有 3 级继承的示例,似乎当编译器下降以构造基本子对象时,类型和相应的地址会发生变化以反映当前正在构造的子对象,因此它指向最派生的类型。因此,我们期望从 ctor 内部调用的虚函数选择该级别的函数,即与非虚拟相同的结果。dtors 也是如此,但相反。并在构造成员时成为成员的 ptr,因此它们也可以正确地调用为它们定义的任何虚函数。this*_vptr.Athis

-1赞 Flo 12/29/2022 #15

我刚刚在程序中遇到了这个错误。 我有这样的想法:如果该方法在构造函数中被标记为纯虚拟,会发生什么?

class Base {
public:
    virtual int getInt() = 0;
    
    Base(){
        printf("int=%d\n", getInt());
    }
};

class Derived : public Base {
    public:
        virtual int getInt() override {return 1;}
};

和。。。有趣的事情!你首先得到编译器的警告:

warning: pure virtual ‘virtual int Base::getInt() const’ called from constructor

还有来自 ld 的错误!

/usr/bin/ld: /tmp/ccsaJnuH.o: in function `Base::Base()':
main.cpp:(.text._ZN4BaseC2Ev[_ZN4BaseC5Ev]+0x26): undefined reference to `Base::getInt()'
collect2: error: ld returned 1 exit status

这完全不合逻辑,你只得到编译器的警告!