非静态数据成员的成员初始化列表和默认成员初始化有什么区别?

28

我想了解在使用一个形式而非另一个形式时有什么区别(如果有的话)。

代码 1(直接在变量上初始化):

#include <iostream>

using namespace std;

class Test 
{
public:
    Test() {
        cout<< count;
    }

    ~Test();

private:
    int count=10;
};

int main()
{
    Test* test = new Test();
}

代码 2(使用初始化列表在构造函数中初始化):

#include <iostream>

using namespace std;

class Test 
{
public:
    Test() : count(10) {
        cout<< count;
    }

    ~Test();

private:
    int count;
};

int main()
{
    Test* test = new Test();
}

语义上有任何区别,还是只是句法上的不同?


11
@Chiel 目前 C++ 的标准是 C++14。我认为除非问题被标记为 C++03,否则我们可以将其视为基准,对吗? - Richard Hodges
1
@RichardHodges 是的,但它仍然是一个有用的评论。 - OMGtechy
1
@RichardHodges 你有没有在超级计算机上运行过软件?我不得不从我们的代码中分离出这些类型的结构,因为它在IBM BlueGene编译器上无法工作。我认为在注释中提到这一点是值得的。 - Chiel
1
这确实非常错误。我的借口。但是如果我删除它,下面的讨论就没有意义了。 - Chiel
2
没有C++14可能还好,但是没有C++11?太可怕了... - wally
显示剩余6条评论
5个回答

21

成员初始化

在这两种情况下,我们都在谈论成员初始化。请记住,成员在类中声明的顺序中进行初始化

代码2:成员初始化列表

在第二个版本中:

Test() : count(10) {

: count(10) 是一个构造函数初始化器(ctor-initializer),而count(10)是作为成员初始化列表的一部分,是一个成员初始化器。我喜欢把这看作是初始化发生的“真正”或主要方式,但它并不决定初始化的顺序。

代码1:默认成员初始化器

在第一个版本中:

private:
    int count=10;

count有一个默认成员初始化器。它是后备选项。如果在构造函数中没有提供成员初始化器,但在类中确定了初始化成员的顺序,则它将被用作成员初始化器。

根据标准的12.6.2初始化基类和成员,第10项

If a given non-static data member has both a brace-or-equal-initializer and a mem-initializer, the initialization specified by the mem-initializer is performed, and the non-static data member’s brace-or-equal-initializer is ignored. [ Example: Given

struct A {
int i = / some integer expression with side effects / ;
A(int arg) : i(arg) { }
// ...
};

the A(int) constructor will simply initialize i to the value of arg, and the side effects in i’s brace-or-equalinitializer will not take place. —end example ]

需要记住的另一件事是,如果你引入了一个非静态数据成员初始化程序,则在C++11中结构体将不再被视为聚合体,但这在C++14中已经得到更新


区别

使用一种形式与另一种形式相比,有什么不同(如果有)。

  • 区别在于给定两个选项的优先级。直接指定的构造函数初始化程序具有优先权。在两种情况下,我们都通过不同的路径得到成员初始化程序。
  • 最好使用默认成员初始化程序,因为
    • 然后编译器可以使用该信息为您生成构造函数的初始化程序列表,并且它可能能够进行优化。
    • 您可以在一个地方和顺序中查看所有默认值。
    • 它减少了重复。然后,您只能将异常放入手动指定的成员初始化程序列表中。

13
在C++核心准则中(参见下面的注释1),指南C.48推荐第一种方法(类内初始化器)。提供的推理是:

明确表示预计在所有构造函数中使用相同的值。避免重复。避免维护问题。它导致最短和最有效的代码。

事实上,如果你的构造函数除了初始化成员变量之外什么都不做,就像你的问题一样,那么指南C.45更加严格,肯定要使用类内初始化器。它解释说:

使用类内成员初始化器可以让编译器为您生成函数。编译器生成的函数可能更有效率。

即使我没有编写编译器,无法证明它更有效率,我也不会与Stroustrup,Sutter和他们的数百个朋友和同事争论。尽可能使用类内初始化器。
  1. 如果您不熟悉指南,请按链接查看示例代码和更多说明。

7
我能想到的区别是成员初始化列表优先于默认成员初始化
通过默认成员初始化,即在成员声明中包含的花括号或等号初始化程序,在成员初始化列表中省略成员时使用。
如果一个成员具有默认成员初始化程序,并且在构造函数中出现在成员初始化列表中,则忽略默认成员初始化程序。
例如:
class Test 
{
public:
    Test() {}  // count will be 10 since it's omitted in the member initializer list
    Test(int c) : count(c) {} // count's value will be c, the default member initializer is ignored. 
private:
    int count = 10;
};

2

代码本身没有区别。差异将在您有多个构造函数重载且多个计数为10时出现。使用第一种版本,您需要编写的内容较少。

class Test 
{
public:
    Test() = default;
    Test(int b) : b(b) {} // a = 1, c = 3

    ~Test();

private:
    int a = 1;
    int b = 2;
    int c = 3;
};

与第二个版本相比,上面的代码将如下所示:
class Test 
{
public:
    Test() : a(1), b(2), c(3) {}
    Test(int b) : a(1), b(b), c(3) {}

    ~Test();

private:
    int a;
    int b;
    int c;
};

成员变量越多,差异就越大。

你是想继续使用 b(10) 而不是使用你正在接受的 int 参数吗? - kfsone
@kfsone 是的,那是个打字错误。我已经修正了它。 - rozina
为什么你在第二个版本中初始化变量?已经有初始化列表可以完成这个任务了。 - markzzz
@paizza 复制/粘贴错误 ;) 感谢您的注意。 - rozina
说实话,我更喜欢第一个版本,而不是使用初始化列表(这会让人感到困惑)。 - markzzz
你能否在构造函数中使用默认参数呢?如果用户传递参数到构造函数中,它们将覆盖默认参数。 - Galaxy

1

当您在成员声明旁初始化时,这只在C++11及以上版本中有效,因此如果您在C++98/03中,则无法执行此操作。

如果该值从未更改,您可以选择将其设置为constexpr static,编译器则需要不使用任何额外的存储空间来存储该值(只要您不定义它),并在使用该值的地方进行常量传播。

使用按声明语法的一个缺点是必须在头文件中,这将导致每次您想要更改其值时重新编译所有包含该头文件的翻译单元。如果这需要很长时间,那可能是不可接受的。

另一个区别是,使用成员初始化列表可以为每个构造函数更改该值,而使用按声明版本仅允许您为所有构造函数指定一个值(尽管您可以覆盖此值...但我个人会避免这样做,因为它可能会变得非常混乱!)。


作为旁注,这里没有必要使用new来创建Test的实例。当人们从其他语言转换到该语言时,这是一个常见错误,我想让你知道。当然,在您的示例之外,还有许多用途可以这样做。

这并不是真的,by-declaration语法要求值是常量。请参阅此处 - rozina
@rozina 那是一个打字错误,我以为已经删除了,你是在指之前的答案吗? :) - OMGtechy
不。你的第一行意味着:“当使用成员初始化列表语法时,您可以基于非常量(例如构造函数参数)初始化成员。” - rozina

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