为什么我更喜欢使用成员初始化列表?

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

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

159626 次浏览

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

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)
{
}

这将只调用AA(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;
};

除了上面提到的性能原因,如果你的类存储了作为构造函数参数传递的对象的引用,或者你的类有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
};

这不是一个秘密,如果代码不是由“代码猴子”写的,维护起来会容易得多,出错的可能性也会小得多。而是由一个基于对自己所做事情的深入思考而做出决定的工程师。

在运行构造函数体之前,调用父类的所有构造函数,然后调用其字段的所有构造函数。默认情况下,调用无参数构造函数。初始化列表允许您选择调用哪个构造函数以及该构造函数接收哪些参数。

如果有引用或const字段,或者使用的类中有一个没有默认构造函数,则必须使用初始化列表。

// Without Initializer List
class MyClass {
Type variable;
public:
MyClass(Type a) {  // Assume that Type is an already
// declared class and it has appropriate
// constructors and operators
variable = a;
}
};
 

编译器遵循以下步骤创建类型为MyClass的对象:

  1. Type的构造函数首先用于“a”。
  2. " Type "的赋值操作符在MyClass()构造函数体内被调用以赋值。
variable = a;
  1. 最后," Type "的析构函数被调用到" a ",因为它超出了作用域。

现在考虑带有初始化列表的MyClass()构造函数的相同代码:

    // With Initializer List
class MyClass {
Type variable;
public:
MyClass(Type a):variable(a) {   // Assume that Type is an already
// declared class and it has appropriate
// constructors and operators
}
};
 

使用初始化器列表,编译器执行以下步骤:

  1. Type类的复制构造函数被调用来初始化:初始化器列表中的参数用于直接复制构造" variable "。

  2. " Type "的析构函数对于" a "被调用,因为它超出了作用域。

  1. 基类的初始化

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

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

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

  1. 初始化只有参数化构造函数的子对象

  2. <强> < / >强效率

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

  1. 初始化非静态const数据成员

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

  1. 初始化引用数据成员

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

语法:

  class Sample
{
public:
int Sam_x;
int Sam_y;


Sample(): Sam_x(1), Sam_y(2)     /* Classname: Initialization List */
{
// Constructor body
}
};

初始化需求列表:

 class Sample
{
public:
int Sam_x;
int Sam_y;


Sample()     */* Object and variables are created - i.e.:declaration of variables */*
{ // Constructor body starts


Sam_x = 1;      */* Defining a value to the variable */*
Sam_y = 2;


} // Constructor body ends
};

在上面的程序中,当类的构造函数被执行时,Sam_xSam_y被创建。然后在构造函数体中定义这些成员数据变量。

用例:

  1. 类中的Const和Reference变量

在C语言中,变量必须在创建过程中定义。同样的,在c++中,我们必须在创建对象时使用Initialization list初始化Const和Reference变量。如果在对象创建后进行初始化(在构造函数内部),则会得到编译时错误。

  1. Sample1(基)类的成员对象,没有默认构造函数

     class Sample1
    {
    int i;
    public:
    Sample1 (int temp)
    {
    i = temp;
    }
    };
    
    
    // Class Sample2 contains object of Sample1
    class Sample2
    {
    Sample1  a;
    public:
    Sample2 (int x): a(x)      /* Initializer list must be used */
    {
    
    
    }
    };
    

While creating object for derived class which will internally calls derived class constructor and calls base class constructor (default). if base class does not have default constructor, user will get compile time error. To avoid, we must have either

 1. Default constructor of Sample1 class
2. Initialization list in Sample2 class which will call the parametric constructor of Sample1 class (as per above program)
  1. 类构造函数的形参名称和类的数据成员相同:

     class Sample3 {
    int i;         /* Member variable name : i */
    public:
    Sample3 (int i)    /* Local variable name : i */
    {
    i = i;
    print(i);   /* Local variable: Prints the correct value which we passed in constructor */
    }
    int getI() const
    {
    print(i);    /*global variable: Garbage value is assigned to i. the expected value should be which we passed in constructor*/
    return i;
    }
    };
    

As we all know, local variable having highest priority then global variable if both variables are having same name. In this case, the program consider "i" value {both left and right side variable. i.e: i = i} as local variable in Sample3() constructor and Class member variable(i) got override. To avoid, we must use either

  1. Initialization list
2. this operator.

只是添加一些额外的信息,以演示成员初始化列表可以产生多大的不同。在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];
}
};

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