一、再探构造函数
在之前的文章中我们大致将构造函数讲完了,但是还有一个比较重要的知识点当时没有讲到,因为如果把这部分内容加上去,会让前面的内容难度变得非常高,所以这部分内容放到这里来讲,之前讲构造函数的文章是:【C++】揭秘类与对象的内在机制(核心卷之构造函数与析构函数的奥秘)
接下来进入今天的正题,我们今天要补充的内容就是构造函数中的“初始化列表”,它可以帮我们对成员变量进行初始化,我们还是先来看看它的语法格式(刚开始学可能看起来怪怪的,熟悉了就好了),如下:
class Date
{
public:
//不使用初始化列表
Date(int year = 2025, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//使用初始化列表
Date(int year = 2025, int month = 1, int day = 1)
:_year(year)//在花括号(函数体)前以冒号为开始
,_month(month)//从第二行开始用英文逗号分隔
,_day(day)
{
}//在花括号中(函数体中)可以正常执行其它代码,这里就不演示了
private:
int _year;
int _month;
int _day;
};
在上面的代码中,有两个构造函数,一个不使用初始化列表和一个使用了初始化列表,在这里它们的作用都是一样的,就是根据用户传来的年月日对当前对象进行初始化
那么既然上面的代码中,无论是否使用了初始化列表,两个构造函数的作用都相同,而且这个初始化列表还这么丑,它到底有什么不同之处呢?接下来我们就正式介绍初始化列表
初始化列表非常重要,有3类成员变量必须使用初始化列表进行初始化,如果不使用初始化列表就会报错,它们分别是cosnt成员变量、引用成员变量以及自定义类型成员变量,那么为什么这3类成员变量就必须使用初始化列表进行初始化呢?
其中cosnt成员变量以及引用成员变量比较特殊,它们都只能初始化一次,如果我们将其写在构造函数内部进行初始化会有一定的歧义,比如const成员变量只能初始化一次,往后这个成员变量的值就不能修改了,如果它在函数体出现两次,该选择哪个值作为它的结果
而引用对象也是,只能初始化一次,往后只能修改引用对象的值,而不能修改引用对象的指向,如果在构造函数函数体中出现了两次,最终该选择哪个变量作为它的引用对象呢?我们来看如下伪代码:
class A
{
public:
//构造
A(int& a, int& b)
{
//如果写在函数体内,编译器最终应该选择a还是选择4这个值
_a = a;
_a = 4;
//_b又应该是谁的引用?会产生歧义
_b = b;
_b = a;
}
private:
const int _a;
int& _b;
};
所以针对这两类比较特殊的成员变量,C++就设计了初始化列表,供这种只能初始化一次的成员变量进行初始化,如果使用上面的方式进行初始化编译器就会直接报错,所以初始化列表确保了初始化的唯一性,如下:
class A
{
public:
A(int& a, int& b)
:_a(a)
, _b(b)
{
}
private:
const int _a;
int& _b;
};
这就是为什么const成员变量和引用成员变量必须使用初始化列表进行初始化的原因,那么为什么自定义类型也要使用初始化列表进行初始化呢?其实跟上面两个的原因差不多,我们慢慢来分析,首先在有默认构造的情况下,编译器会自动调用这个自定义类型的默认构造,如下:
class B
{
public:
B(int b = 10)
{
_b = b;
}
private:
Date _d;
int _b;
};
int main()
{
B b;
return 0;
}
在这个例子中,类B中既有内置类型,又有自定义类型,B中只对内置类型作了处理,编译器也不会报错,因为编译器默认会去调用这个自定义类型的默认构造对这个自定义类型的成员变量作初始化,我们调试来看看: