ChatGPT解决这个技术问题 Extra ChatGPT

调用基类构造函数的规则是什么?

从派生类调用基类构造函数的 C++ 规则是什么?

例如,我知道在 Java 中,您必须将其作为子类构造函数的第一行(如果您不这样做,则假定隐式调用无参数超级构造函数 - 如果缺少,则会出现编译错误) .

吹毛求疵:C++ 中没有“超类”,事实上,标准根本没有提及。这个措辞源于Java(很可能)。在 C++ 中使用“基类”。我猜 super 意味着单亲,而 C++ 允许多重继承。
@andreee 我认为 super class 也称为 base class 并且在 qt 工具包 parent class 中也称为 fe - 按此顺序 sub class 也称为 child class 也许这有助于消除术语上的一些潜在混淆

l
luke

如果它们没有参数,则会自动为您调用基类构造函数。如果要调用带参数的超类构造函数,则必须使用子类的构造函数初始化列表。与 Java 不同,C++ 支持多重继承(无论好坏),因此必须通过名称引用基类,而不是“super()”。

class SuperClass
{
    public:

        SuperClass(int foo)
        {
            // do something with foo
        }
};

class SubClass : public SuperClass
{
    public:

        SubClass(int foo, int bar)
        : SuperClass(foo)    // Call the superclass constructor in the subclass' initialization list.
        {
            // do something with bar
        }
};

有关构造函数的初始化列表 herehere 的更多信息。


我从 SuperClass 构造函数中删除了“显式”。尽管是单参数构造函数的最佳实践,但它与手头的讨论并没有密切关系。有关显式关键字的详细信息,请参阅:weblogs.asp.net/kennykerr/archive/2004/08/31/…
冒号:在实例化子类构造函数之前用来调用超类构造函数的运算符,我想这对方法也是如此?
@hagubear,仅对构造函数有效,AFAIK
当您通过 SubClass anObject(1,2) 实例化一个子类对象时,1 是否会传递给 SuperClass(foo)(成为参数 foo 的参数)?我一直在搜索高低文档,但没有一个明确说明 SubClass 构造函数的参数可以作为参数传递给 SuperClass 构造函数。
@Gnuey,注意 : SuperClass(foo) 部分。 foo 被显式传递给超类的构造函数。
S
Segfault

在 C++ 中,所有超类和成员变量的无参数构造函数都会在进入构造函数之前为您调用。如果你想向它们传递参数,有一个单独的语法称为“构造函数链接”,它看起来像这样:

class Sub : public Base
{
  Sub(int x, int y)
  : Base(x), member(y)
  {
  }
  Type member;
};

如果此时运行任何东西抛出,先前已完成构造的基/成员将调用其析构函数,并将异常重新抛出给调用者。如果要在链接期间捕获异常,则必须使用函数 try 块:

class Sub : public Base
{
  Sub(int x, int y)
  try : Base(x), member(y)
  {
    // function body goes here
  } catch(const ExceptionType &e) {
    throw kaboom();
  }
  Type member;
};

在这种形式中,请注意 try 块是函数体,而不是在函数体内部;这允许它捕获由隐式或显式成员和基类初始化以及函数体期间引发的异常。但是,如果函数 catch 块没有抛出不同的异常,则运行时将重新抛出原始错误;初始化期间的异常不容忽视。


我不确定我是否理解您的第二个示例的语法...... try/catch 构造是构造函数主体的替代品吗?
是的。我改写了该部分,并修复了一个错误(try 关键字位于初始化列表之前)。我应该查一下而不是从内存中写出来,这不是经常使用的东西:-)
感谢您为初始化程序包含 try/catch 语法。我使用 C++ 已有 10 年了,这是我第一次看到这种情况。
我不得不承认,我已经使用 C++ 很长时间了,那是我第一次在构造函数列表中看到 try/catcn。
我可能会说函数体“进入”了 try 块——这样初始化器之后的任何函数体都将捕获它的异常。
D
Dima

在 C++ 中有一个构造函数初始化列表的概念,您可以并且应该在其中调用基类的构造函数,并且还应该在其中初始化数据成员。初始化列表在冒号之后的构造函数签名之后,构造函数主体之前。假设我们有一个 A 类:


class A : public B
{
public:
  A(int a, int b, int c);
private:
  int b_, c_;
};

然后,假设 B 有一个采用 int 的构造函数,A 的构造函数可能如下所示:


A::A(int a, int b, int c) 
  : B(a), b_(b), c_(c) // initialization list
{
  // do something
}

可以看到,在初始化列表中调用了基类的构造函数。顺便说一下,初始化初始化列表中的数据成员比在构造函数的主体内分配 b_ 和 c_ 的值更可取,因为您节省了额外的赋值成本。

请记住,数据成员始终按照它们在类定义中声明的顺序进行初始化,而不管它们在初始化列表中的顺序如何。为避免在数据成员相互依赖时可能出现的奇怪错误,您应始终确保初始化列表和类定义中成员的顺序相同。出于同样的原因,基类构造函数必须是初始化列表中的第一项。如果您完全省略它,则将自动调用基类的默认构造函数。在这种情况下,如果基类没有默认构造函数,则会出现编译器错误。


等一下……你说初始化器节省了分配的成本。但是,如果被调用,它们内部不会发生相同的分配吗?
没有。初始化和赋值是不同的东西。当调用构造函数时,它会尝试使用它认为是默认值的任何值来初始化每个数据成员。在初始化列表中,您可以提供默认值。因此,无论哪种情况,您都会产生初始化成本。
如果你在正文中使用赋值,那么无论如何你都会产生初始化成本,然后是最重要的赋值成本。
这个答案很有帮助,因为它显示了一种语法变体,其中一个人有一个头文件和一个源文件,并且一个人不希望头文件中的初始化列表。非常有帮助,谢谢。
C
Community

每个人都提到了通过初始化列表调用构造函数,但没有人说可以从派生成员的构造函数体中显式调用父类的构造函数。例如,请参阅问题 Calling a constructor of the base class from a subclass' constructor body。关键是,如果您在派生类的主体中使用对父类或超类构造函数的显式调用,这实际上只是创建父类的实例,而不是在派生对象上调用父类构造函数.在派生类的对象上调用父类或超类构造函数的唯一方法是通过初始化列表,而不是在派生类构造函数体中。所以也许它不应该被称为“超类构造函数调用”。我把这个答案放在这里是因为有人可能会感到困惑(就像我一样)。


这个答案有点令人困惑,即使我已经阅读了几次并查看了链接到的问题。我认为它的意思是,如果您在派生类的主体中使用对父类或超类构造函数的显式调用,这实际上只是创建父类的实例,而不是调用父类派生对象的构造函数。在派生类的对象上调用父类或超类构造函数的唯一方法是通过初始化列表,而不是在派生类构造函数体中。
@Richard Chambers 这可能令人困惑,因为英语不是我的第一语言,但你准确地描述了我想说的话。
“可以从派生成员的构造函数的主体中显式调用父类的构造函数”这对于所讨论的实例显然是错误的,除非您指的是放置新,即使那样它也是错误的,因为您必须首先破坏实例。例如 MyClass::MyClass() { new (this) BaseClass; /* UB, totally wrong */ } - 这是用于显式调用构造函数的 C++ 语法。这就是“构造函数调用”的样子。所以这个荒谬的错误答案被赞成的事实对我来说完全是个谜。
我认为您链接到的那个问题的大多数答案都是垃圾,或者回避这个问题。 I wrote the answer that was missing that whole time it seems。我并不感到惊讶,任何人都可能会感到困惑,试图从您的链接中理解任何内容......我也会感到困惑。这很容易,但人们写它好像它是某种魔法。盲人引导盲人。 显式构造函数“调用”是通过放置新语法完成的! MyClass() 不是任何类型的“调用”!它与例如 int() 具有相同的含义,并且它创建一个值!
M
Mnementh

如果您有一个没有参数的构造函数,它将在派生类构造函数执行之前被调用。

如果要使用参数调用基构造函数,则必须在派生构造函数中显式编写,如下所示:

class base
{
  public:
  base (int arg)
  {
  }
};

class derived : public base
{
  public:
  derived () : base (number)
  {
  }
};

如果不调用 C++ 中的父构造函数,就无法构造派生类。如果它是非 arg C'tor,则会自动发生这种情况,如果您如上所示直接调用派生构造函数,或者您的代码将无法编译,则会发生这种情况。


C
CR.

将值传递给父构造函数的唯一方法是通过初始化列表。初始化列表是用一个 : 实现的,然后是一个类列表和要传递给该类构造函数的值。

Class2::Class2(string id) : Class1(id) {
....
}

还要记住,如果您有一个不带父类参数的构造函数,它将在子构造函数执行之前自动调用。


e
edW

如果您的基构造函数中有默认参数,则将自动调用基类。

using namespace std;

class Base
{
    public:
    Base(int a=1) : _a(a) {}

    protected:
    int _a;
};

class Derived : public Base
{
  public:
  Derived() {}

  void printit() { cout << _a << endl; }
};

int main()
{
   Derived d;
   d.printit();
   return 0;
}

输出为:1


这只是因为该特定声明创建了一个隐式 Base(),它与 Base(int) 具有相同的主体,但加上 : _aBase() 的隐式初始化程序。如果在 init-list 中没有链接特定的基本构造函数,则始终调用 Base()。而且,正如其他地方所提到的,C++11 的委托构造函数和大括号或等号初始化使得默认参数变得不太必要(当它们在很多示例中已经是代码味道时)。
D
Dynite
CDerived::CDerived()
: CBase(...), iCount(0)  //this is the initialisation list. You can initialise member variables here too. (e.g. iCount := 0)
    {
    //construct body
    }

R
ReinstateMonica3167040

当一个类派生自多个类时,没有人提到构造函数调用的顺序。在派生类时,序列如前所述。


如果没有人谈论它,它在哪里提到?
@EJP 因为问题是关于调用规则的,所以值得一提的是答案中的调用顺序
M
Markus Dutschke

如果您只是想将所有构造函数参数传递给基类(=parent),这是一个最小的示例。

这使用模板将每个带有 1、2 或 3 个参数的构造函数调用转发到父类 std::string

代码

Live-Version

#include <iostream>
#include <string>

class ChildString: public std::string
{
    public:
        template<typename... Args>
        ChildString(Args... args): std::string(args...)
        {
            std::cout 
                << "\tConstructor call ChildString(nArgs="
                << sizeof...(Args) << "): " << *this
                << std::endl;
        }

};

int main()
{
    std::cout << "Check out:" << std::endl;
    std::cout << "\thttp://www.cplusplus.com/reference/string/string/string/" << std::endl;
    std::cout << "for available string constructors" << std::endl;

    std::cout << std::endl;
    std::cout << "Initialization:" << std::endl;
    ChildString cs1 ("copy (2)");

    char char_arr[] = "from c-string (4)";
    ChildString cs2 (char_arr);

    std::string str = "substring (3)";
    ChildString cs3 (str, 0, str.length());

    std::cout << std::endl;
    std::cout << "Usage:" << std::endl;
    std::cout << "\tcs1: " << cs1 << std::endl;
    std::cout << "\tcs2: " << cs2 << std::endl;
    std::cout << "\tcs3: " << cs3 << std::endl;

    return 0;
}

输出

Check out:
    http://www.cplusplus.com/reference/string/string/string/
for available string constructors

Initialization:
    Constructor call ChildString(nArgs=1): copy (2)
    Constructor call ChildString(nArgs=1): from c-string (4)
    Constructor call ChildString(nArgs=3): substring (3)

Usage:
    cs1: copy (2)
    cs2: from c-string (4)
    cs3: substring (3)

更新:使用可变参数模板

推广到 n 个参数并简化

        template <class C>
        ChildString(C arg): std::string(arg)
        {
            std::cout << "\tConstructor call ChildString(C arg): " << *this << std::endl;
        }
        template <class C1, class C2>
        ChildString(C1 arg1, C2 arg2): std::string(arg1, arg2)
        {
            std::cout << "\tConstructor call ChildString(C1 arg1, C2 arg2, C3 arg3): " << *this << std::endl;
        }
        template <class C1, class C2, class C3>
        ChildString(C1 arg1, C2 arg2, C3 arg3): std::string(arg1, arg2, arg3)
        {
            std::cout << "\tConstructor call ChildString(C1 arg1, C2 arg2, C3 arg3): " << *this << std::endl;
        }

template<typename... Args>
        ChildString(Args... args): std::string(args...)
        {
            std::cout 
                << "\tConstructor call ChildString(nArgs="
                << sizeof...(Args) << "): " << *this
                << std::endl;
        }

我确实有点冒犯,这样好的例子表明在任何地方都使用 std::endl。人们看到这一点并将其放入循环中,并想知道为什么“在 C++ 中”将一堆行写入文本文件比使用 fprintf 慢 5 到 20 倍。 TL;DR:使用 "\n"(添加到现有字符串文字,如果有的话),并且仅在需要将缓冲区刷新到文件时使用 std::endl(例如,如果代码崩溃并且您想查看它的调试最后的话)。我认为 std::endl 是一个方便的设计错误:一个很酷的“小工具”,它的作用远比名字所暗示的要多。