构造函数初始化列表
与普通构造函数的区别
构造函数初始化列表用于在显式初始化类成员。在平常的使用构造函数创建对象时,如果对象中将另一个对象作为成员,那么在调用构造函数之前,编译器会用成员对象的默认构造函数进行初始化。如果不想让编译器用默认的构造函数进行初始化,就要使用构造函数初始化列表来显式规定成员对象的初始化形式。
构造函数进行的操作是,将成员先初始化,再用给定值赋值给初始化后的成员,而构造函数初始化列表则是直接对成员进行初始化,两者之间的区别跟下面两种创建变量的方式区别类似:
//在创建的时候直接初始化,相当于构造函数初始化列表
int x = 3;
//先创建x,再赋值给x,相当于普通构造函数的初始化方式
int x;
x = 3;
防止非内置类型二次创建
对于内置类型,两种方式的初始化方法都是一样的效果。但是对于成员是一个对象的情况,则会有很大的区别,它会导致对成员对象进行多次不必要的复制,进而降低运行效率。有些时候如果管理不当,甚至会导致内存泄漏,更严重的是内存混乱,从而程序崩溃。下面的例子展示了这样的情况:
class Car{
private:
char * brand;
double price;
public:
Car(){
cout << "Using default constructor for Car." << endl;
brand = new char[3];
strcpy(brand, "No");
price = 0;
};
Car(const char * b, double p = 0){
cout << "Using constructor for Car." << endl;
price = p;
brand = new char[strlen(b) + 1];
strcpy(brand, b);
}
Car(const Car & c){
cout << "Using copy constructor for Car." << endl;
brand = new char[strlen(c.brand) + 1];
strcpy(brand, c.brand);
price = c.price;
}
~Car(){
cout << "deleting car" << endl;
delete [] brand;
}
void show(){
cout << "The " << brand << " car worth of $" << price << endl;
}
Car operator=(const Car & c){
Car res(c);
return res;
}
};
class Person{
private:
char * name;
Car car;
public:
Person(){
cout << "Using defualt constructor for Person." << endl;
}
Person(const char * n, Car c){
cout << "Using constructor for Person." << endl;
name = new char[strlen(n) + 1];
strcpy(name, n);
car = c;
}
Car c("Ford", 23);
Person p("Jhon", c);
相比之下,Person
类的构造函数比较简单,而Car
类的构造函数则要复杂得多,这是为了对内存进行管理,以防止内存泄漏。要想知道Car
类的构造函数为什么要这么写,可以看我的另一篇文章,不过不看也没有关系,不影响对构造函数初始化列表的理解。
运行上面的程序,会得到下面的输出结果:
Using constructor for Car.
Using copy constructor for Car.
Using default constructor for Car.
Using constructor for Person.
Using copy constructor for Car.
deleting car
deleting car
deleting car
deleting car
显然这个程序为了创建一个Person
对象,创建并销毁了太多Car
对象,下面解释为什么会产生这些输出。
- 第一行没有什么特殊的,调用
Car
中定义的构造函数,创建对象。 - 第二行,开始进入
Person
类的构造函数Person(const char * n, Car c)
,这个函数中Car
是以值传递的,所以要复制一个副本,所以调用了Car
类的复制构造函数。 - 第三行,
重点来了
,这里调用的是Car
类的默认构造函数,并且这个时候还没有进入Person
类构造函数的函数体内执行代码,因为第一行代码是打印语句,而打印语句在第四行。说明编译器在进入Person
类构造函数执行代码之前,用Car
的默认构造函数创建了一个Car对象
。这就是之前说的,先用默认构造函数创建对象,再对这个对象赋值。 - 第四行,开始进入
Person
类的构造函数并执行代码。 - 第五行,使用
Car
的复制构造函数将外部传进来的Car
对象赋值给内部对象。
综上所述,为了初始化Person
类中的Car
成员,创建了三个Car
对象。
下面通过构造函数初始化列表的方式对其进行改进,将Person
类中的构造函数Person(const char * n, Car c)
替换成:
Person(const char * n, Car c):car(c){
cout << "Using constructor initiation list." << endl;
name = new char[strlen(n) + 1];
strcpy(name, n);
}
该程序输出如下所示:
Using constructor for Car.
Using copy constructor for Car.
Using copy constructor for Car.
Using constructor initiation list.
deleting car
deleting car
可以看到,使用了构造函数初始化列表之后,编译器没有再调用Car
的默认构造函数。而是直接用Car
的复制构造函数初始化car
成员。
初始化顺序
另一个需要注意的点是,构造函数初始化列表初始化成员的顺序是根据成员在类中的声明顺序来的,而不是成员在列表中出现的顺序。例如:
class Test{
private:
int x, y;
public:
Test(int a): y(a), x(y){}
void show(){
cout << "x = " << x << ", y = " << y << endl;
}
};
Test t(3);
t.show();
程序的输出结果为:
x = 0, y = 3
逻辑上来说,应该x
和y
都等于3,但是因为是先初始化x
,然后再初始化y
,所以初始化x
的时候y
的值还是0
,所以x
被初始化成了0
。
必须用构造函数初始化列表的类型
必须用构造函数初始化列表的类型有以下特点:
- 初始化之后不能修改
- 没有默认构造函数的对象
因为如果不适用构造函数初始化列表,编译器就会调用成员对象的默认构造函数先初始化对象,所以没有默认构造函数的对象必须使用构造函数初始化列表。
因为普通构造函数初始化成员的过程是先将其初始化为系统默认值,再将给定值复制给初始化之后的变量。所以那些初始化之后就不能改变的变量就只能使用构造函数初始化列表。例如引用变量
和const变量
。