我倾向于使用我的构造函数的成员初始化列表…但我早就忘了这背后的原因了……

在构造函数中使用成员初始化列表吗?如果有,为什么?如果不是,为什么不是?


当前回答

正如c++核心指南C.49中所解释的:在构造函数中首选初始化而不是赋值 它可以防止对默认构造函数的不必要调用。

其他回答

除了上面提到的性能原因,如果你的类存储了作为构造函数参数传递的对象的引用,或者你的类有const变量,那么你除了使用初始化列表之外没有任何选择。

只是添加一些额外的信息,以演示成员初始化列表可以产生多大的不同。在leetcode 303 Range Sum Query - Immutable, https://leetcode.com/problems/range-sum-query-immutable/中,您需要构造一个具有一定大小的向量并将其初始化为零。这里是两种不同的实现方式和速度的比较。

没有成员初始化列表,获得AC花费了我大约212毫秒。

class NumArray {
public:
vector<int> preSum;
NumArray(vector<int> nums) {
    preSum = vector<int>(nums.size()+1, 0);
    int ps = 0;
    for (int i = 0; i < nums.size(); i++)
    {
        ps += nums[i];
        preSum[i+1] = ps;
    }
}

int sumRange(int i, int j) {
    return preSum[j+1] - preSum[i];
}
};

现在使用成员初始化列表,获得AC的时间大约是108毫秒。通过这个简单的例子,很明显,成员初始化列表更加有效。所有的测量都来自LC的运行时间。

class NumArray {
public:
vector<int> preSum;
NumArray(vector<int> nums) : preSum(nums.size()+1, 0) { 
    int ps = 0;
    for (int i = 0; i < nums.size(); i++)
    {
        ps += nums[i];
        preSum[i+1] = ps;
    }
}

int sumRange(int i, int j) {
    return preSum[j+1] - preSum[i];
}
};

基类的初始化

使用构造函数初始化列表的一个重要原因是基类的初始化,这里的回答中没有提到。

根据构造顺序,基类应该在子类之前构造。没有构造函数初始化列表,如果你的基类有默认构造函数,它将在进入子类的构造函数之前被调用,这是可能的。

但是,如果基类只有参数化的构造函数,则必须使用构造函数初始化列表来确保基类在子类之前初始化。

只有参数化构造函数的子对象的初始化 效率

使用构造函数初始化列表,将数据成员初始化为代码中需要的确切状态,而不是先将它们初始化为默认状态,然后再将它们的状态更改为代码中需要的状态。

初始化非静态const数据成员

如果类中的非静态const数据成员具有默认构造函数,并且你不使用构造函数初始化列表,你将无法将它们初始化为预期状态,因为它们将初始化为默认状态。

初始化引用数据成员

引用数据成员必须在编译器进入构造函数时初始化,因为引用不能在以后声明和初始化。这只有在构造函数初始化列表中才可能实现。

除了性能问题,还有一个非常重要的问题,我称之为代码可维护性和可扩展性。

如果T是POD,并且你开始倾向于初始化列表,那么如果有一次T将改变为非POD类型,你不需要改变初始化周围的任何东西,以避免不必要的构造函数调用,因为它已经优化了。

如果类型T确实有默认构造函数和一个或多个用户定义构造函数,并且有一次您决定删除或隐藏默认构造函数,那么如果使用了初始化列表,则不需要更新用户定义构造函数的代码,因为它们已经正确实现了。

与const成员或引用成员一样,假设最初的T定义如下:

struct T
{
    T() { a = 5; }
private:
    int a;
};

接下来,你决定将a限定为const,如果你从一开始就使用初始化列表,那么这是一个单行更改,但有了上面定义的T,它还需要挖掘构造函数定义以删除赋值:

struct T
{
    T() : a(5) {} // 2. that requires changes here too
private:
    const int a; // 1. one line change
};

如果代码不是由“代码猴子”编写的,而是由工程师根据对所做事情的深入考虑来做决定,那么维护就会更容易,更不容易出错,这不是什么秘密。

对于POD的成员来说,这没有什么区别,只是风格的问题。对于属于类的类成员,则避免不必要地调用默认构造函数。考虑:

class A
{
public:
    A() { x = 0; }
    A(int x_) { x = x_; }
    int x;
};

class B
{
public:
    B()
    {
        a.x = 3;
    }
private:
    A a;
};

在这种情况下,B的构造函数将调用A的默认构造函数,然后将A .x初始化为3。更好的方法是让B的构造函数直接在初始化列表中调用A的构造函数:

B()
  : a(3)
{
}

这将只调用A的A(int)构造函数,而不调用其默认构造函数。在这个例子中,差异可以忽略不计,但是想象一下A的默认构造函数做了更多的工作,比如分配内存或打开文件。你不会想做不必要的事情。

此外,如果一个类没有默认构造函数,或者你有一个const成员变量,你必须使用初始化列表:

class A
{
public:
    A(int x_) { x = x_; }
    int x;
};

class B
{
public:
    B() : a(3), y(2)  // 'a' and 'y' MUST be initialized in an initializer list;
    {                 // it is an error not to do so
    }
private:
    A a;
    const int y;
};