💖作者:小树苗渴望变成参天大树
❤️🩹作者宣言:认真写好每一篇博客
💨作者gitee:gitee
💞作者专栏:C语言,数据结构初阶,Linux,C++
文章目录
- 前言
前言
经过前面的好几篇博客,大家应该感受到类和对象的难度所在了吧,c++之所以难学,就是因为有许多细节,我们不小心就会出错,虽然难学,但我们要克服,迎难而上,接下来我们来学习类和对象的其他知识点,这里面有点知识点还是不太好理解的,所以我打算分好几篇博客来写,这样大家看的就不会太乱,这篇我们来讲解初始化列表
大家还记得我们之前的构造函数是怎么写的吗?我们有三个默认构造函数,还有半缺省的和不缺省的我们来看:(以日期类为例)
date(int year,int month,int day)//这是不缺省的构造函数
{
_year=year;
_month=month;
_day=day;
}
这种构造函数其实是初始化的其中一种方式:函数体内赋值
我们另一种初始化的方式是:初始化列表
再此之前我们其实已经接触过初始化列表,只是之前不知道而已
date()
{}
date(int year)
{}
date(int year,int month,int day)
{}
这些可以认为是初始化列表,只是列表里面什么都没有。
我们来写一个类:
class date
{
public:
friend ostream& operator<<(ostream& cout, const date& d);
private:
//成员变量的声明
int _year;
int _month;
int _day;
};
date d1;//对象的整体定义
我们看到再创建对象的时候,是对对象整体的定义,那么成员变量的定义再哪里呢??
初始化列表就是成员变量定义的地方也就是初始化的地方,函数体只是对定义好并且为初始化的变量进行赋值,例如:
1.
int a;//初始化列表
a=10;//函数体内赋值
2.
int b=10;//初始化列表
对于第一种就相当于函数体内赋值,初始化列表什么也不写,第二种就是把内容写出来,例如:
1.
date()
{a=10;}
2.
date()
:a(10)
{}
如果函数体内不赋值,初始化列表不写,也会对成员变量进行定义,只是不初始化,你写了肯定就是初始化了,没办法单独写一个变量,例如:
date()
:a
{}//这种是不行的
相信大家对初始化列表有了一定的认识,我们开始正式介绍初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)//这才是定义并初始化
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
【注意】
- 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)
我们先来看看前两个
class B
{
public:
B(int a, int ref)
:_ref(ref)
,_n(10)
{}
private:
int& _ref; // 引用
const int _n; // const
};
我们的引用和const有一个共同的特性就是必须初始化
我们再上面说过初始化列表是成员变量的定义的地方,所以我们只能使用初始化列表,不能使用函数体内赋值,原因是函数体内赋值是再定义之后的行为,初始化是在定义时候的行为
这种写法不完全正确,我们说过引用是变量的别名,那么我们再初始化列表引用的是形参ref,出了函数就会被销毁,那么这个引用就没有任何意义,我们应该传引用,外面传变量:
B(int a, int& ref)
:_ref(ref)
,_n(10)
{}
再括号里面可以写自己的参数,也可以传固定的值
class A
{
public:
A(int a)
{_a=a;}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
{}
private:
A _aobj; // 没有默认构造函数
};
对于自定义类型,再构造函数的时候,我们会调用本身的默认构造函数,默认构造是不需要传参数就可以调用的,所以在初始化列表可以什么都不用写,但是没有默认构造函数的时候,我们必须就要进行传参的初始化,再定义A对象的时候,直接调用:A a(20),但是对于B我们要定义对象的时候不能直接传参给_aobj变量进行初始化,只能通过初始化列表,才能达到要求,但是你有默认构造的时候就不需要写。
可以这么简单来说,初始化列表替代了90%的函数体内赋值能完成的事情。
初始化列表完成不了的事:
我们来看看之前的栈类:
typedef int DateType;
class stack
{
public:
stack(int capacity=4)
:_array(( DataType*)malloc(sizeof(DataType)*capacity))
, _capacity(capacity)
,_size(0)
{
if(_array=NULL)//检查
{
perror("malloc:");
}
memset(_array,0,sizeof(DataType)*capacity);//初始化为0
}
private:
DataType* _array;
int _capacity;
int _size;
};
大家看到有些必须在函数体内才能完成操作
初始化列表的优点之处:
我们在之前使用两个栈实现一个队列
class myqueue
{
public:
myqueue(int capacity)
:_push(capacity)
,_pop(capacity)
{}
private:
stack _push;
stack _pop;
};
这两个栈我想初始化我想要的容量,通过本身myqueue自身的构造函数去调用栈里面构造函数打不到我们想要的效果,只有通过初始化列表去完成
大家应该看到效果了,其实初始化列表对于自定义类型回去调用它本身的默认构造,对于内置类型在给缺省值的时候也会做相应的处理:
优先使用你初始化列表里面的值,没有就使用缺省值,就相当于备胎。
初始化列表的细节:
我们来看下这个代码:
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 随机值
我们看看,居然不是输出1 1,而出现了一个随机值,原因是初始化列表的初始顺序是根据成员变量的位置来进行初始话,在类中 _a2在_a1上面,所以先初始化_a2,此时_a1是随机值,所以_a2是随机值,_a1在定义初始化变成了1
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
所以初始化列表的顺序应该和成员变量声明的顺序一致就不会出什么问题
explicit关键字:
我们来看一个类:
class Date
{
public:
Date(int year)
:_year(year)
{}
private:
int _year;
int _month;
int _day;
};
Date d1(2023);
上面的代码大家应该看得懂,给d1的年进行初始化为2023
但我要这么写呢?d1=2024;
这里面存在隐式类型转换,在类型转换的时候,会创建临时变量,对于这个,整型会转换成自定义类型,先构造一个对象,在拷贝构造给d1。
现在的编译器会优化成直接构造,我们把拷贝构造写出来,看看是不是进行优化了
但是此转换仅仅适用于只需要传一个参数的对象。
大家应该可以明白我想要表达的意思了吧
怎么证明它发生了类型转换呢,之前说过有隐式类型转换,会创建临时变量,临时变量具有常性,我们用引用来看看:
上面是权限的放大,下面是权限的平移,希望大家可以理解。如果不想发生这种转换,我们就使用explicit关键字
这个关键字在后面学到String类的时候可能会用到,大家想要具体了解可以看一下这篇博客explicit
对于今天的重点主要是初始化列表,以后我们大部分使用的都是初始化列表,现在不咋熟练没事,经常使用就好,下篇我将讲解static成员,大家也复习复习我讲的const成员。细节非常多,大家要理解,我们下篇再见