【问题标题】:Some basic questions on constructors (and multiple-inheritance) in C++?关于 C++ 中的构造函数(和多重继承)的一些基本问题?
【发布时间】:2011-01-20 01:55:23
【问题描述】:

(很抱歉,如果之前有人问过这个问题;搜索功能似乎被破坏了:结果区域完全空白,即使它说有几页结果……在 Chrome、FireFox、和 Safari)

所以,我只是在学习 C++……而我正在阅读的这本书在以我可以掌握的方式解释构造函数方面做得真的很糟糕。到目前为止,我已经大致了解了其他所有内容,但我无法弄清楚构造函数的语法实际上是如何工作的。

例如,我被告知以下将导致构造函数调用指定超类的构造函数:

class something : something_else {
  something(int foo, double bar) : something_else(int foo) {}
};

另一方面,本书后面在描述如何初始化const成员时使用了相同的语法:

class something : something_else {
private:  const int constant_member;
public:   something(int foo, double bar) : constant_member(42) {}
};

那么……呃……那里到底发生了什么? rv signature(param) : something_else(what); 的语法实际上是什么意思?我无法弄清楚 something_else(what) 是什么,与它周围的代码有关。它似乎具有多种含义;我确信它对应的语言中一定有一些底层元素,我只是想不通什么

编辑:另外,我应该提一下,前面例子中的what 有时是一个参数列表(所以something_else(what) 看起来像一个函数签名)......有时是一个常量值表达式(所以something_else(what) 看起来像一个函数调用)。

现在,继续:多重继承和构造函数呢?如何指定调用哪些父类的构造函数……以及默认调用哪些构造函数?我知道,默认情况下,以下两个是相同的……但我不确定涉及多重继承时的等价物是什么:

class something : something_else {
//something(int foo, double bar) : something_else() {}
  something(int foo, double bar) {}
};

非常感谢您对探索这些主题的任何帮助;我不喜欢这种我无法理解基本知识的感觉。我不喜欢它一点

编辑 2: 好的,到目前为止,下面的答案都非常有帮助。不过,他们提出了这个问题的另一个部分:“初始化列表”中基类构造函数调用的参数与您定义的构造函数有何关系?它们是否必须匹配……是否必须有默认值?他们必须匹配多少?换句话说,以下哪些是非法的

class something_else {
  something_else(int foo, double bar = 0.0) {}
  something_else(double gaz) {}
};


class something : something_else {
  something(int foo, double bar)  : something_else(int foo, double bar) {}   };
class something : something_else {
  something(int foo)              : something_else(int foo, double bar) {}   };
class something : something_else {
  something(double bar, int foo)  : something_else(double gaz) {}   };

【问题讨论】:

  • 你在读哪本书? void something()... 完全错误。
  • 对不起,我仍然坚持使用 C 的语法。这些都不是直接从书中复制的,因此我认为我做错了一些事情。 (另外,我认为 void 是合法的,但没有必要……\一些编译器会吐出来,不过,我认为……所以我确定它是错误的形式。)
  • 哦,这本书是 Practical C++ Programming,作者 Steve Oualline
  • C++ 足够复杂,第一次学习。我知道如果你已经花时间在一本书上会很痛苦——但相信我,你会读得更多。所以让自己更轻松,从一本好书开始。我建议从 Accelerated C++ 开始。
  • 请注意,您总是弄错:它是something(int foo, double bar) : something_else( /*int*/foo, /*int*/bar )。初始化列表中的元素是对构造函数的调用。

标签: c++ inheritance constructor class multiple-inheritance


【解决方案1】:

C++ 的语法非常模棱两可,因此请准备好迎接许多具有不同语义的相似语法结构。

例如:

A a(b);

这可能意味着通过使用参数值“b”调用其构造函数“A::A”来创建“A”类的对象“a”。 但这也可以是函数“a”的声明,该函数具有“b”类型的形式参数并返回“A”类型的值。

对于“简单”语法,编译器可以实现为包含几乎独立模块的管道:Lexer、Parser、Semantic Analyzer 等。 通常,此类语言不仅易于编译器决定,而且易于人类(程序员)理解。

C++ 的语法(和语义)非常复杂。 因此,没有语义信息,C++ 解析器无法决定应用哪个语法规则。 这导致了设计和实现 C++ 编译器的困难。 此外,C++ 也让程序员理解程序变得困难。

所以你理解语法的问题的根源不在你的头脑中,而是在 C++ 的语法中。

以上原因导致建议不要使用 C++(和其他过于复杂的语言)来教授初学者编程。 首先,使用简单(但足够强大)的语言来培养编程技能,然后转向主流语言。

【讨论】:

  • 不,这似乎是错误的。函数声明只能出现在函数之外……那怎么会模棱两可呢?
  • 哦。等待。我刚刚意识到……您可以在函数之外初始化值。好的,所以,你明白了……编译器如何解决这个问题? d-:
【解决方案2】:

在你的构造函数中,你可以为你的成员变量显式调用构造函数。

class FileOpener
{
public:
  // Note: no FileOpener() constructor
  FileOpener( string path ){ //Opens a file }
};
class A
{
public:
  A():b("../Path/To/File.txt"){}
  FileOpener b;
};

当您的成员变量没有默认构造函数时,这是必不可少的。

同样,当父类的默认构造函数不存在或不存在时,您可以显式调用父类的构造函数。

class F
{
public:
  // Note: No default constructor again.
  F( int arg ){ var = arg;}
private:
  int var;
};
class D : public F
{
  D(){} //Compiler error! Constructors try to use the parent's default C 
        // constructor by default.
  D( int arg ):C(arg){} //This works!
};

无论如何,像这样显式调用构造函数称为初始化器。

编辑:请务必按照您在标头中声明它们的顺序初始化成员,否则您的代码将在编译时出现警告。

【讨论】:

  • 请特别注意,您不会弄错的,初始化列表中的代码顺序由声明/继承链决定。说 Base(), member() 与 member(), Base() 没有什么不同。在这两种情况下,Base() 都将首先运行。
  • 啊,对。更新注释 Re: 属性声明顺序。我倾向于使用 -Werror 打开,所以我对初始化命令非常敏感。将基本初始化程序放在成员初始化程序之后会导致警告吗?
【解决方案3】:

本书试图解释 C++初始化列表。通常,初始化列表由对父类构造函数和类属性构造函数的构造函数调用组成。

初始化列表应包括(按顺序):

  1. 基类构造函数
  2. 类属性构造函数

首先应该调用所有基类构造函数。基类构造函数调用的顺序由编译器定义。如C++ FAQ所述:

[基类构造函数]...是 按照它们出现的顺序执行 深度优先从左到右遍历 基类图,左侧 右指的顺序 基类名称的外观。

因此,初始化列表中基类构造函数的顺序无关紧要。如果初始化列表中没有明确列出基类构造函数,则将调用默认构造函数。

在基类构造函数调用之后是类属性构造函数调用。这些看起来像函数调用,但本质上是一种有效的初始化变量的方法,称为构造函数初始化。例如,下面这段 C++ 代码是完全有效的:

int i(0);

注意初始化列表中类属性的顺序要与类头中定义的顺序一致。

最后值得一提的是,使用初始化列表是一种很好的做法。它比在构造函数体中使用赋值更有效,因为它消除了使用默认值或未定义值的类属性的初始构造,并确保了严格的初始化顺序。

【讨论】:

  • 从 FAQ 中读到 sn-p 时,在我看来,它们将按照它们在 class 的原始文件中定义的顺序进行遍历声明,不是基于 initialization list 声明的顺序...即class foo : bar, gazfoo() : gaz(), bar() 将被执行为bar(), gaz()...对吗?还是我不在基地?
【解决方案4】:

构造函数定义的语法是:

Type( parameter-list ) : initialization-list 
{
   constructor-body
};

“initialization-list”是一个逗号分隔的对基础和/或成员属性的构造函数的调用列表。需要初始化没有默认构造函数、常量子对象和引用属性的任何子对象(基类或成员),并且在所有其他情况下,应优先于构造函数块中的赋值。

struct base {
   base( int ) {};
};
struct base2 {
   base2( int ) {};
};
struct type : base, base2
{
   type( int x ) 
      : member2(x), 
        base2(5), 
        base(1), 
        member1(x*2) 
   { f(); }
   int member1;
   int member2;
};

初始化列表的执行顺序在类声明中定义:基类按照声明顺序,成员属性按照声明顺序。在上面的示例中,在构造函数主体中执行 f() 之前,该类将按以下顺序初始化其基类和属性:

  1. 使用参数 1 调用 base(int) 构造函数
  2. 使用参数 5 调用 base2(int) 构造函数
  3. 用值x*2初始化member1
  4. 用值x初始化member2

当你抛出虚拟继承时,虚拟基类在虚拟继承层次结构的最派生类中初始化,因此它可以(或者如果没有默认构造函数,则必须)出现在该初始化列表中。在这种情况下,虚拟基础将在从该基础虚拟继承的第一个子对象之前初始化。

class unrelated {};
class base {};
class vd1 : virtual base {};
class vd2 : virtual base {};
struct derived : unrelated, vd1, vd2 {
   derived() : unrelated(), base(), vd1(), vd2() {} // in actual order
};

在编辑 2 中

我认为您没有阅读答案中的详细信息。初始化列表中的元素是构造函数调用,而不是声明。如果合适,编译器会为调用应用通常的转换规则。

struct base {
   base( int x, double y );
   explicit base( char x );
};
struct derived : base {
   derived() : base( 5, 1.3 ) {}
   derived( int x ) : base( x, x ) {} 
      // will convert x into a double and call base(int,double)
   derived( double d ) : base( 5 ) {} 
      // will convert 5 to char and call base(char)
// derived( base b ) {} // error, base has no default constructor
// derived( base b, int x ) : base( "Hi" ) {} 
      // error, no constructor of base takes a const char *
};

【讨论】:

  • 简洁一些。您不需要在每个类中添加额外的public:。再说一次,在真正的代码中,除了小仿函数之外,我几乎从不使用它。整个格式也是如此......我会在这里和那里使用额外的换行符,但答案已经足够长了,没有额外的空间。
  • class X { 等价于struct X { private:,反之亦然:struct X { 等价于class X { public:
  • @elliot - 一个类的默认访问是私有的,而一个结构是公共的。这确实是它们之间的唯一区别。
  • SP说的是真的,而且structs默认继承是public,而class默认继承是private。 struct X : A {} 与 struct X : public A {} 相同,而 class X : A {} 与 class X 相同: private A {}
  • 似乎值得坚持这样一个事实,即定义初始化列表的顺序无关紧要:控制初始化顺序的是类定义。如果您自己的列表组织方式不同,好的编译器会发出警告,以便您检查。
【解决方案5】:

在构造函数初始化器列表中,您编写 data_member(val) 以便用 val 初始化 data_member。请注意,val 可能是一个表达式,甚至是一个只能在运行时计算的表达式。如果数据成员是一个对象,它将使用该值调用它的构造函数。此外,如果它的构造函数需要多个参数,您可以将它们全部传递,就像在函数调用中一样,例如 data_member(i, j, k)。现在,出于此类初始化的目的,您应该将对象的基类部分视为一个数据成员,其名称只是基类的名称。因此MyBase(val)MyBase(i, ,j ,k)。将调用基类的构造函数。多重继承以相同的方式工作。只需将您想要的任何基类初始化为列表中的单独项目:MyBase1(x), MyBase2(y)。如果这些基类存在,您没有显式调用其构造函数的基类将由它们的默认构造函数初始化。如果不这样做,除非您显式初始化,否则代码将无法编译。

【讨论】:

    【解决方案6】:

    这个成语叫做initialization list

    基本上每个项目都调用一个构造函数:

    class C: public A, public B {
        int a;
        std::string str;
    
    public:
        C(): 
            A(5),            // 1
            B('c'),          // 2
            a(5),            // 3
            str("string")    // 4
        {};
    };
    

    在 (1) 处调用以int 作为参数的基类构造函数,或者可以执行适当的转换。

    在 (2) 处调用以char 为参数的基类构造函数

    在 (3) 处,您调用“构造函数”来初始化 int,在这种情况下是简单的赋值

    在 (4) 处,您调用 std::string(const char*) 构造函数。

    【讨论】:

      【解决方案7】:

      编译器可以确定您正在调用基类的构造函数的天气或您正在进行初始化的天气。

      示例 1:

      class something : something_else {
        void something(int foo, double bar) : something_else(int foo) {}
      };
      

      编译器可以看到您提供的名称属于一个基类。因此它将调用基类中相应的构造函数。

      示例 2:

      class something : something_else {
      private:  const int constant_member;
      public:   something(int foo, double bar) : constant_member(42) {}
      };
      

      编译器可以看到您有一个名为 constant_member 的成员变量作为您的类的一部分,因此它将使用提供的值对其进行初始化。

      您可以在同一个初始化列表中初始化成员和调用基类构造函数(这就是构造函数中的函数声明语法——初始化列表)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-06-24
        • 1970-01-01
        • 1970-01-01
        • 2016-02-29
        • 1970-01-01
        相关资源
        最近更新 更多