【问题标题】:Class construction with initial values具有初始值的类构造
【发布时间】:2011-08-26 16:38:28
【问题描述】:

我是 C++ 的新手,而且对类的整个概念也很陌生 - 我仍在阅读一本书以尝试学习。我正在阅读的书说,当我构造一个类时,我可以通过这样做来分配默认值:

class foo {
public:
   foo(char c, int i);
private:
   char exampleChar;
   int exampleInt;
};

foo::foo(char c, int i):
exampleChar(c),
exampleInt(i)
{}

这段代码(对我来说)看起来很乱,并且不遵循我在其他语言中习惯的规则。 我的问题是,做上面和这个(下面,我个人认为看起来更干净)有什么区别?

foo::foo(char c, int i) {
   exampleChar = c;
   exampleInt = i;
}

我正在考虑的事情是:如果大规模进行,是否存在性能/效率问题 - 还是完全相同?

【问题讨论】:

标签: c++


【解决方案1】:

第一种方式,通过 : exampleChar(c), exampleInt(i) 被称为初始化列表。

如果你用第二种方法,这两个变量是默认构造的首先1然后你给它们赋值。 (当输入构造函数的实际主体时,初始化列表未初始化的任何内容都是默认构造的。)这是浪费时间,因为无论如何您只是覆盖了这些值。对于像 intchar 这样的小类型,这没什么大不了的,但是当这些成员变量是需要大量周期来构造的大类型时,您肯定希望使用初始化列表。

第二种方法不会浪费时间给它们一个默认值然后覆盖它 - 它会将它们的值直接设置为你给它的值(或者如果成员是一个对象,则调用正确的构造函数)。

您可以通过这样做来了解我们的意思:

class MyClass {
public:
    int _i; // our data

    // default constructor
    MyClass() : _i(0) { cout << "default constructor"; }

    // constructor that takes an int
    MyClass(int i) : _i(i) { cout << "int constructor"; }

    // assignment operator
    void operator=(int i) { _i = i; cout << "assignment operator"; }
};

class OtherClass {
public:
    MyClass c;

    OtherClass() {
        c = 54;
    }
};

OtherClass oc;

你会看到的

default constructor
assignment operator

被打印出来。这是两个函数调用,对于其他类来说,可能会很昂贵。

如果你把OtherClass的构造函数改成

OtherClass() : c(54) {   }

你会看到的

int constructor

被打印出来。与两个相比,只有一个电话。这是最有效的方法。

初始化列表也是必须的

  1. 具有没有默认构造函数的类型。你必须在初始化列表中调用正确的构造函数。

  2. 有一个const 成员,你想给它一些价值(而不是永久拥有默认值

  3. 有一个参考成员。您必须在这些上使用初始化列表。

tl;dr:这样做是因为它至少和其他方式一样快,但从不慢,有时甚至快得多。

1 对于像intchar 这样的内置类型,它们实际上根本没有被构造;他们只是拥有之前碰巧拥有的任何记忆的价值。

【讨论】:

  • C++ 不会为基元分配默认值。这就是为什么使用未分配的变量会给你一个警告——这是未定义的行为,因为它包含一个恰好位于该内存块中的随机值。
【解决方案2】:

不同之处在于编译器将始终在第一个用户定义的构造函数语句之前初始化所有成员(按声明顺序)。对于charint,它们都是原始类型,这里的“初始化”实际上意味着“没有初始化”。但是,如果您有一个具有构造函数的成员执行一些实际工作,则该构造函数将被预先调用 - 如果您这样做

foo::foo() {
    myComplexMember = MyComplexClass(42);
}

编译器在调用您的代码之前已经调用了 MyComplexClass 默认构造函数,这是一种资源浪费(如果无法访问默认 ctor,则会出现编译器错误)。

通过使用初始化列表,您可以自定义默认初始化,避免无所事事。显然,这是要走的路。

【讨论】:

    【解决方案3】:

    有些事情你可以做到,否则你无法做到。

    1. 如果其中一个成员没有默认构造函数。这是您可以在施工时启动成员的唯一方法。 (基类也是如此)

    2. 您可以为const 成员赋值。

    3. 您可以在构造函数开始运行之前确保类的已定义状态。

    4. 如果成员是引用,则需要在初始化列表中对其进行初始化。因为引用是不可变的,一开始只能初始化一次(如 const)

    【讨论】:

      【解决方案4】:

      嗯,这是一个典型的常见问题解答问题:http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.6

      在您的情况下,使用 charint 没有区别。

      一般规则:尽可能使用初始化列表,在极少数情况下您可以更喜欢赋值,例如为了提高可读性:

      MyClass::MyClass
      {
        a = b = c = d = e = f = 0;
      }
      

      优于

      class MyClass::MyClass : a(0), b(0), c(0), d(0), e(0), f(0) { }
      

      【讨论】:

        【解决方案5】:

        如果成员有非平凡的构造函数,在下面的代码中首先会调用默认构造函数,然后执行赋值,而在上面的代码中它们只会被初始化一次。所以是的,可能存在性能问题。

        还有一个实际问题:如果是const、引用,或者没有默认构造函数,就不能使用下面的版本。

        【讨论】:

          【解决方案6】:

          这两个选项之间存在细微但重要的区别。顶部的内容称为成员初始化列表。创建对象时,此列表中的成员将初始化为括号中的任何内容。

          当您在构造函数主体中执行 assignment 时,值首先被 初始化,然后是 assigned。我将在下面发布一个简短的示例。

          示例 1:成员初始化

          class foo 
          {
          public:
             foo(char c, int i);
          
          private:
             char exampleChar;
             int exampleInt;
             Bar exampleBar;
          };
          
          foo::foo(char c, int i):
              exampleChar(c),
              exampleInt(i),
              exampleBar()     //Here, a bar is being default constructed
          {
          }
          

          示例 2:构造函数赋值

          class foo 
          {
          public:
             foo(char c, int i, Bar b);
          
          private:
             char exampleChar;
             int exampleInt;
             Bar exampleBar;
          };
          
          foo::foo(char c, int i, Bar b):
              //exampleChar(c),
              //exampleInt(i),
              //exampleBar()  
          {
              exampleChar = c;
              exampleInt = i;
              exampleBar = someOtherBar;  //Here, a bar is being assigned
          }
          

          这就是有趣的地方。请注意,正在分配 exampleBar。在幕后,Bar 实际上首先是默认构造的,即使您没有指定。此外,如果您的 Bar 比简单的结构更复杂,则需要确保实现赋值运算符才能以这种方式对其进行初始化。此外,为了从成员初始化列表中的另一个Bar 初始化Bar,您必须实现复制构造函数!

          示例 3:成员 init 中使用的复制构造函数

          class foo 
          {
          public:
             foo(char c, int i, Bar b);
          
          private:
             char exampleChar;
             int exampleInt;
             Bar exampleBar;
          };
          
          foo::foo(char c, int i, Bar b):
              //exampleChar(c),
              //exampleInt(i),
              exampleBar(b)     //Here, a bar is being constructed using the copy constructor of Bar
          {
              exampleChar = c;
              exampleInt = i;
          }
          

          【讨论】:

            【解决方案7】:

            我会养成使用初始化列表的习惯。当有人将 char 更改为首先调用默认构造函数的某个对象时,它们不会遇到问题,并且对于 const 值的 const 正确性也是如此!

            【讨论】:

              【解决方案8】:
              foo::foo(char c, int i):exampleChar(c),exampleInt(i){}
              

              此构造在 C++ 中称为 成员初始化器列表

              初始化您的成员 exampleChar 到值 cexampleInti


              构造函数内部的初始化和赋值有什么区别? &
              有什么优势?

              使用初始化列表初始化成员和在构造函数主体内为其分配值之间存在差异。

              当您通过初始化列表初始化字段时,构造函数将被调用一次。

              如果您使用赋值,则字段将首先使用默认构造函数初始化,然后使用实际值重新分配(通过赋值运算符)。

              如您所见,后者有额外的创建和分配开销,这对于用户定义的类来说可能是相当大的。

              对于整数数据类型(您使用它)或 POD 类成员,没有实际开销。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2013-12-13
                • 1970-01-01
                • 1970-01-01
                • 2016-11-28
                • 2013-05-23
                • 2020-12-11
                • 1970-01-01
                相关资源
                最近更新 更多