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

Posted

技术标签:

【中文标题】从子类的构造函数体中调用基类的构造函数【英文标题】:Calling a constructor of the base class from a subclass' constructor body 【发布时间】:2014-02-19 03:35:22 【问题描述】:

我的印象是这是不可能的,例如: Calling the constructor of the base class after some other instructions in 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。

【问题讨论】:

所以根据下面的answers/cmets,答案是:应该准确理解“从子类D构造函数体调用基类的构造函数B”的含义。构造函数 B 不能以创建此子对象的父级部分的方式调用。谢谢大家! 也许可以。直接通过placement new(不确定这样做是否可以),或者有时可以使用实用函数调用***.com/questions/62434909/… 进行模拟 【参考方案1】:

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

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

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

你不能从body中调用构造函数的原因是因为C++保证父类会在子类构造函数启动之前完成构造。

【讨论】:

我知道我可以把它放入初始化列表,我在问为什么我的代码可以工作(所以我可以从子构造函数的主体中调用它!)。 您只是通过调用Person(); 来实例化一个临时Person 对象。它与当前正在构造的 Child 对象无关(当然,除了从 Child 对象的构造函数中调用)。 @TT_ 我不认为它实际上是在调用构造函数,至少不是针对您感兴趣的对象。 @happydave 再一次,它没有回答这个问题。我不是在问副作用,问题是:为什么这段代码有效? @TT_ 不,我无法生成这样的示例,因为创建临时对象完全可以。它只是没有达到您的预期。【参考方案2】:

子类构造函数内部的调用不是调用基类构造函数,而是创建一个临时的、未命名的 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 在下面的 cmets 中写了同样的内容。可能,我不明白构造函数调用是什么意思。如果我或您的示例中的Person(); 不是构造函数调用,那么人们究竟是什么意思说:您不能从子类构造函数调用基本构造函数? @TT_ 他们的意思是你不能在那个对象上调用它。当您的 Person 构造函数第二次运行时,“this”指针的值将与第一次运行时的“this”指针完全不同。因为第二次,它只是在一个临时的、不相关的对象上调用构造函数。 @happydave ...意味着我的第二个构造函数调用与子级父级部分的构造无关。这是有道理的。术语似乎不够准确。【参考方案3】:

以下是“Accelerated C++”的节选: "派生对象的构造方法: 1. 为整个对象(基类成员和派生类成员)分配空间; 2.调用基类构造函数初始化对象的基类部分; 3.按照构造函数初始化器的指示初始化派生类的成员; 4. 执行派生类构造函数的主体,如果有的话。”

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

【讨论】:

从子类的构造函数体中调用基类的构造函数是不可能的完全有可能,只是未定义的行为,所以它有助于放置新语法来实际提取这样的特技关闭是模糊的。但是例如完全破坏构造函数内的对象,然后重新构造它是 AFAIK 定义的,所以你可以C::C() thread_local static C*guard; if (guard == this) return; guard = this; ~C(); new (this)C(); guard = nullptr; 。我尝试过的所有最近的编译器都会为其生成有效且工作的机器代码,至少:)【参考方案4】:

这个问题的答案虽然在技术上通常是真实和有用的,但不要给出大局。大局与看起来有些不同:)

    总是调用基类的构造函数,否则在派生类的构造函数的主体中,您将有一个部分构造并因此无法使用的对象。您有向基类构造函数提供参数的选项。这不会“调用”它:无论如何它都会被调用,你可以传递一些额外的参数给它:

    // 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)  ... 
    

    显式调用构造函数的 C++ 语法有一个奇怪的名称,并且符合这个名称,因为它很少使用 - 通常只在库/基础代码中。它被称为 placement new,不,它与内存分配无关 - 这是在 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++.
    

    因此,在您的问题中,这就是您要问的问题,但不知道 :) 我认为这种奇怪的语法很有帮助,因为如果它很容易,那么来自此类构造函数调用很常见的语言的人(例如 Pascal/Delphi)可以编写许多看似可以工作的代码,这些代码会以各种方式被完全破坏。 未定义的行为并不能保证崩溃,这就是问题所在。肤浅/明显的 UB 经常导致崩溃(如空指针访问),但很多 UB 是一个无声的杀手。因此,通过使某些语法晦涩难懂来增加编写错误代码的难度是一种语言的理想特性。

    问题中的“第二个选项”与构造函数“调用”无关。创建 BaseClass 对象的默认构造实例的 C++ 语法是:

    // 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;
    
    

    在 C++ 中,构造对象实例的概念无处不在:您一直都在这样做,因为语言语义将对象的存在等同于已构造的对象。所以你也可以这样写:

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

    整数类型的对象也被构造和析构 - 但构造函数和析构函数是微不足道的,因此没有开销。

    请注意,以这种方式构造和销毁对象并不意味着任何堆分配。如果编译器决定必须实际实现一个实例(例如,由于构造或销毁的可观察副作用),则该实例是一个临时对象,就像在表达式评估期间创建的临时对象一样 - 哈哈,我们注意到 @987654326 @ 是一个表达式!

    因此,在您的情况下,Person(); 语句是无操作的。在以发布模式编译的代码中,不会为其生成任何机器指令,因为无法观察此语句的效果(在特定的Person 类的情况下),因此如果没有人听到树秋天,那么这棵树一开始就不需要存在。这就是 C++ 编译器优化东西的方式:他们做了大量工作来证明(正式地,在数学意义上)任何一段代码的影响是否可能是不可观察的,如果是,则代码被视为死代码并被删除。

【讨论】:

【参考方案5】:

是的,我知道这已经一岁了,但我找到了一种方法。这可能不是最佳做法。例如,从派生类构造函数中销毁基类实例听起来像是灾难的秘诀。您可以跳过析构函数步骤,但如果基类构造函数进行任何分配,这可能会导致内存泄漏。

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
   
;

【讨论】:

以上是关于从子类的构造函数体中调用基类的构造函数的主要内容,如果未能解决你的问题,请参考以下文章

C++中如何在子类的构造函数中调用基类的构造函数来初始化基类成员变量

C++中,继承时,创建子类对象,能否在子类构造函数初始化列表里调用基类构造函数?

定义子类对象时要先调用基类构造函数,是应该哪样理解呢

C++中,建立子类对象的时候,会调用基类的构造函数,

C#在自己的构造函数之后调用基类的构造函数?

派生类的构造函数与析构函数的调用顺序