目录
- 初始化列表
- 特征
- 疑惑
- 区别
- 必在初始化列表中初始化的三种成员变量
- 1、引用成员变量
- 程序例子:
- 运行结果:
- 2、const成员变量
- 程序例子:
- 运行结果:
- 3、自定义类型成员(没有默认构造函数的类)
- 程序例子:
- 运行结果:
- 总结
初始化列表
特征
① 以一个冒号开始 ;
② 以一个逗号作为数据成员的分割标志 ;
③ 在每个成员变量后面跟个括号,括号内放入初始值或表达式 ;
如下:
class Date
{
public:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day) // 初始化列表
{}
private:
int _year;
int _month;
int _day;
};
注意:
1、每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2、有3种成员变量,必须放在初始化列表位置中进行初始化。哪3种变量在下面再做讲解。
疑惑
在学习之前,有没有这么一个疑问:前面学习 构造函数 的时候,如下:
class Date
{
public:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
不是已经实现了对象变量的初始化了吗?怎么又蹦出来个初始化列表一说,跟前面学的构造函数的初始化有什么区别吗?
答案是,两者是有所区别的。
首先,什么叫初始化?或者说初始化的文意是什么?初始化的含义是只运行一遍、只赋值一次…,即一次以后就没有初始化的什么事了。但是在前面学习的构造函数中,如下:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
_year = 2023;
_month = 12;
_day = 13;
}
我们可以在构造函数里面,对要初始化的变量,多次赋值,而这明显在概念上与初始化相违背。
确切的说,这种方式,构造函数内的语句只能称为赋初值,但不能称为初始化。因为初始化只能初始化 ( 赋值 ) 一次,而这里的构造函数体内可以多次赋值。这便是两者在概念上的差别。
区别
前面了解了两种 “初始化” 概念上的的区别后,那么有个问题,在功能上、实际效果中,两者有没有什么区别呢?
我们发现,不管是使用初始化列表:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day) // 初始化列表
{}
进行初始化,还是使用:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
这种方式实现“初始化”,这里两者在功能实现上,并没有什么区别啊。
是的,对于有些类型的成员变量,不管是放在初始化列表中进行初始化,还是放在构造函数体内进行初始化,两者在实际效果上并没有什么区别。
但是 ! 如前面特征中注意事项中的第二点提到的,有3种类型的成员变量,必须放在初始化列表中进行初始化。
下面对这3种类型依个进行学习。
必在初始化列表中初始化的三种成员变量
在类中成员变量为以下三种类型之一时,初始化必须要在初始化列表中才能够让编译器自动去初始化变量。
1、引用成员变量
当类中的成员变量类型为引用时,如下:
程序例子:
class A
{
public:
// 可以理解成初始化列表是对象的成员变量的定义的地方
A(int ref = 0)
:_ref(ref)
{}
void print()
{
cout << _ref << endl ;
}
private:
// 成员变量的声明
int& _ref; // 引用
};
int main()
{
A a1;
a1.print();
return 0;
}
运行结果:
收入眼帘的,我们发现在初始化列表中初始化过的引用成员变量_ref 并不是我们所想的初始化为0啊。这是因为,
引用成员变量_ref在初始化列表中,引用的是构造函数中的形参,形参缺省值是0,当程序运行到初始化列表中_ref的值确实是被初始化为0的,我们通过调试可以发现,结果如下:
但是出了构造函数后,形参ref在构造函数作用域内时开辟的空间将随着出了构造函数的作用域而被销毁,空间使用权归还编译器,这时 _ref 引用的空间依旧是原先形参开辟的空间,但出了构造函数后那块空间已经归还编译器了,因此那块空间将是随机值,所以打印的结果是随机值。
要想如我们所构想的,给引用成员变量_ref 初始化为0,那么我们要先给 _ref 提供一个有效的对象。这个对象并不会在出了构造函数后就被销毁,如:
class A
{
public:
static int s;
A()
:_ref(s)
{}
void print()
{
cout << _ref << endl;
}
private:
// 成员变量的声明
int& _ref; // 引用
};
int A::s = 0;
int main()
{
A a1;
a1.print();
A::s = 10;
a1.print();
return 0;
}
我们在类中声明了一个静态整形变量s,然后将_ref 初始化为静态整形变量s的别名,同时在类外,对s定义初始化为0。这时就完成了对 引用成员变量 _ref 的初始化,即绑定到静态对象s的地址上。
运行结果:
结果显示,当我们对静态整形变量s更改时,引用成员变量 _ref 的值也跟着更改。
那么如果,我们将引用成员变量 _ref 放在构造函数体内进行初始化呢?
static int s;
A()
{
_ref = s;
}
编译器会报错,编译不通过。具体是为什么,我是这么理解的,
首先,引用的特性是在创建时必须被初始化。在private中只是引用声明,还没有被创建,而当用类创建出对象时,编译器自动调用构造函数,对成员变量初始化。
其次,程序是先运行到初始化列表,而后才进入函数体内的。当运行到函数体内时,引用成员变量已经被创建出来了,而被创建出来后又没有对其初始化,即给定一个对象(在C++中对象和变量是等效的说法)。这违背了引用的特性,所以编译器会报错。
所以要在引用成员变量被创建之前,即在初始化列表中,对引用成员变量进行初始化。
可以这么理解,在初始化列表中 "_ref(s) " 时,引用成员变量_ref被创建出来并且作为了静态对象s的引用。而程序运行如果出了初始化列表进入函数体内时,已经完成了对引用成员变量 _ref 的创建了。
以上,便是必须在初始化列表中初始化的成员变量类型之一,引用成员变量。接下来,对下一个成员变量类型进行学习。
2、const成员变量
程序例子:
class B
{
public:
// 可以理解成初始化列表是对象的成员变量的定义的地方
B(int a = 0)
:_n(a)
{}
void print()
{
cout<< _n << endl;
}
private:
// 成员变量的声明
const int _n; // const
};
int main()
{
B b;
return 0;
}
运行结果:
原因和引用成员变量类似,也是因为const关键字的特性: const 变量在声明时必须被初始化,因为它们一旦被赋值之后就不能再修改。
所以当程序跑到构造函数体内时,const修饰的成员变量 _n 已经被创建出来了,但是又没有初始化,所以编译器会报错。
因此同样的,对于const修饰的成员变量的初始化,必须在初始化列表中。
下面学习最后一种必须在初始化列表中初始化的成员变量类型。
3、自定义类型成员(没有默认构造函数的类)
对于自定义类型成员,并且自定义类型是没有默认构造函数的类,就是只有需要传参才能对对象进行初始化的构造函数。如下:
程序例子:
class A
{
public:
A(int a)
:_a(a)
{}
int Getnum()
{
return _a;
}
private:
int _a;
};
class B
{
public:
// 可以理解成初始化列表是对象的成员变量的定义的地方
B(int a, int ref)
:_aobj(1)
{}
void print()
{
cout<< _aobj.Getnum() << endl;
}
private:
// 成员变量的声明
A _aobj; // 没有默认构造函数(需要传参才可以调用的构造函数)
};
int main()
{
B b;
b.print();
return 0;
}
程序中,类B中的有一个成员变量_aobj,类型是自定义类型类A。在类A中,有一个显示构造函数,但是因为该构造函数参数没有缺省值,因此要想调用这个构造函数时,必须得传参数。即类A中是一个没有默认构造函数(不用传参数就能调用的构造函数 )的类。
这种情况下,要想对类B中的成员变量_aobj 初始化时,必须得放到初始化列表中进行i初始化。先看看程序运行的结果,
运行结果:
结果如期所至,对象_aobj 的成员变量被初始化成1。
至于为什么没有默认构造函数的自定义类型的变量,初始化时必须放在初始化列表中,按照我的理解,和前面的原因是一致的:
因为程序跑到类B中构造函数体内时,成员变量类A实例出的对象 _aobj 已经被创建出来了,而创建出来时又没有给对象_aobj传入参数,又因为类A中没有默认构造函数,所以对象_aobj调用构造函数失败,即类B的成员变量_aobj初始化失败,导致类B实例的对象b初始化失败,因此编译器报错。
如果还有疑问:命名函数体内已经 " _aobj(1); " 传参数初始化了啊?为什么说没给对象_aobj传参呢?
对于这个疑问,可以理解为,当程序运行到构造函数体内的 " _aobj(1);" 时( 当_aobj初始化是放在构造函数体内,而不是初始化列表时 ),对象_aobj已经在初始化列表处的位置被创建出来了, 创建出来的时候又因为没有传入参数,而对象_aobj的类型A中又没有默认构造函数,所以对于对象_aobj调用类A的构造函数进行初始化时,编译出错。
至此,对于必须放在初始化列表中进行初始化的类型,介绍完毕。
如果到此还不太理解,可以联想以上三种类型,在普通函数或者main函数中,定义(创建)时的要求是什么?可以理解为是一样的概念。如下:
class C
{
public:
C(int c)
: _c(c)
{}
private:
int _c;
}
int main()
{
const int a; // 编译能通过吗?
int& b; // 编译能通过吗?
class C; // 编译能通过吗?
return 0;
}
以上在main函数中,创建出来的三个变量,a、b、c 能被编译器顺利通过吗?这个问题就留给你们自己去思考了。
总结
1、对于大多数类型的成员变量,初始化时放在初始化列表也行,放在构造函数体内也罢,效果都是一样的;
2、对于以上特殊的三种成员变量类型,必须放在初始化列表中进行初始化;
注意事项:
成员变量的初始化顺序就是成员变量在类中声明的顺序,跟在初始化列表中的先后次序无关
建议:
1、对于所有的成员变量的初始化,都放在初始化列表中进行初始化。因为 C++ 的设计中,构造函数的初始化列表就是专门用于初始化成员变量的地方。将所有成员变量的初始化都放在初始化列表中有助于提高代码的清晰度和一致性。
2、初始化列表中成员变量初始化的先后次序,与类中成员变量的声明顺序保持一致,有助于代码的可观读性。