什么时候需要用到初始化列表

时间:2021-05-04 16:01:36
初始化列表用于对类的数据成员进行初始化。成员列表在构造函数中进行初始化,跟在构造函数的冒号后面。
参考下面例子:

  1. #include<iostream>  
  2.   
  3. class Point  
  4. {  
  5. private:  
  6.     int x;  
  7.     int y;  
  8. public:  
  9.     Point(int i = 0, int j = 0):x(i), y(j) {}  
  10.     /*  上面的初始化列表是可选的,因为也可以写成下面的形式: 
  11.         Point(int i = 0, int j = 0) { 
  12.             x = i; 
  13.             y = j; 
  14.         } 
  15.     */  
  16.     int getX() const {return x;}  
  17.     int getY() const {return y;}  
  18. };  
  19.   
  20. int main()  
  21. {  
  22.   Point t1(11, 22);  
  23.   std::cout<<”x = ”<<t1.getX()<<“, ”;  
  24.   std::cout<<”y = ”<<t1.getY();  
  25.   return 0;  
  26. }  
#include<iostream>

class Point
{
private:
    int x;
    int y;
public:
    Point(int i = 0, int j = 0):x(i), y(j) {}
    /*  上面的初始化列表是可选的,因为也可以写成下面的形式:
        Point(int i = 0, int j = 0) {
            x = i;
            y = j;
        }
    */
    int getX() const {return x;}
    int getY() const {return y;}
};

int main()
{
  Point t1(11, 22);
  std::cout<<"x = "<<t1.getX()<<", ";
  std::cout<<"y = "<<t1.getY();
  return 0;
}
输出:
x = 11, y = 22

上面代码是对初始化列表的简单演示. x和y也能在构造函数体中进行初始化。
但是,在某些情形下,数据成员不能在构造函数中初始化,而只能使用初始化列表。下面列出了这些情形:

1) non-static const 非静态常量数据成员的初始化:

const 数据成员必须使用初始化列表. 参考下面例子中的成员变量t.




  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. class Test  
  5. {  
  6.     const int t;  
  7. public:  
  8.     Test(int t):t(t) {}  //必须使用初始化列表  
  9.     int getT() { return t; }  
  10. };  
  11.   
  12. int main()  
  13. {  
  14.     Test t1(111);  
  15.     cout<<t1.getT();  
  16.     return 0;  
  17. }  
#include<iostream>
using namespace std;

class Test
{
    const int t;
public:
    Test(int t):t(t) {}  //必须使用初始化列表
    int getT() { return t; }
};

int main()
{
    Test t1(111);
    cout<<t1.getT();
    return 0;
}


输出:
111

2) 引用成员的初始化:
引用成员必须使用初始化列表. 参考下面例子中的’t’.

  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. class Test  
  5. {  
  6.     int &t;  
  7. public:  
  8.     Test(int &t):t(t) {}  //必须使用初始化列表  
  9.     int getT() { return t; }  
  10. };  
  11.   
  12. int main()  
  13. {  
  14.     int x = 22;  
  15.     Test t1(x);  
  16.     cout<<t1.getT()<<”, ”;  
  17.     x = 33;  
  18.     cout<<t1.getT()<<endl;  
  19.     return 0;  
  20. }  
#include<iostream>
using namespace std;

class Test
{
    int &t;
public:
    Test(int &t):t(t) {}  //必须使用初始化列表
    int getT() { return t; }
};

int main()
{
    int x = 22;
    Test t1(x);
    cout<<t1.getT()<<", ";
    x = 33;
    cout<<t1.getT()<<endl;
    return 0;
}

输出:
22, 33

3) 初始化没有默认构造函数的成员对象:
下面例子中, 类’B’ 的数据成员’a’是类’A’的一个对象, 并且’A’没有默认构造函数,则’B’必须使用初始化列表来对’a’进行初始化。




  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class A  
  5. {  
  6.   int i;  
  7. public:  
  8.   A(int);  
  9. };  
  10.   
  11. A::A(int arg)  
  12. {  
  13.   i = arg;  
  14.   cout << ”A’s Constructor called: Value of i: ” << i << endl;  
  15. }  
  16.   
  17. // 类B包含A的一个对象  
  18. class B  
  19. {  
  20.   A a;  
  21. public:  
  22.   B(int);  
  23. };  
  24.   
  25. B::B(int x) : a(x)   //必须使用初始化列表  
  26. {  
  27.   cout << ”B’s Constructor called”;  
  28. }  
  29.   
  30. int main()  
  31. {  
  32.   B obj(10);  
  33.   return 0;  
  34. }  
#include <iostream>
using namespace std;

class A
{
  int i;
public:
  A(int);
};

A::A(int arg)
{
  i = arg;
  cout << "A's Constructor called: Value of i: " << i << endl;
}

// 类B包含A的一个对象
class B
{
  A a;
public:
  B(int);
};

B::B(int x) : a(x)   //必须使用初始化列表
{
  cout << "B's Constructor called";
}

int main()
{
  B obj(10);
  return 0;
}


输出:
A’s Constructor called: Value of i: 10
B’s Constructor called

如果类A既有默认构造函数又有带参数的构造函数,当想要用默认构造函数来初始化’a’时, 则B不一定需要使用初始化列表;当想要用参数的构造函数来初始化’a’时,则B一定要使用初始化列表。


4) 基类数据成员的初始化: 
与上面的第3条类似, 如果想要带参数的构造函数初始化基类,则子类必须使用初始化列表。
  1. #include <iostream>  
  2.   
  3. class A  
  4. {  
  5.   int i;  
  6. public:  
  7.   A(int);  
  8. };  
  9.   
  10. A::A(int arg)  
  11. {  
  12.   i = arg;  
  13.   std::cout << ”A’s Constructor called: Value of i: ” << i << std::endl;  
  14. }  
  15.   
  16. // Class B 继承自Class A  
  17. class B : A  
  18. {  
  19. public:  
  20.   B(int);  
  21. };  
  22.   
  23. B::B(int x) :  A(x)  
  24. //必须使用初始化列表  
  25.   std::cout << ”B’s Constructor called”;  
  26. }  
  27.   
  28. int main()  
  29. {  
  30.   B obj(10);  
  31.   return 0;  
  32. }  
#include <iostream>

class A
{
  int i;
public:
  A(int);
};

A::A(int arg)
{
  i = arg;
  std::cout << "A's Constructor called: Value of i: " << i << std::endl;
}

// Class B 继承自Class A
class B : A
{
public:
  B(int);
};

B::B(int x) :  A(x)
{ //必须使用初始化列表
  std::cout << "B's Constructor called";
}

int main()
{
  B obj(10);
  return 0;
}
运行结果:
A’s Constructor called: Value of i: 10
B’s Constructor called

5) 当构造函数的参数名字与数据成员的名字相同时:
如果构造函数的参数与数据成员的名字相同,则数据成员必须使用初始化列表,或者带this指针的方式,进行初始化。
参考下面程序中的参数’i’与数据成员’i’。
  1. #include <iostream>  
  2.   
  3. class A  
  4. {  
  5.   int i;  
  6. public:  
  7.   A(int);  
  8.   int getI() const  
  9.   {  
  10.     return i;  
  11.   }  
  12. };  
  13.   
  14. A::A(int i) :  i(i)  // 或者使用初始化列表,或者使用this指针  
  15. {  
  16. }  
  17.   
  18. /* 也可以写出下面代码 
  19.  A::A(int i) { 
  20.  this->i = i; 
  21.  } 
  22.  */  
  23.   
  24. int main()  
  25. {  
  26.   A a(10);  
  27.   std::cout << a.getI();  
  28.   return 0;  
  29. }  
#include <iostream>

class A
{
  int i;
public:
  A(int);
  int getI() const
  {
    return i;
  }
};

A::A(int i) :  i(i)  // 或者使用初始化列表,或者使用this指针
{
}

/* 也可以写出下面代码
 A::A(int i) {
 this->i = i;
 }
 */

int main()
{
  A a(10);
  std::cout << a.getI();
  return 0;
}
输出:
10

6) 性能原因:
使用初始化列表的性能更好。参见下面例子:
  1. #include <iostream>  
  2. class Type  
  3. {  
  4. public:  
  5.   Type()  
  6.   {  
  7.     std::cout << ”constructor called\n”;  
  8.   }  
  9.   ~Type()  
  10.   {  
  11.     std::cout << ”destructor called\n”;  
  12.   }  
  13.   Type(const Type & type)  
  14.   {  
  15.     std::cout << ”copy constructor called\n”;  
  16.   }  
  17.   Type& operator=(const Type & type)  
  18.   {  
  19.     std::cout << ”operator= called\n”;  
  20.     return *this;  
  21.   }  
  22. };  
  23.   
  24. // 不使用初始化列表  
  25. class MyClass  
  26. {  
  27.   Type variable;  
  28. public:  
  29.   MyClass(Type a) //假设Type是一个定义了拷贝构造与赋值操作符的类  
  30.   {  
  31.     variable = a;  
  32.   }  
  33. };  
  34.   
  35. int main()  
  36. {  
  37.   Type type;  
  38.   MyClass mc(type);  
  39.   return 0;  
  40. }  
#include <iostream>
class Type
{
public:
  Type()
  {
    std::cout << "constructor called\n";
  }
  ~Type()
  {
    std::cout << "destructor called\n";
  }
  Type(const Type & type)
  {
    std::cout << "copy constructor called\n";
  }
  Type& operator=(const Type & type)
  {
    std::cout << "operator= called\n";
    return *this;
  }
};

// 不使用初始化列表
class MyClass
{
  Type variable;
public:
  MyClass(Type a) //假设Type是一个定义了拷贝构造与赋值操作符的类
  {
    variable = a;
  }
};

int main()
{
  Type type;
  MyClass mc(type);
  return 0;
}
输出:
constructor called //main中的”Type type”
copy constructor called //使用新建的对象type来创建MyClass构造函数中的参数a
constructor called //MyClass的成员对象variable的构造
operator= called //MyClass构造函数中的”variable = a”
destructor called //a的生命周期结束destructor called //mc析构,其中成员对象variable也会析构
destructor called //type生命周期结束

由上面可知道,编译器会遵循下面顺序来创建一个 MyClass的对象:
1. 调用Type的拷贝构造函数来创建’a’.
2. 调用Type的构造函数来创建成员对象variable.
3. 调用Type的赋值操作符,修改成员对象variable。
        variable = a; 
4. 调用Type的析构函数,因为a的生命周期结束了.

如果MyClass的构造函数使用的是初始化列表,如下面例子所示:
  1. // 使用初始化列表  
  2. class MyClass {  
  3.     Type variable;  
  4. public:  
  5.     MyClass(Type a):variable(a) {   // 假设Type是一个定义了拷贝构造与赋值操作符的类  
  6.     }  
  7. };  
// 使用初始化列表
class MyClass {
    Type variable;
public:
    MyClass(Type a):variable(a) {   // 假设Type是一个定义了拷贝构造与赋值操作符的类
    }
};
输出结果为:
constructor called
copy constructor called
copy constructor called
destructor called
destructor called
destructor called

使用初始化列表后,编译器会遵循下面顺序:
1. 调用Type的拷贝构造函数来初始化‘a’ .
2. 调用Type的拷贝构造函数,在初始化列表中的使用参数’a’来对成员对象”variable”进行初始化.
3. 调用Type的析构函数,因为a的声明周期结束了.

由上面的对比例子可知,使用初始化列表少用了一个步骤。即如果是在构造函数体中赋值,则需要copy constructor+constructor+assignment operator+destructor, 如果用初始化列表,则只需要copy constructor+copy constructor+destructor, 少了一个调用赋值运算符的过程。

在真实的应用程序中,如果存在很多的成员数据,则多出的这一个拷贝过程,会消耗掉可观的性能。