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

What is this weird colon-member (" : ") syntax in the constructor?

提问人:nils 提问时间:11/11/2009 最后编辑:Jan Schultkenils 更新时间:6/14/2023 访问量:138875

问:

最近我看到了一个如下示例:

#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++书中找到?

C 语法 构造函数 C++-FAQ ctor-initializer

评论

116赞 Rasmus Kaj 11/11/2009
一本没有提到这一点的“普通 c++ 书”可能是一本 c 书,有人认为“++”在封面上看起来很酷......
102赞 Steve Jessop 11/11/2009
“你永远不会在普通的 C++ 书中找到”。哦。亲爱的。现在就扔掉你的“普通C++书”。不是在窗外 - 其他人可能会捡起它。最好将其切碎并放入回收利用。做?现在咨询 stackoverflow.com/questions/388242/...以获得一本新书。
61赞 Charles Salvia 11/11/2009
这种语言功能并不深奥。这是对象构造的一个相当重要的特征。
47赞 Charles Salvia 11/11/2009
事实上,远非深奥,您通常别无选择,只能使用初始值设定项列表。例如,如果类包含成员变量或引用,则必须使用启动器列表。const

答:

249赞 James McNellis 11/11/2009 #1

这是一个成员初始化列表。你应该在任何一本好的C++书中找到有关它的信息。

在大多数情况下,您应该初始化成员初始化列表中的所有成员对象(但是,请注意 FAQ 条目末尾列出的例外情况)。

FAQ条目的要点是,

在所有其他条件相同的情况下,如果使用初始化列表而不是赋值,则代码运行速度会更快。

评论

0赞 Mark Ransom 11/11/2009
了解术语至关重要——我很嫉妒我没有想到它。
1赞 Martin Beckett 11/11/2009
使用 init 列表还有很多其他原因。尤其是当初始化顺序很重要时。可惜它有这样一个愚蠢的假函数调用语法。
21赞 ScottJ 11/11/2009
@mgb,初始化列表不能确定初始化的顺序。成员变量按照它们在类中声明的顺序进行初始化,即使这与构造函数上的初始化顺序不同。
14赞 Steve Jessop 11/11/2009
@mgb:我不认为这是一个虚假的函数调用语法。它是初始化语法,如 、 、 等。当然,只有在删除类型的情况下,因为该类型位于成员声明中。int i(23);std::vector<double> emptyVec(0);std::vector<double> fullVec(10,23.);
1赞 Lee Louviere 4/23/2011
@Martin :它没有函数调用语法,它有一个构造语法(ala: new String(“Name”))。它比 Foo(int num) 更适合构造函数:m_Count = 5。更不用说无论如何都必须在这一点上构造类,因为它是在这里初始化的。Foo(int num) : Bar = num,无法正确编译。看到 Foo(int num) : m_Count(num) 似乎很奇怪,因为原始类型没有被构造。
5赞 Aric TenEyck 11/11/2009 #2

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

评论

8赞 Charles Salvia 11/11/2009
好处是它通常更有效率。而且,在某些情况下,例如,当您具有成员变量或作为引用的成员变量时,您必须使用初始值设定项列表。const
6赞 Mark Ransom 11/11/2009 #3

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

评论

5赞 Martin Beckett 11/11/2009
写在声明中的一行上,很容易不将其发现为初始化列表
18赞 Josh 11/11/2009 #4

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

请看以下两个例子:

// 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 11/11/2009
我不会说这与效率有关。它是关于提供一种方法来初始化需要初始化但不能默认初始化的东西。出于某种原因,人们提到常量和引用作为示例,而最明显的例子是没有默认构造函数的类。
1赞 Josh 11/11/2009
我们俩都是对的;在他的例子中,你可以为效率辩护;对于 const/reference/no default 构造函数问题,它既是效率又是必要的。我在下面投了赞成票,因为它:)[法恩斯沃斯配音]它可以做其他事情。为什么不呢?
1赞 Lightness Races in Orbit 12/27/2015
Bar bar(); // default constructor是否确定?
0赞 asn 8/2/2019
@LightnessRacesinOrbit 只是想知道:根据你的说法,这应该是什么?
16赞 LeopardSkinPillBoxHat 11/11/2009 #5

这称为初始化列表。这是一种初始化类成员的方法。使用它而不是简单地将新值分配给构造函数主体中的成员是有好处的,但是如果类成员是常量引用则必须初始化它们。

评论

0赞 LeopardSkinPillBoxHat 5/1/2013
@LightnessRacesinOrbit - 采取了点,但常量或参考点在我的答案中仍然有效。
6赞 nos 11/11/2009 #6

这是一个初始化列表。它将在运行构造函数主体之前初始化成员。 考虑

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

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

在第一个示例中,str 将由其无参数构造函数初始化

string();

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

string& operator=( const string& s );

将在 'str' 上调用,就像你做 str = p 一样;

在第二个示例中,str 将直接由 调用其构造函数

string( const string& s );

以“p”为参数。

5赞 pm100 11/11/2009 #7

还有另一个“好处”

如果成员变量类型不支持 null 初始化,或者它是引用(不能初始化 null),则别无选择,只能提供初始化列表

8赞 AnT stands with Russia 11/11/2009 #8

另一个已经向你解释过,你观察到的语法称为“构造函数初始值设定项列表”。此语法允许您自定义初始化类的基子对象和成员子对象(而不是允许它们默认初始化或保持未初始化状态)。

我只想指出,正如您所说,“看起来像构造函数调用”的语法不一定是构造函数调用。在 C++ 语言中,语法只是初始化语法的一种标准形式。对于不同的类型,它的解释不同。对于具有用户定义构造函数的类类型,它意味着一件事(它确实是一个构造函数调用),对于没有用户定义构造函数的类类型,它意味着另一件事(所谓的值初始化)对于空),对于非类类型,它再次意味着不同的东西(因为非类类型没有构造函数)。()()

在本例中,数据成员的类型为 。 不是类类型,因此它没有构造函数。对于类型,此语法的意思是简单地“使用 ” 的值初始化,仅此而已。它就是这样完成的,直接不涉及构造函数,因为,再一次,它不是类类型,因此它不能有任何构造函数。intintintbarnumint

评论

0赞 Destructor 5/11/2016
但是bjarne stroustrup在他的书TC++PL和C++编程语言中说“内置类型也有默认构造函数”。geeksforgeeks.org/c-default-constructor-built-in-types & informit.com/guides/content.aspx?g=cplusplus&seqNum=15 还表示内置类型具有构造函数。我个人通过邮件向 bjarne 提出了这个问题,他说是的,内置类型也有构造函数。所以你的答案是错误的!!
0赞 AnT stands with Russia 5/11/2016
@Destructor:我的回答是绝对正确的。Bjarne Stroustrup 为了简化它,故意在他的书中明确撒谎。比较 TC++PL 书籍的大小和 C++ 语言标准的大小。看到区别了吗?TC++PL 的相对紧凑性所付出的代价是像您提到的那样明显(并且众所周知)错误和遗漏(还有很多其他错误和遗漏)。所以,更简洁地说:我的答案是对的,TC++PL是错的。但对于像您这样刚刚开始学习的人来说,TC++PL 已经足够好了。
0赞 AnT stands with Russia 5/11/2016
请不要告诉我们关于“通过邮件向 bjarne 提出这个问题”的巨魔童话故事。这个问题又是众所周知的,很久以前就已经进行了详尽的讨论和结束。(但即使他对你说了这样的话,也没关系。最后,唯一重要的是语言规范说了什么。比亚恩·斯特鲁斯特鲁普(Bjarne Stroustrup)所说的是无关紧要的。您链接的 geeksforgeeks 上的帖子完全是假的。
0赞 Destructor 5/11/2016
那么,你认为这个标准是没有错误的吗?C++标准也有错误。
0赞 AnT stands with Russia 5/11/2016
@Destructor:当然可以。但是,该标准定义了语言。根据定义,标准所说的一切都是绝对真理。它可能存在的唯一“错误”主要是与措辞有关的东西,例如自相矛盾的措辞、模棱两可的措辞、指定不足等等。这些错误被强烈地寻找、报告、记录、讨论和解决。意图中的“错误”也可能存在,但它们是一个有争议的问题。
9赞 wkl 12/12/2010 #9

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

基本上,在您的情况下,将使用 、 和 初始化。x_xy_yz_z

5赞 suszterpatt 12/12/2010 #10

它是构造函数的初始化列表。这些成员将立即使用这些值进行初始化,而不是默认构造,然后为它们分配参数中接收的值。这对 s 来说似乎不是很有用,但对于构造成本高昂的自定义类来说,它可以节省相当多的时间。xyzfloat

415赞 Alok Save 12/16/2011 #11
Foo(int num): bar(num)    

此构造在 C++ 中称为成员初始值设定项列表

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


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

成员初始化:

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没有默认构造函数,因此必须通过成员初始值设定项列表进行初始化。
  • 基类没有默认构造函数,因此要初始化其成员,需要使用 Member Initializer List。MyClass

使用成员初始值设定项列表时要注意的要点:

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

它们不会按照在“成员初始值设定项列表”中指定的顺序进行初始化。
简而言之,成员初始化列表并不能确定初始化的顺序。

鉴于上述情况,最好将成员初始化的顺序与在类定义中声明成员的顺序保持相同。这是因为编译器不会警告这两个顺序是否不同,但相对较新的用户可能会将成员 Initializer 列表混淆为初始化顺序,并编写一些依赖于该顺序的代码。

评论

13赞 ForceMagic 3/14/2012
@nils 这是迄今为止最好的答案。Als 指出的初始化顺序也非常重要,虽然 Visual Studio 编译器不会对此发表任何说明,但其他编译器(如 gcc)将失败。同样重要的是要注意,根据您的编译器和情况,这并不总是正确的,这将提高性能或提高效率。
1赞 Alok Save 1/7/2013
@ryf9059:你为什么觉得会不方便?无论如何,您都必须列出它们,那么为什么不按照与声明相同的顺序列出呢?
2赞 Coffee_lover 8/13/2015
这应该是答案。谢天谢地,我向下滚动,否则我会错过它。
1赞 Abhishek Mane 5/15/2021
@AlokSave MyClass(int a, int b, int c) : i(a), b(b), k(c) { // 不带成员初始值设定项 // this->b = b;} 它应该是这样的 MyClass(int &a , int b, int c) : i(a), b(b), k(c) { // 没有成员初始值设定项 // this->b = b;}等声明和调用的相应变化。如果没有此更改,将引用但不能引用,因为它只包含值,因此间接无法引用。因此,如果我们修改 的值,那么它只是修改而不是iaaxxixiax
1赞 Peter - Reinstate Monica 5/20/2021
@AbhishekMane 你是对的,这里有一个指向相关问题的链接:stackoverflow.com/q/67619383/3150802
3赞 M.M 5/30/2017 #12

此线程中尚未提及:从 C++11 开始,成员初始值设定项列表可以使用列表初始化(又名。“统一初始化”、“支撑初始化”):

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

它与其他上下文中的列表初始化具有相同的语义。

3赞 Karen Baghdasaryan 8/24/2021 #13

虽然这是一个古老的讨论,但我找不到任何关于委托构造函数的提及,它以以下方式使用奇怪的 “:” 符号。

class Foo 
{
public: 
    Foo(char x, int y) 
    {}
    Foo(int y) : Foo('a', y) 
    {}
};

它所做的只是委派给 .因此Foo(y)Foo('a', y)

Foo foo(15); //=> foo('a', 15)

定义委托构造函数时,除了目标构造函数之外,初始值设定项列表中不能有任何成员。

1赞 Gabriel Staples 9/1/2022 #14

类构造函数中的冒号语法 () 是什么,将整数传递给构造函数有什么作用?:std::vector<>

我想从这个重复的问题中解释下面的例子:

class UnionFind {
    public:
        UnionFind(int sz) : root(sz) {
            for (int i = 0; i < sz; i++) {
                root[i] = i;
            }
        }
    private:
        vector<int> root;
    };
    
    
    int main() {
        
       
        UnionFind uf(10);
    }

冒号 () 表示“初始化列表”或“初始值设定项列表”的开始,该列表将每个变量初始化为括号中的值。就好像您为每个变量调用了一个构造函数,括号中的该值被传递到该变量的构造函数中。:

因此,使用 初始化变量 ,这就像做 .但是,这样做允许传递给类构造函数。: root(sz)rootint szvector<int> root(sz);szUnionFind

初始化一个大小如下的向量是构造函数 #3 这里 (https://en.cppreference.com/w/cpp/container/vector/vector):

// Constructor (3) as shown at
// https://en.cppreference.com/w/cpp/container/vector/vector
explicit vector( size_type count,
                 const T& value = T(),
                 const Allocator& alloc = Allocator());

它将(在上面的例子中)多个元素构造到向量中,每个元素都有值,这意味着在这种情况下,由于是一个 . 看起来像一个函数调用,但基本上是值零 () 的整数默认构造函数。它被称为“值初始化”。可以把它想象成调用一个“整数构造函数”,如果整数是对象并且有构造函数。另请参阅我的问题:在 C++ 中调用 char() 作为函数是什么?countszT()int()rootvector<int>int()0

所以,让我们回顾一下:在构造函数中就像构造一样,它在向量中创建许多元素,每个元素都有初始值,这是 到 0 的“值初始化”的语法。: root(sz)vector<int> root(sz);szrootint()int

另请注意,传递给上面所示的构造函数 #3 的参数实际上应该是一个 ,它可以写成 ,并且通常是 (请参阅此处的“成员类型”部分)。因此,最好改为。因此,请将此构造函数行:更改为 this: 。countsize_typestd::vector::size_typesize_tint szsize_t szUnionFind(int sz) : root(sz) {UnionFind(size_t sz) : root(sz) {