目录
创建一个类对象时,编译器通过调用构造函数,给类对象中各个成员变量赋初值:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
但上述赋初值不能称作类对象成员的初始化,因为构造函数体内可以多次赋值:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
{
_year = year;
_month = month;
_day = day;
_year = 2023;//构造函数体内允许对成员变量进行多次赋值
}
private:
int _year;
int _month;
int _day;
};
而初始化列表能只能初始化一次。
一、用初始化列表初始化对象
1.初始化列表用法
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括 号中的初始值或表达式。
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
:_year(year) //初始化列表初始化
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
2.初始化列表特性
(1)初始化列表能只能初始化一次,多次初始化会报错:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
:_year(year)
,_month(month)
,_day(day)
,_month(month) //初始化列表多次初始化
{}
private:
int _year;
int _month;
int _day;
};
编译器也允许构造函数赋初值和初始化列表初始化混用:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
:_year(year) //两者混用
,_month(month)
{
_day = day;
}
private:
int _year;
int _month;
int _day;
};
混用时初始化列表初始化和构造函数赋初值不冲突:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year) //两者不冲突
, _month(month)
{
_day = day;
_year = 2023;
}
private:
int _year;
int _month;
int _day;
};
但混用时初始化列表初始化还是要遵循只能初始化一次成员变量的原则:
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year) //初始化列表初始化
, _month(month)
, _year(2023) //_year在初始化列表里被初始化了两次,不允许
{
_day = day;
}
private:
int _year;
int _month;
int _day;
};
(2)const成员变量、引用成员变量、没有默认构造函数的自定义类型成员只能在初始化列表初始化。
①const成员变量必须在定义的时候初始化
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year)
, _month(month)
, _n(2) //const成员变量必须使用初始化列表进行初始化
{
_day = day;
//_n = 2; //const成员变量不能在函数体内初始化
}
private:
int _year;
int _month;
int _day;
const int _n = 1;
};
②引用成员变量必须在定义的时候初始化
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year)
, _month(month)
,_ref(year)//引用成员变量要在初始化列表初始化
{
_day = day;
//_ref = year; //引用成员变量不能在函数体内初始化
}
private:
int _year;
int _month;
int _day;
int& _ref;
};
③没有默认构造函数的自定义类型成员变量
#include <iostream>
using namespace std;
class A
{
public:
//默认构造函数是不用传参就可以调用的构造函数,有3种:
//1.无参默认构造函数
//2.带参全缺省的默认构造函数
//3.我们不写,编译器自动生成的默认构造函数
A(int x)//不属于以上任何一种,所以A类的对象没有默认构造函数
{
cout << "A(int x)" << endl;
_x = x;
}
private:
int _x;
};
class Date
{
public:
//构造函数
Date(int year = 2022, int month = 4, int day = 19)
: _year(year)
, _month(month)
, _a(20)//没有默认构造函数的自定义类型成员变量必须在初始化列表进行初始化
{
_day = day;
}
private:
int _year;
int _month;
int _day;
A _a;
};
const成员变量、引用成员变量、没有默认构造函数的自定义类型成员变量必须在初始化列表内初始化的原因:
①初始化列表是对象的成员变量定义的地方。
②对象的内置类型成员变量在初始化列表定义时没有要求必须初始化,因此既可以在初始化列表进行初始化,也可以在构造函数体内初始化。
③而const成员变量、引用成员变量、没有默认构造函数的自定义类型成员变量不能先定义再初始化,它们在初始化列表内定义,并且必须在定义时就初始化,因此必须在初始化列表内初始化。
(3) 尽量使用初始化列表初始化,因为不管是否使用初始化列表,虽然对于内置类型没有差别,但是对于自定义类型成员变量,一定会先使用初始化列表初始化。
为什么会先使用初始化列表初始化?
如下,Date类没有默认构造函数,因为26行的构造函数不属于默认构造函数中的任意一种,在对Date类的对象d进行初始化时,会调用Date类的默认构造函数,所以对象d的day实参12和hour实参12都没有被传进去,_t作为Date类的自定义类型成员变量会调用Time类的默认构造函数,_hour默认传参为0,因此打印_hour的值也为0,d的参数没有传成功:
#include<iostream>
using namespace std;
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0)
: _hour(hour)
{
cout << _hour << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int day, int hour)
{}
private:
int _day;
Time _t;
};
int main()
{
Date d(12, 12);
return 0;
}
假如Date类的构造函数不使用初始化列表进行初始化,使用函数体内初始化时,要把Date类的构造函数的形参hour的值给d,那么就必须构造一个Time类对象t,对该对象传参传hour,再使用赋值运算符重载函数将对象t拷贝给_t:
#include<iostream>
using namespace std;
class Date; // 前置声明
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0)
: _hour(hour)
{
cout << _hour << endl;
}
private:
int _hour;
};
class Date
{
public:
//自定义类型,不使用初始化列表,就需要使用构造函数 + operator=
Date(int day, int hour)
{
//函数体内初始化
Time t(hour);//调用Time类的构造函数
_t = t;
_day = day;
}
private:
int _day;
Time _t;
};
int main()
{
Date d(12, 12);
cout << 4 << endl;
return 0;
}
这还不如直接使用使用初始化列表初始化呢,还不需要赋值运算符重载函数:
class Date
{
public:
//自定义类型,使用初始化列表,只需要构造函数
Date(int day, int hour)
:_t(hour)
{
_day = day;
}
private:
int _day;
Time _t;
};
因此,建议尽量直接使用初始化列表进行初始化。
(4)成员变量初始化的顺序就是成员变量在类中的声明次序,与初始化列表中的先后次序无关。
如下代码,类成员变量中先声明了_a2,再声明了_a1,因此初始化的顺序是先初始化_a2,再初始化_a1:
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
: _a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;//先声明_a2
int _a1;//后声明_a1
};
int main() {
A aa(1);
aa.Print();
}
先声明_a2就会先初始化_a2,用_a1初始化_a2,由于此时_a1还是随机值,因此_a2的值也是随机值,_a1使用a的值1进行初始化,因此,_a1的值为1:
所以,建议类中的成员变量声明的顺序和初始化列表中初始化的顺序一致。
二、explicit关键字
1.内置类型的隐式转换
int i = 0;
double d = i;//隐式类型转换
根据监视可以看出:
double d = i;并不是将i直接赋值给d,而是用i创建一个临时变量,再把临时变量的值给d,那么d改变的是临时变量的值,而不是i的值,因为程序执行完毕后,i的值并未发生改变。
如果d作为引用,那么必须加上const关键字进行修饰,因为d不是i的引用,是临时变量的引用,而临时变量具有常性,不允许引用权限放大。
int i = 0;
const double& d = i;//d引用了临时变量,临时变量具有常性,所以d也必须具有常性
2.如何避免单参构造函数初始化发生隐式类型转换
正常的类对象初始化如下面的aa1,也可以使用拷贝构造初始化,如aa2。由于c++支持隐式类型转换,因此也支持单参数构造函数初始化,如aa3:
#include<iostream>
using namespace std;
class A
{
public :
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A aa1(1);//构造aa1对象
A aa2(aa1);//拷贝构造,程序没写拷贝构造,编译器会自动生成拷贝构造函数,对内置类型完成浅拷贝
A aa3 = 3;//单参数的构造函数,会发生隐式类型转换
return 0;
}
那么
A aa3 = 3;
是如何支持类型转换的呢?
对于自定义类型A,aa3是A类型,3是整形。编译器会先拿A构造一个临时对象temp,3作为参数传给这个临时对象temp,再拿aa3(temp)去拷贝构造,发生隐式类型转换,即先构造,再拷贝构造:
//A aa3 = 3;
A temp(3); //先构造
A aa3(temp); //再拷贝构造
不过现在的编译器已经优化过了,会直接调用构造函数A aa(3)。
如果不想让单参数的构造函数发生隐式类型转换,可以使用explicit关键字修饰构造函数,表明该构造函数是显式的,而不是隐式的,就会避免发生不期望的类型转换,使用场景如下:
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
int main()
{
A aa1(1);//构造aa1对象
A aa2(aa1);//拷贝构造,程序没写拷贝构造,编译器会自动生成拷贝构造函数,对内置类型完成浅拷贝
A aa3 = 'x';//先拿A构造一个临时对象temp,字符x作为参数传给这个临时对象temp,会发生隐式类型转换,再拿aa3(temp)去拷贝构造
return 0;
}
aa3作为A类的对象,构造时传参应该传int型,但却传了char型,由于发生隐式类型转换,因此编译也没毛病,但是它传参就是不伦不类。这时候可以给A的构造函数加上explicit声明不让该单参构造函数发生隐式类型转换,编译就会报错:
class A
{
public:
explicit A(int a)
:_a(a)
{}
private:
int _a;
};
这时候只能乖乖给aa3传int型参数了。
三、匿名对象
1.匿名对象定义
没有名字的对象叫做匿名对象,A(3)跟aa1和aa2相比少了个对象名,没有名字,aa1和aa2的生命周期在main函数内,A(3)的生命周期只在当前行:
#include<iostream>
using namespace std;
class A
{
public:
explicit A(int a)
:_a(a)
{
cout << "A(int a):"<< a << endl;
}
A(const A& aa)
{
cout << "A(const A&)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);//生命周期在main函数内
A aa2(aa1);//生命周期在main函数内
A(3);//构造匿名对象,生命周期只在这一行
return 0;
}
F10调试:当执行完A(3)还没执行return 0时,aa1和aa2的生命周期还没有结束,不会调用析构函数,此时打印的析构函数只能是匿名对象A(3)的析构函数:
所以A(3)这一行执行完就调析构函数了。
2.匿名对象应用场景
假设有一个函数f,且A类的构造函数全缺省:
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)//构造函数全缺省
:_a(a)
{
cout << "A(int a):"<< a << endl;
}
A(const A& aa)
{
cout << "A(const A&)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void f()//f函数
{
cout << "f()" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);//生命周期在main函数内
A aa2(aa1);//生命周期在main函数内
A(3);//构造匿名对象,生命周期只在这一行
return 0;
}
调用f()函数时,需要定义一个A类对象,才能调用A类函数f,这就需要写两行:
int main()
{
A aa1(1);//生命周期在main函数内
A aa2(aa1);//生命周期在main函数内
A(3);//构造匿名对象,生命周期只在这一行
A aa4;//需要定义一个A类对象,才能调用f
aa4.f();
return 0;
}
对象aa4 在main函数结束后才会销毁。如果定义对象只是为了调用函数,那么可以考虑直接定义一个匿名对象:
int main()
{
A aa1(1);//生命周期在main函数内
A aa2(aa1);//生命周期在main函数内
A(3);//构造匿名对象,生命周期只在这一行
A aa4;//需要定义一个A类对象,才能调用f
aa4.f();
A().f();//定义匿名对象来调用函数f()
return 0;
}
这个匿名对象就是为了调用函数f,这个匿名对象后边也没人用它,在当前行调用完f()函数就销毁了。