从子类构造函数体中调用基类构造函数

19
我曾认为这是不可能的,例如可以参考以下问题: 在C++中在其他一些指令之后调用基类的构造函数 但以下程序能够运行并产生两行“Constructor Person”的输出:
#include <iostream>

class Person
{
public:
    Person() 
    { 
        std::cout << "Constructor Person" << std::endl; }
    };

class Child : public Person
{
public:
    Child() 
    { 
        c = 1; 
        Person(); 
    }
    int c;
};

int main() 
{
    Child child;
    return 0;
}

第一种情况是默认构造函数的隐式调用,这很清楚。那第二种情况呢——它是否意味着标题中描述的操作是合法的?我使用的是Visual C++ 2010。


根据下面的答案/评论,结论是:应该准确理解“从子类D构造函数体中调用基类B的构造函数”的含义。不能以这种方式调用构造函数B,以创建此子对象的父部分。谢谢大家! - TT_ stands with Russia
也许可以。可以直接使用 placement new(不确定是否可以这样做),或者有时可以通过实用程序函数调用来模拟。https://stackoverflow.com/questions/62434909/manually-calling-constructor-of-base-class-outside-initialization-list - oromoiluig
5个回答

21

在子类构造函数中调用的方法并不是调用基类构造函数,而是创建了一个临时、未命名的新对象,类型为Person。该对象将在构造函数退出时被销毁。为了澄清,你的示例与执行以下操作相同:

Child() { c = 1; Person tempPerson; }

除非在这种情况下,临时对象有一个名称。

如果您稍微修改一下示例,您就可以看到我的意思:

class Person
{
public:
    Person(int id):id(id) { std::cout << "Constructor Person " << id << std::endl; }
    ~Person(){ std::cout << "Destroying Person " << id << std::endl; }
    int id;
};

class Child : public Person
{
public:
    Child():Person(1) { c = 1; Person(2); }
int c;
};

int main() {
Child child;

Person(3);
return 0;
}

这会产生以下输出:

Constructor Person 1
Constructor Person 2
Destroying Person 2
Constructor Person 3
Destroying Person 3
Destroying Person 1

是的,@happydave在下面的评论中也写了同样的话。可能我不理解构造函数调用的含义。如果我的或你的示例中的Person();不是构造函数调用,那么当人们说“你不能从子类构造函数调用基类构造函数”时,他们到底是什么意思? - TT_ stands with Russia
1
@TT_ 他们的意思是你不能在那个对象上调用它。当你的Person构造函数第二次运行时,“this”指针的值将与第一次运行时完全不同。因为第二次,它只是在一个临时的、不相关的对象上调用构造函数。 - happydave
1
@happydave...这意味着我的第二个构造函数调用与子类的父部分的构建无关。这是有道理的。似乎术语不够精确。 - TT_ stands with Russia

12
以下是“Accelerated C++”一书的摘录:
“派生对象的构造方式如下:
1. 为整个对象(包括基类成员和派生类成员)分配空间;
2. 调用基类构造函数来初始化对象的基类部分;
3. 根据构造函数初始化器指示初始化派生类成员;
4. 执行派生类构造函数的主体(如果有的话)。"

总结答案和评论:从子类的构造函数体中调用基类构造函数是不可能的,因为上述步骤#2必须先于步骤#4。但我们仍然可以在派生构造函数体中创建一个基类对象,从而调用基类构造函数。 它将是与当前正在执行的派生构造函数构造的对象不同的对象。

从子类构造函数体内调用基类构造函数是不可能的,只会产生未定义行为。然而,使用放置(placement)new语法来完成此操作是晦涩难懂的。但是,在构造函数中完全销毁对象,然后重新构造对象是可以的。例如:C::C() { thread_local static C*guard; if (guard == this) return; guard = this; ~C(); new (this)C(); guard = nullptr; }。我尝试使用所有最近版本的编译器都可以生成有效且可工作的机器代码。 :) - Kuba hasn't forgotten Monica

10

您无法从子构造函数的主体中调用它,但可以将其放入初始化程序列表中:

public:
    Child() : Person() { c = 1; }

当然,调用父类的默认构造函数是没有任何帮助的,因为这将自动发生。如果需要传递参数给构造函数,则更有用。

你不能在函数体中调用构造函数的原因是C++保证父类在子类构造函数开始之前完成构造。


我知道我可以把它放入初始化列表中,但我想知道我的代码为什么有效(这样我就可以从子构造函数的主体中调用它!)。 - TT_ stands with Russia
1
@TT_ 我认为它实际上并没有调用构造函数,至少不是你感兴趣的对象。 - Mark Ransom
不是的,这是一个对象实例化。它声明了一个全新的Person对象。这就像是在说int x;,只是它是匿名的,并且与Child无关(它是一个Person,而不是整数)。 - happydave
@happydave,我理解你的观点,但构造函数的调用会是什么样子呢?我知道有些事情是不可能的,但它似乎可以工作。所以自然而然的问题是 - 到底什么是不可能的?当人们说:你不能从子类构造函数中调用基类构造函数时,他们到底是什么意思? - TT_ stands with Russia
1
@TT_ 不,我无法提供这样的示例,因为创建临时对象是完全可以的。它只是不能达到你的期望。 - Mark Ransom
显示剩余7条评论

3
这个问题的答案通常在技术上是正确和有用的,但并不能给出整体情况。而且整体情况与表面看起来有些不同 :)
  1. The base class's constructor is always invoked, otherwise in the body of the derived class's constructor you'd have a partially constructed and thus unusable object. You have the option of providing arguments to the base class constructor. This doesn't "invoke" it: it gets invoked no matter what, you can just pass some extra arguments to it:

    // Correct but useless the BaseClass constructor is invoked anyway
    DerivedClass::DerivedClass() : BaseClass() { ... }
    // A way of giving arguments to the BaseClass constructor
    DerivedClass::DerivedClass() : BaseClass(42) { ... }
    
  2. The C++ syntax to explicitly invoke a constructor has a weird name and lives up to this name, because it's something that's very rarely done - usually only in library/foundation code. It's called placement new, and no, it has nothing to do with memory allocation - this is the weird syntax to invoke constructors explicitly in C++:

    // This code will compile but has undefined behavior
    // Do NOT do this
    // This is not a valid C++ program even though the compiler accepts it!
    DerivedClass::DerivedClass() { new (this) BaseClass(); /* WRONG */ }       
    DerivedClass::DerivedClass() { new (this) BaseClass(42); /* WRONG */ }
    // The above is how constructor calls are actually written in C++.
    

    So, in your question, this is what you meant to ask about, but didn't know :) I imagine that this weird syntax is helpful since if it were easy, then people coming from languages where such constructor calls are commonplace (e.g. Pascal/Delphi) could write lots of seemingly working code that would be totally broken in all sorts of ways. Undefined behavior is not a guarantee of a crash, that's the problem. Superficial/obvious UB often results in crashes (like null pointer access), but a whole lot of UB is a silent killer. So making it harder to write incorrect code by making some syntax obscure is a desirable trait in a language.

  3. The "second option" in the question has nothing to do with constructor "calls". The C++ syntax of creating a default-constructed instance of a value of BaseClass object is:

    // Constructs a temporary instance of the object, and promptly
    // destructs it. It's useless.
    BaseClass();
    // Here's how the compiler can interpret the above code. You can write either
    // one and it has identical effects. Notice how the scope of the value ends
    // and you have no access to it.
    {
      BaseClass __temporary{};
    }
    

    In C++ the notion of a construction of an object instance is all-permeating: you do it all the time, since the language semantics equate the existence of an object with that object having been constructed. So you can also write:

    // Constructs a temporary integer, and promptly destructs it.
    int();
    

    Objects of integer type are also constructed and destructed - but the constructor and destructor are trivial and thus there's no overhead.

    Note that construction and destruction of an object this way doesn't imply any heap allocations. If the compiler decides that an instance has to be actually materialized (e.g. due to observable side effects of construction or destruction), the instance is a temporary object, just like the temporaries created during expression evaluation - a-ha, we notice that type() is an expression!

    So, in your case, that Person(); statement was a no-op. In code compiled in release mode, no machine instructions are generated for it, because there's no way to observe the effects of this statement (in the case of the particular Person class), and thus if no one is there to hear the tree fall, then the tree doesn't need to exist in the first place. That's how C++ compilers optimize stuff: they do lot of work to prove (formally, in a mathematical sense) whether the effects of any piece of code may be unobservable, and if so the code is treated as dead code and removed.


-1

是的,我知道这已经过去一年了,但我找到了一种方法来解决它。这可能不是最佳实践。例如,在派生类构造函数中从基类实例中销毁可能会导致灾难。您可以跳过析构步骤,但如果基类构造函数执行任何分配,则可能会导致内存泄漏。

class Derived : public Base
{
public:
   Derived()
   {
       // By the time we arrive here, the base class is instantiated plus 
       // enough memory has been allocated for the additional derived class stuff.

       // You can initialize derived class stuff here

       this->Base::~Base(); // destroy the base class
       new (this) Base(); // overwrites the base class storage with a new instance
   }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接