关于c ++:为什么我更喜欢使用成员初始化列表?

Why should I prefer to use member initialization list?

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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的构造函数:

1
2
3
4
B()
  : a(3)
{
}

这只调用AA(int)构造函数,而不是它的默认构造函数。在这个例子中,差异是可以忽略的,但是设想一下,如果您愿意,A的默认构造函数做得更多,例如分配内存或打开文件。你不想不必要地那样做。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的定义如下:

    1
    2
    3
    4
    5
    6
    struct T
    {
        T() { a = 5; }
    private:
        int a;
    };

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

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

    如果代码不是由"代码猴子"编写的,而是由一名工程师基于对他正在做的事情的深入考虑做出决定,那么维护就容易得多,也不容易出错,这不是一个秘密。


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

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


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 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。首先为"a"调用类型的构造函数。2。在myClass()构造函数的主体内调用"type"的赋值运算符来赋值

    1
    variable = a;
  • 最后,"type"的析构函数被称为"a",因为它超出了范围。

    现在考虑使用带有初始值设定项列表的myClass()构造函数的相同代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 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
    }
    };

    对于初始值设定项列表,编译器将执行以下步骤:

  • 调用"type"类的复制构造函数来初始化:变量(A)。初始值设定项列表中的参数用于直接复制构造"variable"。
  • "type"的析构函数被调用为"a",因为它超出了作用域。

  • 只需添加一些附加信息来演示成员初始化列表可以产生多大的差异。在leetcode 303 range sum query-immutable中,https://leetcode.com/problems/range-sum-query-immutable/,您需要在其中构造并初始化一个具有特定大小的向量,使其为零。下面是两种不同的实现和速度比较。

    没有成员初始化列表,要获得AC,大约需要212毫秒。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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的时间大约是108ms。通过这个简单的例子,很明显,成员初始化列表的效率更高。所有测量均来自LC的运行时间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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];
    }
    };

    Syntax:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      class Sample
      {
         public:
             int Sam_x;
             int Sam_y;

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

    需要初始化列表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     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_x和sam_y。然后在构造函数体中,定义这些成员数据变量。

    用例:

  • 类中的常量和引用变量
  • 在C语言中,变量必须在创建期间定义。同样的方式,在C++中,我们必须使用初始化列表在对象创建过程中初始化const和参考变量。如果我们在对象创建之后(在构造函数体内部)进行初始化,就会得到编译时错误。

  • 没有默认构造函数的sample1(base)类的成员对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     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 */
      {

      }
     };
  • 为将在内部调用派生类构造函数并调用基类构造函数(默认)的派生类创建对象时。如果基类没有默认的构造函数,用户将得到编译时错误。为了避免,我们必须

    1
    2
     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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     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;
        }
     };
  • 众所周知,如果两个变量的名称相同,那么局部变量的优先级最高,而全局变量的优先级最高。在这种情况下,程序考虑"i"值左右两侧的变量。即:i=i作为sample3()构造函数中的局部变量,类成员变量(i)被重写。为了避免,我们必须使用

    1
    2
      1. Initialization list
      2. this operator.