成员初始化列表(Member Initialization List)
对类的成员进行初始化的时候,有两种方式,第一种是使用初始化列表;第二种是在构造函数体内初始化。
class Node
{
public:
Node(int x) : m_x(x) // 初始化列表
{
Node* m_next = nullptr; // 构造函数体内初始化
}
int m_x;
Node* m_next;
};
一般情况下,两种方式都可以(效率并不相同)。但在一些情况下只能只用初始化列表进行初始化。这些例外就是:
- 引用成员。
- const成员。
- 基类或者成员的有参初始化。
对于第三种情况,虽然也可以在构造函数体内进行初始化,但是效率可能并不高。例如对于类Animal来说。在构造函数内初始化m_name完全没问题。但是这样的初始化效率并不高,因为会有临时变量的产生。如构造函数内初始化的伪代码所示,构造函数会先调用成员m_name的默认构造函数,然后再生成一个临时对象,用该临时对象给m_name进行赋值,然后再销毁这个临时变量。所以,这就是为什么构造函数体内初始化开销较大的原因。如果采用初始化列表进行初始化呢?
// 构造函数内初始化
class Animal
{
public:
Animal() {m_name(""); m_age(0);}
string m_name;
int m_age;
};
// 构造函数内初始化的伪代码
Animal::Animal()
{
m_name.string::string();
string _temp_name = string("");
m_name = _temp_name;
_temp_name.string::~string();
m_age = 0;
}
// 初始化列表初始化
class Animal
{
public:
Animal() : m_name("") { m_age(0);}
string m_name;
int m_age;
};
// 初始化列表展开
Animal::Animal()
{
m_name.string::string("");
m_age = 0;
}
可以看到,采用初始化列表的方式,可以直接调用成员m_name的有参构造函数,完成初始化。相对于构造函数内初始化,其效率更高。虽然采用列表初始化效率更高,但完全没必要所有成员都放到初始化列表里面,对于基本类型来说,完全可以放到构造函数体内进行初始化。
初始化列表会如何被编译器利用呢?编译器会将初始化列表里的初始化,插入到构造函数体的前面,即放在所有用户代码之前。插入的顺序是按照类成员的初始化顺序进行的,所以对于成员变量来说,其初始化顺序即是其声明的顺序(当用一个成员变量去初始化另一个成员变量的时候,这一点尤为重要)。 对于以下代码,采用初始化列表进行初始化的时候,编译器会先初始化m_ageInMonth,然后再初始化m_ageInYear。初始化m_ageInMonth的时候,m_ageInYear是个垃圾值,所以m_ageInMonth也是个垃圾值。正确的做法是将其放到构造函数体内(这样应该是赋值操作,而不是初始化)。
class Dog
{
public:
// 错误的构造
Dog(int ageInYear) : m_ageInYear(ageInYear), m_ageInMonth(m_ageInYear * 12)
{
}
// 正确的构造
Dog(int ageInYear)
{
m_ageInYear(ageInYear), m_ageInMonth(m_ageInYear * 12)
}
int m_ageInMonth;
int m_ageInYear;
};
当然,在初始化列表中你也可以用一个函数的返回值去初始化一个成员变量,但是这个时候要处于声明顺序与初始化顺序的一致。作者建议,如果要用一个成员变量去初始化另一个成员变量,这个时候最好将初始化语句写到构造函数体内。比如下面这个类的声明。
class Animal
{
public:
Animal() {}
Animal(int ageInMonth) : m_ageInMonth(age) {}
int m_ageInMonth;
};
class Dog : public Animal
{
Dog(int ageInYear) : m_ageInYear(ageInYear),
Animal(transYearToMonth(m_ageInYear)) {}
int transYearToMonth(int ageInYear) {return ageInYear;}
int m_ageInYear;
};
// ------------------------------------------------------------
class Animal
{
public:
Animal() {}
Animal(int ageInMonth) : m_ageInMonth(ageInMonth) {}
int m_ageInMonth;
};
class Dog : public Animal
{
public:
Dog(int ageInYear)
{
m_ageInYear = ageInYear;
this->Animal::m_ageInMonth = transYearToMonth(m_ageInYear);
}
int transYearToMonth(int ageInYear) {return ageInYear * 12;}
int m_ageInYear;
};
这样声明肯定是不对的,因为(经过编译器处理后)基类的成员变量要先于派生类成员的初始化。所以,为了避免这种情况,最好将初始化列表显示的写到构造函数体内,但也不是完全将初始化列表复制过去,而是先采用默认初始化来初始化基类,然后再给成员进行赋值。
总结
本小节的要点就是,引用和const成员的初始化必需使用初始化列表;初始化列表可以避免默认构造函数的调用及临时变量的生成和销毁,效率更高;成员的初始化顺序与声明顺序是一样的。
关于初始化顺序与声明顺序的补充
对于以下代码,当采用构造函数体进行初始化的时候。由于f的声明顺序在t之前,所以初始化的 时候,先调用Food的默认构造函数初始化f,然后再调用Toy的默认构造函数;之后调用Toy的有参构造函数生成一个临时对象,然后调用Toy的赋值运算符用这个临时变量给成员t赋值;再之后调用Food的有参构造函数生成一个临时对象,然后调用Food的拷贝运算符用这个临时变量给成员f赋值;最后销毁两个临时变量。当采用初始化列表去构造Dog对象的时候,将先调用成员f的有参构造函数,然后调用成员t的有参构造函数。
#include <iostream>
using namespace std;
class Food
{
public:
Food() { cout << "construct the food" << endl;}
Food(int price) { cout << "construct the food with price" << endl;}
void operator=(const Food& f) { cout << "construct the food with operator =" << endl;}
};
class Toy
{
public:
Toy() { cout << "construct the toy" << endl;}
Toy(int num) { cout << "construct the toy with num" << endl;}
void operator=(const Toy& f) { cout << "construct the toy with operator =" << endl;}
};
class Dog
{
public:
Dog(int price, int num)
{
t = Toy(1);
f = Food(1);
}
Food f;
Toy t;
};
int main()
{
Dog d(1, 1);
return 1;
}
/*
以上代码的输出:
构造函数体内初始化时:
construct the food
construct the toy
construct the toy with num
construct the toy with operator =
construct the food with price
construct the food with operator =
初始化列表初始化时:
construct the food with price
construct the toy with num
*/