前面我们了解了构造函数的用法和使用场景,知道构造函数是用来对类中成员进行初始化的,但真正进行初始化作用的是属于构造函数的另一部分:初始化列表
为什么称在函数体内进行的赋值不是初始化呢?首先我们应知道初始化只可以进行一次,但函数体内赋值却可以进行多次,所以我们只能叫函数体内赋值为赋初值。
那么初始化列表的组成形式是什么样的:
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
对于初始化列表有以下特点:
每个成员变量在初始化列表中最多只能出现一次
这对应着(初始化只能初始化一次)
这里虽然写着最多只能出现一次,但并不是说不出现编译器就不会对变量进行初始化
例如,我们在声明变量中加一个int _size,我们不对其进行初始化,编译器就什么也不做了吗,编译器会对它进行初始化,只是会给一个随机值,当我们给_size一个缺省值时,那么初始化列表就会直接调用这个缺省值给_size,解释这样也是想说明一个问题:缺省值其实就是给初始化列表调用的。
那既然有了构造函数体内赋值那么初始化列表出现的目的仅是为了普通的定义吗?
首先,有三种情况必须要使用初始化列表:
1、引用成员变量
2、const成员变量
3、自定义成员变量(该类没有默认构造函数)
为什么这三种成员变量必须要使用初始化列表呢?
我们先看一下引用成员变量和const成员变量的共同点:二者都必须在定义时进行初始化。
我们上面说了构造函数体内赋值不是初始化,也就是说不算是定义,其他类型因为没有这样的特性,在函数体内赋值还算是 ‘初始化’,但这种类型的成员哪怕想要体内赋值都没有办法,而根据初始化列表的特性,我们知道初始化列表中就是初始化,就是用来定义的。
了解了前两种的成员必须使用初始化列表,那么第三种成员使用初始化列表的原因呢?
假如我们在B类中声明了一个A类的自定义成员_aobj,编译器在执行B类时肯定会先去调用A类的构造函数给它初始化,但A类中的没有默认构造函数,声明又无法传值,那么此时_aobj就无法进行初始化,所以这时就可以在B中写一个初始化列表,将_aobj进行初始化。
下图为三个特殊成员的初始化过程:
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
//这里是将局部变量ref初始化给引用_ref,
//若不加&,那么ref销毁后,_ref引用的就是空地址
B(int a, int& ref)
:_aobj(a)
, _ref(ref)
, _n(10)
{}
private:
A _aobj;// 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
初始化列表的另一好处是可以提高效率:
class A
{
public:
A()
{
cout << "default constructor" << endl;
_data = 0;
}
A(int i)
{
cout << "construtor" << endl;
}
A(const A& a)
{
cout << "copy constructor form "<< endl;
_data = a._data;
}
A& operator=(const A& a)
{
cout << "operator"<< endl;
_data = a._data;
return *this;
}
~A()
{
cout << "deconstructor"<< endl;
}
private:
int _data;
};
class B
{
public:
B(int data)
{
a = data;
}
private:
A a;
};
int main()
{
B b(100);
return 0;
}
class B
{
public:
B(int data)
:a(data)
{}
private:
A a;
};
int main()
{
B b(100);
return 0;
}
上面第一种是未使用初始化列表的情形,第二种是使用初始化列表的情形,我们根据结果可以很好地看出使用初始化列表后大大减少了调用函数的次数。那么这样效率的增加是如何来的呢?
这里最重要的一点是用到了隐式类型转换:
我们可以看到在未使用初始化列表时出现了将int型的data赋给了自定义类型a的情况,这里正是用到了隐式类型转换使得效率降低
A(int i)函数除了可以用于显式创建 A 对象外,还可以用于隐式类型转换
首先,编译器会先执行对B类的实例化 -> 进入B类中 -> 对B类中的成员变量a初始化 ->
进入A类中调用A的默认构造函数(A())-> 进入到B(int data)函数体内执行a=data ->
因为这里的类型不匹配问题,所以在这里创建了一个临时A类的对象,编译器会使用 data 作为参数调用 A 的带参构造函数(A(int i)),创建一个临时的 A 对象。这个临时对象是匿名的,它的生命周期只在当前表达式中有效。假设 data 的值为 100,那么编译器会执行类似下面的操作来创建临时对象:A tmp(data),tmp是临时变量 ->执行赋值运算符重载函数
这也是为什么编译器会调用两次构造函数的原因,而这个临时变量销毁时会调用对应的析构函数,而b对象销毁时要销毁A类对象a又会调用析构函数,所以出现了调用两次析构的情形。
而初始化列表却可以避免使用隐式类型转换
为什么?
因为在对一个自定义类型初始化时,会先看是否进行了显示初始化,如果有那么会直接进入显示初始化中,这时编译器会根据提供的参数来选择合适的构造函数。
在这个例子中,提供了一个 int 类型的参数 data,编译器会查找 A 类中接受 int 类型参数的构造函数。所以不需要编译器多次调用构造函数就可以初始化对象步骤:编译器会先执行对B类的实例化 -> 进入B类中 -> 对B类中的成员变量a初始化发现已经对其进行了显示初始化所以不会调用默认构造函数 -> 执行a(data) -> 编译器识别参数类型为int,然后去调用对应参数的构造函数A(int i)
因为初始化列表避免了隐式类型转换,自然也不会有临时变量的出现,那么析构函数调用一次就可以完成对应的销毁操作。
那既然初始化列表这么好用,那函数体内还有什么用处吗?
例如下面的代码,我们要判断是否创建成功的语句就无法放到初始化列表中,只可以放到函数体内,还有不少例子是需要在函数体内进行的,所以初始化列表无法替代所有场景。
class Stack
{
public:
Stack(int capacity)
:_a((int*)malloc(sizeof(int)* capacity))
, _top(0)
, _capacity(capacity)
{
if (_a == nullptr)
{
perror("创建失败\n");
exit(-1);
}
}
private:
int _top;
int _capacity;
int* _a;
};
下面有一串代码,大家可以分析一下运行结果会是什么:
A.输出1,1
B.程序崩溃
C.编译不通过
D.输出1,随机值
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
}
答案是D
语法明明没有问题,为什么一个会输出随机值?
原因:因为这里涉及到了另一个初始化列表的特性:
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
先声明_a2,所以先初始化_a2,此时_a1还没有进行初始化,自然给_a2的就是随机值,接着对_a1进行初始化将a赋给它,_a1就变成了1,所以这也就解释了为什么会出现随机值。
所以尽量让声明顺序与初始化顺序一样。