c++ 为什么我应该更喜欢使用成员初始值设定项列表?

hc2pp10m  于 2023-06-25  发布在  其他
关注(0)|答案(9)|浏览(149)

我倾向于在构造函数中使用成员初始化列表,但我早就忘记了这背后的原因。
你在构造函数中使用成员初始化列表吗?如果是这样,为什么?若否,原因为何?

laawzig2

laawzig21#

在运行构造函数体之前,父类的所有构造函数以及字段的所有构造函数都会被调用。默认情况下,调用无参数构造函数。初始化列表允许您选择调用哪个构造函数以及该构造函数接收的参数。
如果你有一个引用或一个常量字段,或者如果使用的类之一没有默认构造函数,你必须使用一个初始化列表。

wrrgggsh

wrrgggsh2#

// 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“调用。
    1.“Type“的赋值运算符在MyClass()构造函数的主体中调用以进行赋值。
variable = a;

1.最后,“Type”的析构函数被调用为“a”,因为它超出了作用域。
现在考虑使用MyClass()构造函数和Initializer List的相同代码:

// 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(a)。初始化器列表中的参数用于直接复制构造“variable”。
1.为“a“调用“Type“的析构函数,因为它超出了作用域。

x759pob2

x759pob23#

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

wribegjk

wribegjk4#

语法:

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.类中的常量和引用变量
在C中,变量**必须在创建时定义。在C++中,我们必须使用初始化列表在对象创建期间初始化常量和引用变量。如果我们在对象创建后进行初始化(在构造函数体中),我们将得到编译时错误。
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 */
  {

  }
 };

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

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.类构造函数的参数名称和类的Data成员相同:

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. Initialization list 
  2. this operator.
j5fpnvbx

j5fpnvbx5#

只是添加一些额外的信息来演示成员初始化列表可以产生多大的差异。在leetcode 303 Range Sum Query - Immutable,https://leetcode.com/problems/range-sum-query-immutable/中,您需要构造并初始化一个具有特定大小的向量。下面是两种不同的实现方式和速度比较。
不初始化成员列表,获取AC花费212 ms

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 ms。通过这个简单的例子,很明显,成员初始化列表更有效。所有测量均来自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];
}
};
bzzcjhmw

bzzcjhmw6#

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

这将只调用AA(int)构造函数,而不调用它的默认构造函数。在本例中,差异可以忽略不计,但是如果您愿意,可以想象A的默认构造函数做了更多的事情,比如分配内存或打开文件。你不想做不必要的事。
此外,如果一个类没有默认构造函数,或者你有一个const或引用数据成员,你必须使用一个初始化列表:

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

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

dwbf0jvd7#

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

jv4diomz

jv4diomz8#

1.* * 基类初始化**
使用构造函数初始化列表的一个重要原因是基类的初始化。
根据构造顺序,基类应该在子类之前构造。如果没有构造函数初始化列表,这是可能的,如果你的基类有默认的构造函数,它将在进入子类的构造函数之前被调用。
但是,如果你的基类只有参数化的构造函数,那么你必须使用构造函数初始化列表来确保你的基类在子类之前被初始化。
1.* * 初始化只有参数化构造函数的子对象**
1.* * 效率**
使用构造函数初始化列表,您可以将数据成员初始化为代码中所需的精确状态,而不是先将它们初始化为默认状态&然后将它们的状态更改为代码中所需的状态。
1.* * 初始化非静态const数据成员**
如果类中的非静态const数据成员有默认的构造函数&你没有使用构造函数初始化列表,你将无法将它们初始化为预期的状态,因为它们将被初始化为默认状态。
1.* * 引用数据成员初始化**
当编译器输入构造函数时,引用数据成员必须初始化,因为引用不能只是在以后声明和初始化。这只能在构造函数初始化器列表中实现。

k5hmc34c

k5hmc34c9#

除了性能问题之外,还有一个非常重要的问题,我称之为代码的可维护性和可扩展性。
如果一个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
};

如果代码不是由“代码猴”编写的,而是由一个工程师编写的,他基于对自己正在做的事情的更深层次的考虑来做出决策,那么维护会变得更容易,更不容易出错,这不是什么秘密。

相关问题