【问题标题】:What are the rules for calling the base class constructor?调用基类构造函数的规则是什么?
【发布时间】:2010-09-12 08:41:15
【问题描述】:

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

例如,我知道在 Java 中,您必须将其作为子类构造函数的第一行(如果不这样做,则假定隐式调用无参数的超级构造函数 - 如果不见了)。

【问题讨论】:

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

标签: c++ inheritance constructor


【解决方案1】:

如果没有参数,则会自动为您调用基类构造函数。如果要调用带参数的超类构造函数,则必须使用子类的构造函数初始化列表。与 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 被显式传递给超类的构造函数。
【解决方案2】:

在 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 块 - 这样初始化器之后的任何函数体都将捕获其异常。
【解决方案3】:

在 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_ 的值更可取,因为这样可以节省额外的赋值成本。

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

【讨论】:

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

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

【讨论】:

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

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

如果你想用参数调用一个基构造函数,你必须在派生构造函数中显式地写成这样:

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

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

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

【讨论】:

    【解决方案6】:

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

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

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

    【讨论】:

      【解决方案7】:

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

      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) 具有相同的主体,但加上: _a{1} 的隐式初始化程序。如果在 init-list 中没有链接特定的基本构造函数,则始终调用 Base()。而且,正如其他地方所提到的,C++11 的委托构造函数和大括号或等号初始化使得默认参数变得不太必要(当它们在很多示例中已经是代码味道时)。
      【解决方案8】:
      CDerived::CDerived()
      : CBase(...), iCount(0)  //this is the initialisation list. You can initialise member variables here too. (e.g. iCount := 0)
          {
          //construct body
          }
      

      【讨论】:

        【解决方案9】:

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

        【讨论】:

        • 如果没有人谈论它,它在哪里提到?
        • @EJP 因为问题是关于调用规则的,所以值得一提的是答案中调用的顺序
        【解决方案10】:

        如果您只是想将所有构造函数参数传递给基类(=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 是一个方便的设计错误:一个很酷的“小工具”,它的作用远比名字所暗示的要多。
        猜你喜欢
        • 2021-05-29
        • 2013-01-28
        • 2020-03-17
        • 1970-01-01
        • 2016-11-01
        • 2016-07-19
        • 2011-02-12
        • 2018-07-16
        相关资源
        最近更新 更多