构造函数中的这个奇怪的冒号成员(" : ")语法是什么意思?

440
最近我看到了一个类似以下的例子:
#include <iostream>

class Foo {
public:
  int bar;
  Foo(int num): bar(num) {};
};

int main(void) {
  std::cout << Foo(42).bar << std::endl;
  return 0;
}

这个奇怪的: bar(num)是什么意思?它似乎是用来初始化数据成员的,但我以前从未见过这种语法。它看起来像是一个函数/构造函数调用,但是针对一个int。这对我来说毫无意义。
还有其他类似的神秘语言特性吗?在普通的C++书籍中是找不到的吗?

114
一本没有提到这个的“普通C++书”可能只是一本C语言书,作者只觉得在封面上加上“++”看起来很酷。 - Rasmus Kaj
101
“你在普通的C++书籍中找不到。”哦,亲爱的,请立刻丢掉你的“普通C++书籍”。不要从窗户扔出去——别人可能会捡起来。最好将其撕碎并进行回收。完成了吗?现在请查阅https://dev59.com/_3RC5IYBdhLWcg3wK9yV获取一本新书。 - Steve Jessop
61
这种语言特性并不晦涩难懂,它是对象构造的一个相当重要的特性。 - Charles Salvia
47
实际上,使用初始化列表并不是什么高深的技巧,有时你必须使用它。例如,如果你的类包含一个const成员变量或引用,就必须使用初始化列表。 - Charles Salvia
14个回答

415
Foo(int num): bar(num)    

在C++中,这个结构称为成员初始化列表

简单地说,它将您的成员bar初始化为一个值num


在构造函数中初始化和赋值有什么区别?

成员初始化:

Foo(int num): bar(num) {};

成员赋值:

Foo(int num)
{
   bar = num;
}

使用成员初始化列表和在构造函数体内赋值两种方式来初始化一个成员变量有明显的区别。

当你通过成员初始化列表进行初始化时,构造函数会被调用一次,对象会在一次操作中构造和初始化。

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

正如你所看到的,在后者中创建和分配的额外开销可能对于用户定义的类是相当大的。

Cost of Member Initialization = Object Construction 
Cost of Member Assignment = Object Construction + Assignment

后者实际上等同于:

Foo(int num) : bar() {bar = num;}

前者等同于仅仅是:

Foo(int num): bar(num){}

对于内置的(你的代码示例)或POD类成员,实际上不存在任何开销。


什么时候必须使用成员初始化列表?

如果:

  • 您的类具有引用成员
  • 您的类具有非静态const成员,或者
  • 您的类成员没有默认构造函数,或者
  • 为了初始化基类成员,或者
  • 当构造函数的参数名称与数据成员相同时(这并不是绝对必须的)

则必须(而不是被迫)使用成员初始化列表。


示例代码:

class MyClass {
public:
  // Reference member, has to be Initialized in Member Initializer List
  int &i;
  int b;
  // Non static const member, must be Initialized in Member Initializer List
  const int k;

  // Constructor’s parameter name b is same as class data member
  // Other way is to use this->b to refer to data member
  MyClass(int a, int b, int c) : i(a), b(b), k(c) {
    // Without Member Initializer
    // this->b = b;
  }
};

class MyClass2 : public MyClass {
public:
  int p;
  int q;
  MyClass2(int x, int y, int z, int l, int m) : MyClass(x, y, z), p(l), q(m) {}
};

int main() {
  int x = 10;
  int y = 20;
  int z = 30;
  MyClass obj(x, y, z);

  int l = 40;
  int m = 50;
  MyClass2 obj2(x, y, z, l, m);

  return 0;
}
  • MyClass2没有默认构造函数,因此必须通过成员初始化列表进行初始化。
  • 基类MyClass没有默认构造函数,因此要初始化它的成员,需要使用成员初始化列表。

在使用成员初始化列表时需要注意以下重要点:

类成员变量始终按照在类中声明的顺序进行初始化。

它们不会按照在成员初始化列表中指定的顺序进行初始化。
简而言之,成员初始化列表不能决定初始化的顺序。

鉴于上述情况,始终将Member initialization列表中成员的顺序与其在类定义中声明的顺序保持一致是一个良好的实践。这是因为编译器不会发出警告,如果两个顺序不同,那么一个相对较新的用户可能会将Member Initializer list与初始化顺序混淆,并编写一些依赖于此的代码。


13
@nils 到目前为止,这是最好的答案。Als指出的初始化顺序也非常重要,虽然Visual Studio编译器不会对此发表任何意见,但像gcc这样的其他编译器将失败。还需要注意,根据您的编译器和情况,这并不总是会提高性能或更加有效。 - ForceMagic
1
@ryf9059:你为什么认为这会不方便呢?无论如何你都必须列出它们,那为什么不按照声明的顺序呢? - Alok Save
2
这应该是答案。感谢上帝我往下滚动了,否则我就错过了它。 - Coffee_lover
1
@AlokSave MyClass(int a, int b, int c) : i(a), b(b), k(c) { // Without Member Initializer // this->b = b; } 应该改为这样 MyClass(int &a , int b, int c) : i(a), b(b), k(c) { // Without Member Initializer // this->b = b; } 并且在声明和调用中相应地进行更改。如果不进行此更改,则 i 将引用 a,但 a 无法引用 x,因为它只包含 x 的值,因此间接地 i 也不能引用 x。因此,如果我们修改 i 的值,则它只会修改 a 而不是 x - Abhishek Mane
1
@AbhishekMane 你是正确的,这里有一个相关问题的链接:https://stackoverflow.com/q/67619383/3150802 - Peter - Reinstate Monica
显示剩余4条评论

249

1
使用初始化列表的其他原因也有很多,特别是当初始化顺序很重要时。但可惜的是,它具有如此愚蠢的伪函数调用语法。 - Martin Beckett
除非成员变量在其父对象实例化后包含垃圾(例如I/O缓冲区),否则您应始终初始化它们,因为如果不这样做,您保证会得到垃圾。 - David R Tribble
21
@mgb,初始化列表并不决定初始化的顺序。成员变量的初始化顺序是按照它们在类中声明的顺序进行的,即使这与构造函数中的初始化顺序不同也是如此。 - ScottJ
14
@mgb: 我不认为它是假函数调用语法。它是初始化语法,就像int i(23);std::vector<double> emptyVec(0);std::vector<double> fullVec(10,23.);等等。当然,类型被移除了,因为类型在成员声明中。 - Steve Jessop
1
@Martin:它没有函数调用语法,而是有一个构造语法(例如:new String("Name"))。它更适合于构造函数,而不是Foo(int num) : m_Count = 5。更不用说类必须在此时构造,因为它在这里初始化了。Foo(int num) : Bar = num将无法编译通过。看到Foo(int num) : m_Count(num)似乎有些奇怪,因为原始类型并不是构造的。 - Lee Louviere
显示剩余3条评论

18

这是构造函数初始化。它是在类构造函数中正确初始化成员变量的方法,因为它可以防止默认构造函数被调用。

看下面两个例子:

// Example 1
Foo(Bar b)
{
   bar = b;
}

// Example 2
Foo(Bar b)
   : bar(b)
{
}
在示例1中:
Bar bar;  // default constructor
bar = b;  // assignment
在例子2中:
Bar bar(b) // copy constructor

这一切都关乎效率。


7
我不会说这与效率有关,而是提供了一种初始化某些需要初始化但无法默认初始化的东西的方式。出于某种原因,人们提到常量和引用作为例子,而最明显的例子应该是没有默认构造函数的类。 - AnT stands with Russia
1
我们两个都是对的;在他的例子中,你可以为效率辩护;对于const/reference/no默认构造函数问题,它既是效率又是必要性。我因此给下面的答案点了赞 :)[Farnsworth voice] 它可以做其他事情。为什么不呢? - Josh
1
Bar bar(); // 默认构造函数 你确定吗? - Lightness Races in Orbit
@LightnessRacesinOrbit 只是好奇想知道:根据你的看法,那应该是什么? - ajaysinghnegi

16

这被称为初始化列表。这是一种初始化类成员的方式。与在构造函数体中简单地赋新值给成员相比,使用它有益处。但是,如果您有类成员是常量引用,它们必须进行初始化。


@LightnessRacesinOrbit - 理解了,但是我的回答中常量或引用点仍然有效。 - LeopardSkinPillBoxHat

9

这并不是晦涩难懂,它是C++初始化列表语法

基本上,在你的情况下,x将用_x初始化,y将用_y初始化,z将用_z初始化。


8
另一个人已经向您解释了,您观察到的语法称为“构造函数初始化列表”。此语法使您能够自定义初始化类的基本子对象和成员子对象(与允许它们默认初始化或保持未初始化相反)。
我只想指出,正如您所说,“看起来像构造函数调用”的语法不一定是构造函数调用。在C++语言中,()语法只是一种标准的初始化语法形式。对于不同类型,它的解释也不同。对于具有用户定义构造函数的类类型,它表示一种含义(确实是构造函数调用),对于没有用户定义构造函数的类类型,它表示另一种含义(称为值初始化,对于空的()),对于非类类型,它再次表示不同的含义(因为非类类型没有构造函数)。
在您的情况下,数据成员具有int类型。 int不是类类型,因此它没有构造函数。对于int类型,这个语法意味着简单地“用num的值初始化bar”,就这样完成了,直接进行,没有涉及到任何构造函数,因为再次提醒,int不是类类型,因此它不能有任何构造函数。

@Destructor:我的答案是绝对正确的。Bjarne Stroustrup在他的书中故意和明确地“撒谎”,只是为了简化它。比较一下TC++PL书和C++语言标准的大小,看到区别了吗?相对紧凑的TC++PL的代价是如此明显(并且众所周知)的错误和遗漏,就像你提到的那个(还有其他很多)。因此,更简洁地说:我的答案是正确的,TC++PL是错误的。但对于像你这样刚开始学习的人来说,TC++PL已经足够好了。 - AnT stands with Russia
那么,你认为标准是没有漏洞的吗?C++标准也有漏洞。 - Destructor
@析构函数:当然会有。但是,标准定义了语言。标准所说的一切都是绝对的真理。它可能存在的唯一“漏洞”大多是像自相矛盾的措辞、模棱两可的措辞、不充分的措辞等方面的问题。这些“漏洞”被积极寻找、报告、记录、讨论和解决。意图上的“漏洞”也可能存在,但它们是一个争论的问题。 - AnT stands with Russia
“初始化”的概念和“构造函数”的作用已经存在很长时间了。意图清晰,相应的措辞也很明确。那里没有错误。即使措辞有错误,意图仍然是完全清楚的。意图不在争议中。如果您想挑战这个意图-您可以向委员会提交提案。但目前在C++中只有类类型可以拥有构造函数。 - AnT stands with Russia
好的,非常感谢您抽出时间进行这个深入的解释!!! - Destructor
显示剩余2条评论

6

这是一个初始化列表。它会在构造函数体运行之前初始化成员变量。 请考虑以下示例:

class Foo {
 public:
   string str;
   Foo(string &p)
   {
      str = p;
   };
 };

vs

class Foo {
public:
  string str;
  Foo(string &p): str(p) {};
};

在第一个示例中,str将通过其无参数构造函数进行初始化。
string();

Foo构造函数的主体之前。在foo构造函数内部,

string& operator=( const string& s );

当你使用 str = p; 时,'str' 将会被调用,

而在第二个例子中,str 将通过直接调用其构造函数进行初始化。

string( const string& s );

使用'p'作为参数。


6

我不知道你怎么会错过这个,它非常基础。这是初始化成员变量或基类构造函数的语法。它适用于普通的数据类型以及类对象。


5
在声明中只写一行,就很容易忽略它是初始化列表。 - Martin Beckett

5

您是正确的,这确实是初始化成员变量的一种方式。我不确定这样做有什么好处,除了清楚地表达它是一个初始化之外。在代码中放置“bar=num”可能更容易被移动、删除或误解。


8
好处在于通常更有效率。而且,在某些情况下,例如当您有const成员变量或成员变量是引用时,您必须使用初始化列表。 - Charles Salvia

5

还有一个“好处”

如果成员变量类型不支持空初始化或者是引用类型(无法进行空初始化),那么你必须提供一个初始化列表。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接