13

如果可能,构造函数应通过初始化列表初始化其所有成员对象。它比通过构造函数体内的赋值来构建构造函数更有效。

有人可以解释一下,为什么在示例的帮助下使用初始化列表更有效?

4

8 回答 8

14

考虑这个程序:

#include <iostream>

struct A {
  A() { std::cout << "A::A()\n"; }
  A(int) { std::cout << "A::(int)\n"; }
  void operator=(const A&) { std::cout << "A::operator=(const A&)\n"; }
};

struct C1 {
  A a;
  C1(int i) { 
    a = i;
  }
};

struct C2 {
  A a;
  C2(int i)  : a(i) {}
};

int main() {
  std::cout << "How expesive is it to create a C1?\n";
  { C1 c1(7); }
  std::cout << "How expensive is it to create a C2?\n";
  { C2 c2(7); }
}

在我的系统(Ubuntu 11.10,g++ 4.6.1)上,该程序产生以下输出:

How expesive is it to create a C1?
A::A()
A::(int)
A::operator=(const A&)
How expensive is it to create a C2?
A::(int)

现在,考虑一下它为什么这样做。在第一种情况下,C1::C1(int),a必须是默认构造的,然后C1才能调用 's 构造函数。然后它必须分配给 via operator=。在我的简单示例中,没有int可用的赋值运算符,因此我们必须A从 int 构造一个 out。因此,不使用初始化器的代价是:一个默认构造函数、一个int构造函数和一个赋值运算符。

在第二种情况下C2::C2(int),仅int调用构造函数。无论默认A构造函数的成本是多少,显然 的成本C2:C2(int)不大于 的成本C1::C1(int)


或者,考虑这个替代方案。假设我们将以下成员添加到A

void operator=(int) { std::cout << "A::operator=(int)\n"; }

然后输出将显示:

How expesive is it to create a C1?
A::A()
A::operator=(int)
How expensive is it to create a C2?
A::(int)

现在不可能普遍地说哪种形式更有效。在您的特定类中,默认构造函数的成本加上赋值的成本是否比非默认构造函数更昂贵?如果是这样,那么初始化列表更有效。否则不是。

我写过的大多数类都会在初始化列表中更有效地初始化。但是,这是一个经验法则,可能并不适用于所有可能的情况。

于 2011-12-05T14:46:22.440 回答
9

好吧,否则你调用默认构造函数然后执行赋值。根据初始化的性质,这要长一步,并且可能会变得非常低效。

于 2011-12-05T14:34:37.753 回答
5

因为它直接初始化,而不是默认初始化然后分配。对于 POD 而言,性能方面可能无关紧要,但如果类型构造函数正在执行繁重的工作,则它会很重要。

此外,在某些情况下,您必须使用 init list,因此您应该始终这样做以保持一致性。

于 2011-12-05T14:35:28.277 回答
2

C++ 常见问题解答

考虑以下使用初始化列表初始化成员对象 x_ 的构造函数:Fred::Fred() : x_(whatever) { }。这样做最常见的好处是提高了性能。例如,如果表达式whatever 与成员变量x_ 的类型相同,则whatever 表达式的结果直接在x_ 内部构造——编译器不会制作对象的单独副本。即使类型不同,编译器在初始化列表上也比在赋值上做得更好。

构建构造函数的另一种(低效)方法是通过赋值,例如:Fred::Fred() { x_ = 不管; }。在这种情况下,表达式whatever 会导致创建一个单独的临时对象,并且这个临时对象被传递给x_ 对象的赋值运算符。然后该临时对象在 ; 处被破坏。那是低效的。

好像这还不够糟糕,在构造函数中使用赋值时还有另一个低效率的来源:成员对象将由其默认构造函数完全构造,例如,这可能会分配一些默认数量的内存或打开一些默认值文件。如果任何表达式和/或赋值运算符导致对象关闭该文件和/或释放该内存(例如,如果默认构造函数没有分配足够大的内存池或者如果它打开错误的文件)。

于 2011-12-05T14:36:11.153 回答
0

防止双重初始化。

class B
{
//whatever
};

class A
{
   B _b;
public:
   A(B& b)
};

现在分两种情况:

//only initializes _b once via a copy constructor
A::A(B& b) : _b(b)
{
}

//initializes _b once before the constructor body, and then copies the new value
A::A(B& b)
{
   //_b is already initialized here
   //....
   //one extra instruction:
   _b = b;
}
于 2011-12-05T14:35:08.090 回答
0

就 POD 类型而言,初始化和赋值应该是等价的,因为如果没有显式执行初始化,它们将保持未初始化状态,因此唯一的操作仍然是赋值。

对于具有默认构造函数和赋值运算符的类,情况有所不同:不是直接在正确的状态下创建对象,而是首先必须调用默认构造函数,然后调用赋值运算符(在构造函数的主体内)。这肯定比从一开始就使用正确的构造函数初始化对象效率低(两步而不是一步,第一步 - 默认构造 - 通常完全浪费)。

直接初始化还产生了一个优点,即您可以确定,如果执行到达构造函数的主体,则所有各种字段都已处于“正确”状态。

于 2011-12-05T14:36:39.907 回答
0

假设你的类中有一个 std::string 类型的数据成员。当执行该类的构造函数时,会自动调用默认的构造函数字符串类,因为对象在构造函数体之前被初始化。

如果您在构造函数的主体内分配字符串,则将创建一个临时对象并将其提供给字符串的赋值运算符。临时对象将在赋值语句结束时被销毁。如果您在初始化列表中执行此操作,则不会创建临时对象。

class Person
{
public:
    Person(string s);
    ~Person();
private:
    string name;
};


// case 1
Person::Person(string s)
{
   name = s;   // creates a temporary object
}

// case 2
Person::Person(string s):name(s) {} // whereas this will not create a temporary object.
于 2011-12-05T14:54:01.623 回答
0

因为如果您使用的是初始化器列表,那么您所调用的是该对象的构造函数副本。

而如果你在构造函数体内初始化对象,你就是在做一个赋值。

示例:这里我调用 int 的复制构造函数。


  myClass::myClass( int x ) : member(x) {}

而在这里我调用 operator=(const int&)。任务


    myClass::myClass( int x )
    {
         member = x;
    }

通常分配比简单的副本执行更多操作。你也必须记下临时对象!

于 2011-12-05T14:56:17.017 回答