初始化列表
首先,初始化列表是我们的祖师爷本贾尼博士为了解决在某些成员变量在定义时必须初始化的情况。这个初始化列表其实发生在构造函数之前,也就是实例化整个对象时先对所有的成员都进行了初始化
初始化的概念区分
在之前的博客学习中,我们已经学习了【C++】的六大默认成员函数 ,想必大家已经对构造函数已经比较熟悉了,可是大家是否遇到过,在构造函数后面跟了一个冒号,这个问题让我很是困惑
在了解 初始化列表之前,我们首先回顾两个重要的知识:
1. 构造函数是干嘛的?
答: 用于初始化类中的成员变量
2. 什么是初始化?
答: 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
接下来再来看一段代码:
class Date
{
public://构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
上面这个Date类是我们之前写过的,这里有一个它的有参构造函数,虽然在这个构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化。构造函数体中的语句只能将其称为【赋初值】,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
class Date
{
public:Date(int year = 2022, int month = 5, int day = 24){_year = year;_year = 2023; //第二次赋值_year = 2024; //第三次赋值_month = month;_day = day;}
private:int _year;int _month;int _day;
};
既然构造函数体的语句只能称作为赋初值,现在,可否有一种方式进行初始化呢?即初始化列表初始化。
总结
- 我们之前写的构造函数其实并不是对成员变量进行初始化而是进行赋初值。
- 如果想要对成员变量进行初始化,需要用到初始化列表
初始化列表的概念理解
以一个冒号 “ :” 开始,接着是一个以 , 分隔的数据成员列表,每个"成员变量"后面跟一个放在 ()的初始值或表达式
例如如下代码:
class Date
{
public://构造函数: -->初始化列表初始化Date(int year = 2024, int month = 8, int day = 2):_year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
当然,我可以在初始化列表初始化,也可以在大括号内进行赋值:
Date(int year = 2024, int month = 8, int day = 2):_year(year), _month(month)
{_day = day;
}
初始化列表的注意事项
初始化列表可以认为就是对象成员变量定义的地方
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
类中包含以下成员,必须放在初始化列表位置进行初始化
- 引用成员变量
- const成员变量
- 自定义类型成员(该类没有默认构造函数)
先前我们都知道引用的变量和const变量只能在定义时初始化,而普通的变量在定义时不强求初始化,所以我们就不能按照如下的方式操作:
成员变量为const和引用的时候-----正确的代码为:
class Date
{
public://析构函数Date(int year = 12, int month = 10, int day = 1):_year(year), _month(month), _day(day){}void Printf(){cout << "year为:" << _year << endl;cout << "month为:" << _year << endl;cout << "day为:" << _year << endl;}
private://定义时不强求初始化,后面可以再赋值修改int _year; //声明//const修饰的变量 和 引用的变量 需要在定义的时候就进行初始化const int _month;int& _day;
};int main()
{Date d1;d1.Printf();return 0;
}
自定义类型成员(该类没有默认构造函数)同样也得在初始化列表进行初始化:
class A
{
public:A(int x) //不是默认构造函数,因为接受一个参数:_x(x){}private:int _x;
};class Date
{
public:Date(int a) //在初始化列表对自定义类型 _aa 进行初始化:_aa(a){}
private:A _aa;
};
注意这里的条件,一定要是没有默认构造函数的自定义类型成员才得在初始化列表进行初始化,而默认构造函数简单来说就是不需要传参的函数
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
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();
}
A、输出1 1 B、程序崩溃 C、编译不通过 D、1 随机值
答案:D
解析:注意成员变量在类中声明次序就是其在初始化列表中的初始化顺序,既然_a2先声明,则必然进入初始化列表要先执行, _a2(_a1) 。意思是说拿_a1去初始化_a2,不过此时的_a1还是随机值,自然_a2即为随机值,随后执行:_a1(a)。拿a初始化_a1,所以输出的值为1和随机值。
explicit关键字
在我们自己平时写 C++ 代码的时候,较少会用到 explicit关键字 。但是在C++相关的标准类库中,看到explicit关键字的频率还是很高的。既然出现的频率这么高,那么我们就来看看explicit关键字的作用到底是干什么的。
什么是explicit关键字
explicit是C++中的一个关键字,它用来修饰只有一个参数的类构造函数,以表明该构造函数是显式的,而非隐式的。当使用explicit修饰构造函数时,它将禁止类对象之间的隐式转换,以及禁止隐式调用拷贝构造函数。
既然解释中提到了 类的构造函数 那么下面我将从构造函数中详细的给大家,讲解explicit其中的含义。
构造函数还具有类型转换的作用
在理解 explicit 关键字 之前,我们必须要了解构造函数的类型转换作用,以便于我们更好的理解 explicit 关键字
单参构造函数与explicit关键字
还是来说说老朋友日期类,我们通过下面这个日期类进行讲解
class Date
{
public:
// 构造函数Date(int year):_year(year) // 初始化列表{}private:int _year;int _month = 3;int _day = 31;
};
对于下面的 d1 很清楚一定是调用了有参构造进行初始化,不过对于 d2 来说,也是一种构造方式
int main()
{// d1 和 d2 都会调用构造函数Date d1(2022); Date d2 = 2023;return 0;
}
我们依旧通过调试来看就会非常清晰,这种 【Date d2 = 2023】 写法也会去调用构造函数
此时,大家可能会产生疑问,这种构造方式从来没有见过,为什么 Date d2 = 2023 会调用 构造函数呢? 其实这都是因为有【隐式类型转换】的存在,下面我将从一个简单的例子来为大家讲解。
像下面将一个int类型的数值赋值给到一个double类型的数据,此时就会产生一个隐式类型转换
int i = 1;
double d = i;
对于类型转换而言,这里并不是将值直接赋值给到左边的对象,而是在中间呢会产生一个临时变量,例如右边的这个 i 会先去构造一个临时变量,这个临时变量的类型是 [double] 。把它里面的值初始化为 1,然后再通过这个临时对象进行拷贝构造给d,这就是编译器会做的一件事
那对于这个 d2 其实也是一样,2023会先去构造一个临时对象,这个临时对象的类型是[Date]
把它里面的year初始化为2023,然后再通过这个临时对象进行拷贝构造给到d2
不是说构造函数有初始化列表吗?拷贝构造怎么去初始化呢?
别忘了【拷贝构造】也是属于构造函数的一种哦,也是会有初始化列表的
//拷贝构造
Date(const Date& d):_year(d._year),_month(d._month),_day(d._day)
{}
刚才说到了中间会产生一个临时对象,而且会调用构造 + 拷贝构造,那此时我们在Date类中写一个拷贝构造函数,调试再去看看会不会去进行调用
- 很明显没有,我在进入Date类后一直在按F11,但是却进不到拷贝构造中,这是为什么呢?
原因其实在于编译器在这里地方做了一个优化,将【构造 + 拷贝构造】优化成了【一个构造】,因为编译器在这里觉得构造再加拷贝构造太费事了,干脆就合二为一了。其实对于这里的优化不同编译器是有区别的,像一下VC++、DevC++可能就不会去优化,越是新的编译器越可能去进行这种优化。
但是怎么知道中间赋值这一块产生了临时对象呢?如果不清楚编译器的优化机制这一块肯定就会认为这里只有一个构造
这点确实是,若是我现在不是直接赋值了,而是去做一个引用,此时会发生什么呢?
Date& d3 = 2024;
可以看到,报出了一个错误,原因就在于d3是一个Date类型,2024则是一个内置类型的数据
一个常量让d3共用会造成权限放大!!
- 但若是我在前面加一个
const
做修饰后,就不会出现问题了,这是为什么呢? - 其实这里的真正原因就在于产生的这个【临时变量】(临时变量具有常性),它就是通过Date类的构造函数构造出来的,同类型之间可以做引用。还有一点就是临时变量具有常性,所以给到一个
const
类型修饰对象不会有问题
从这里我们就可以看到在中间赋值的时候是产生了临时变量。
但若是你不想让这种隐式类型转换发生怎么办呢?此时就可以使用到C++中的一个关键字叫做explicit
- 它加在构造函数的前面进行修饰,有了它就不会发生上面的这一系列事儿了,它会【禁止类型转换】
explicit Date(int year):_year(year)
{}
多参构造函数与explicit关键字
//多参构造函数
Date(int year, int month ,int day = 31):_year(year),_month(month),_day(day)
{}
根据从右往左缺省的规则,我们在初始化构造的时候要给到2个参数,d1
没有问题传入了两个参数,但是若是像上面那样沿袭单参构造函数这么去初始化还行得通吗?很明显不行,编译器报出了错误
这个时候就要使用到我们C++11中的新特性了,在对多参构造进行初始化的时候在外面加上一个{}
就可以了,可能你觉得这种写法像是C语言里面结构体的初始化,但实际不是,而是在调用多参构造函数
Date d2 = { 2023, 3 };
- 不仅如此,对于下面这种也同样适用,调用构造去产生一个临时对象
const Date& d3 = { 2024, 4 };
那要如何去防止这样的隐式类型转换的发生呢,还是可以使用到explicit
关键字吗?
//多参构造函数
explicit Date(int year, int month ,int day = 31):_year(year),_month(month),_day(day)
{}
还有一种例外,当缺省参数从右往左给到两个的时候,此时只需要传入一个实参即可,那也就相当于是单参构造explicit
关键字依旧可以起到作用
explicit Date(int year, int month = 3,int day = 31):_year(year),_month(month),_day(day)
{}