C++入门-----初始化列表
学习目标
- 1. 初始化列表
- 1.1 是什么
- 1.2 为什么
- 1.3 怎么做
- 2. 注意的点
- 3. explicit关键字
1. 初始化列表
1.1 是什么
与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
}
1.2 为什么
如果该类增加一个常量
class Date
{
//省略
private:
int _year;
int _month;
int _day;
const int _t;
}
要如何对_t进行初始化?如果是调用构造函数,那么在进入构造函数的时候建立栈帧,_t也随之被定义,常量被定义后是无法进行修改的,因为常量的初始化只能有一次。
所以就引用了初始化列表,在其定义的时候便进行初始化:
class Date
{
public:
//初始化列表,成员变量定义的地方
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
,_t(10)
{}
private:
int _year;
int _month;
int _day;
const int _t;
};
int main()
{
Date d1(2000,1,1);
return 0;
}
也可以只处理_t
Date(int year, int month, int day)
:_t(10)
{}
C++中没有规定一定要使用初始化列表,这样初始化的方式和构造函数的区别就是:
int x = 1;//定义的时候初始化
int x;//先定义再初始化
x = 1;
1.3 怎么做
类中包含以下成员,必须放在初始化列表位置进行初始化:
1. 引用成员变量
2. const成员变量
3. 自定义类型成员(且该类没有默认构造函数时)
引用和const成员变量没有默认构造函数的自定义类型成员,都在定义的时候必须初始化。
建议是自定义类型成员都用初始化列表。当然也可以在构造函数和内置类型一起初始化。
- 举例
如下面的例子:
定义一个A类及其对象_a,分别写出其拷贝构造,默认构造和赋值操作符函数。让_a在Date的构造函数中一起初始化,然后观察打印的结果:
- 当没有写初始化列表时:
class A
{
public:
A(const A& _a)
{
cout << "A的拷贝构造" << endl;
}
A(int _a = 0)
{
cout << "A的默认构造" << endl;
}
A& operator=(const A& _a)
{
cout << "A的赋值操作符" << endl;
return *this;
}
};
class Date
{
public:
Date(int year, int month, int day, const A& a)
{
_a = a;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
A _a;
//const int _t;
};
int main()
{
Date d1(2000,1,1,10);
return 0;
}
可以看见,调用了两次A的默认构造和一次A的赋值操作符
- 当写了初始化列表时
Date(int year, int month, int day, const A& a)
:_year(year)
, _month(month)
, _day(day)
, _a(a)
{}
这时候是一次默认构造一次拷贝构造,没有赋值运算符。
- 原因解析
- 没有写初始化列表
- 写了初始化列表
写了初始化列表以后就只有初始化的默认构造和对象与对象之间的拷贝构造
减少了赋值、拷贝操作。
- 结论
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
2. 注意的点
- 注意
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
观察下列代码
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();
}
结果会输出1和随机值。
由于生命顺序是先_a2再_a1,所以其定义的顺序也应该是这样,所以先执行_a2(_a1)此时_a1还未初始化所以_a2保存的是随机值,然后再执行_a1(a),_a1变成1。
3. explicit关键字
- 作用
单参构造函数,没有使用explicit修饰,具有类型转换作用;用explicit修饰构造函数,将会禁止构造函数的隐式转换。
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
例如
class A
{
public:
A(const A& _a)
{
cout << "A的拷贝构造" << endl;
}
A(int _a = 0)
{
cout << "A的默认构造" << endl;
}
private:
int _a;
};
int main()
{
A a1(2000);
A a2 = 2000;
return 0;
}
注意到这句代码
A a2 = 2000;
2000是int类型,a2是A类,为什么可以相互转换?这里其实可以类比于这句代码
int* p = NULL;
int a = (int)p;
期间生成了一个临时变量,把(int)p的值给到该临时变量,然后再赋值给a。
上面的代码也同理,用一个整形变量给A类型对象赋值,实际编译器背后会用2000构造一个无名对象,最后用无名对象给d1对象进行赋值。
2000构造出来的临时无名对象A(2000)是一个构造过程,再用这个对象拷贝构造给a2。编译器会默认将连续构造的函数过程合二为一,也就是说上述两个构造过程最终合并成一个默认构造。
explicit修饰构造函数,禁止类型转换—explicit去掉之后,代码可以通过编译
explicit A(int _a = 0)
{
cout << "A的默认构造" << endl;
}