3-6:类与对象下篇——构造函数中的初始化列表匿名对象和explicit关键字
Posted 快乐江湖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3-6:类与对象下篇——构造函数中的初始化列表匿名对象和explicit关键字相关的知识,希望对你有一定的参考价值。
(1)真的是初始化吗
上一篇在说构造函数的时候,我们把构造函数的作用称之为在对象被实例化的同时初始化,也就是下面这样
class Date
{
public:
Date(int year=1998,int month=12,int day=20)
{
_year=year;
_month=month;
_day=day;
}
private:
int _year;
int _month;
int _day;
}
int main()
{
Date d1(2000,2,2);
}
上述这样操作的确“初始化”了,因为我们看到它确实给d1对象了一个值。
但是明确点说,这样的初始化顶多称之为“伪初始化”
因为 初始化只能初始化一次,但是构造函数内可以多次赋值,所以这样的方式称为构造函数体赋值
一个典型的例子是const
对象,如下,如果把一个int修饰为const,那么这个const int必须在定义的时候就初始化,任何后续的赋值将不被允许
int main()
{
//定义的时候就必须初始化
const int a=10;
//这种方式错误
const int a;
a=10;
}
返回到类,如果在之前的成员中加上一个const
成员,如果报错说明这种方式就不是初始化
class Date
{
public:
Date(int year=1998,int month=12,int day=20,int n=100)
{
_year=year;
_month=month;
_day=day;
_n=n;
}
private:
int _year;
int _month;
int _day;
const int _n;
}
int main()
{
Date d1(2000,2,2,200);
}
答案显而易见。
所以对于这类成员,在对象生成的时候同时就要进行初始化,所以引入真正的初始化的方式——初始化列表
(2)初始化列表
A:概述
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员列表由“成员变量”后面跟上一个放在括号中的“初值或表达式构成”
(初始化列表就是不在括号内进行初始化)
就拿上面报错的那个例子,使用初始化列表应该是下面这样的
class Date
{
public:
Date(int year, int month, int day,int n)//这样写
:_year(year)
,_month(month)
, _day(day)
,_n(n)
{}
void print()
{
std::cout << _year << "-" << _month << "-" << _day << "-" << _n << std::endl;
}
private:
int _year;
int _month;
int _day;
const int _n;
};
int main()
{
Date d1(2000, 2, 2,200);
d1.print();
}
- 每个成员变量在初始化列表只能出现一次(因为初始化本质就是只能初始化一次)
所以这样的初始化才叫真正的初始化,或许我写成下面这样,大家体会才能更深
B:哪些成员必须在初始化列表进行初始化
既然本段标题发出这样的疑问了,那么就表明有些必须在初始化列表(括号外)初始化,有些可以不在初始化列表初始化。很明显上面的const的成员必须在初始化列表初始化,那么这个例子中其他变量可以放在括号内,也就是可以写成这样
class Date
{
public:
Date(int year = 1998, int month = 12, int day = 20, int n = 20)
:_n(n)//const类必须放在初始化列表
{
_year = year;
_month = month;
_day = day;
}
void print()
{
std::cout << _year << "-" << _month << "-" << _day << "-" << _n << std::endl;
}
private:
int _year;
int _month;
int _day;
const int _n;
};
int main()
{
Date d1(2000, 2, 2);
d1.print();
}
除了const外,引用成员初始化必须放在初始化列表
下面的例子中引用了year
class Date
{
public:
Date(int year = 1998, int month = 12, int day = 20)
:_ref(year)//引用year
{
_year = year;
_month = month;
_day = day;
}
void print()
{
std::cout << _year << "-" << _month << "-" << _day << "-" << _ref << std::endl;
}
private:
int _year;
int _month;
int _day;
int& _ref;//引用
};
int main()
{
Date d1;
d1.print();
}
- 之所以输出随机值是因为引用的是形参,在调用下次print时已经销毁了。
还有就是自定义类型也必须放在初始化列表
下面增加一个类,这个类的构造函数没有设置默认值,在Date类中增加它的自定义类型,然后不去赋值
class Time
{
public:
Time(int hour)
:_hour(hour)
{}
private:
int _hour;
};
结果报错,显示没有合适的默认构造函数
- 这一点需要说明,如果用户没有显示定义构造函数,那么C++会默认生成构造函数,这样的话这个程序是可以编译通过的,只不过是随机值
(当然如果我给Time的hour初始值,这个构造函数就正确了,也就可以编译通过了,这里就不演示了)
所以这种情况下,就要我们手动传参,必须放在初始化列表,写法如下
(3)小结
之前我们的构造函数是放在函数体内的,但是这样的初始化本质上是赋值,像const这类成员,不可能存在const int a
a=10
这样的操作,而必须是const int a=10
,也就是定义的时候初始化,所以为了解决这样的问题,**在C++中我们要使用初始化列表对这些变量进行初始化。**对其他变量没有要求
但是在日常开发中,还是推荐不管三七二十一任何变量都是用初始化列表,尤其是在对自定义类型进行初始化时,如果使用赋值的方式将会比使用初始化列表麻烦很多
最后:一定要保证,成员变量在勒种的声明和次序和初始化列表中的初始化顺序一致 。否则就会出现下面的这样的错误
这段代码按照正常的逻辑角度上讲,应该输出的是1, 1
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void print()
{
std::cout<<_a1<<" "<<_a2<<std:endl;
}
private:
int _a2;
int _a1;
}
int main()
{
A a(1);
a.print();
}
但是其结果却为
出现随机值的原因已在上方用红色字体标注。这里声明次序先是_a2,再是_a1,所以初始化时也是这样的顺序,导致_a2在初始化时获得了随机值
(4)匿名对象
一般实例化对象时都是这样进行的
class A
{
public:
A(int a)
:_a(a);
{}
private:
int _a;
}
int main()
{
A aa(1);
}
这样实例化,那么这个对象的生命周期就是main函数内
而如果这样实例化的对象,它的生命周期仅仅在一行,从下一行开始它的生命周期也就结束了,也就去调用它的析构函数了
class A
{
public:
A(int a)
:_a(a);
{}
private:
int _a;
}
int main()
{
A aa(1);//声明周期是main函数
A(1);//匿名对象,生命周期仅仅在一行
}
(5)explicit关键字
如果初始化时只有一个参数,将会发生隐式类型转换
下面的案例中,直接使用了一个整形给一个对象进行赋值,是可以编译通过的
class Date
{
public:
Date(int year)
:_year(year)
{}
private:
int _year;
};
int main()
{
Date d1(2020);
d1 = 2019;//直接用一个整形给对象赋值
}
但是我们在赋值运算符重载那一节,讲过将“=”
重载后,采用赋值号可以进行类似的赋值操作,但是在这里并没有对“=”
进行重载,它却赋值成功了
实际上,上述例子中,编译器会偷偷的用2019创造出一个匿名对象,最后用匿名对象给d1进行赋值,这从表面看,就好像发挥了隐式转换的作用
如果使用explicit关键字修饰构造函数,将会禁止这种单参数的构造函数的隐式转换
以上是关于3-6:类与对象下篇——构造函数中的初始化列表匿名对象和explicit关键字的主要内容,如果未能解决你的问题,请参考以下文章