吃透C++类和对象(下):初始化列表深度解析

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

一、再遇构造函数(初始化列表)

1、初始化列表的结构

2、初始化列表的意义

[3、针对 const 成员和引用成员](#3、针对 const 成员和引用成员)

4、针对自定义类型成员

5、声明给缺省值

缺省值总结

6、构造函数形参缺省值与声明缺省值的区别和联系

1、有初始化列表

2、没有初始化列表

[2.1 有声明给缺省值](#2.1 有声明给缺省值)

[2.2 没有声明给缺省值](#2.2 没有声明给缺省值)

7、初始化列表练习

结束语


一、再遇构造函数(初始化列表)

1、初始化列表的结构

之前我们实现构造函数时,初始化 成员变量主要使用函数体内赋值 ,构造函数初始化还有一种方式,就是初始化列表 ,初始化列表的使用方式是以一个冒号开始 ,接着是一个以逗号分隔 的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

cpp 复制代码
//Date.cpp
Date::Date(int year, int month, int day)
	:_year(year)
	, _month(month)
	, _day(day) //初始化列表
{

}

在上面我把"赋值"进行标红,原因就在于在构造函数体内 进行的并不是我们以为的"初始化" ,而只是一次赋值。这句话非常的重要,虽然对于内置类型的成员变量没有什么区别,但对于等会讲解的const成员以及引用成员会有非常大的影响。

2、初始化列表的意义

每个成员变量在初始化列表中只能出现一次 ,语法理解上初始化列表 可以认为是每个成员变量定义初始化的地方

这句话可谓是初始化列表的意义所在,也是等会解决所有问题的关键。C++规定了:初始化列表是唯一能够指定非静态数据成员初始化的地方,并且成员初始化在构造函数体执行之前就已经完成了初始化

这也是为什么我会说在函数体内所写的 _year = year; 等并非是我们认为的"初始化",因为当编译器执行到函数体内 时成员变量都已经完成了初始化 ,此时只是再次赋值

3、针对 const 成员和引用成员

首先我们回顾一下 const 成员、引用成员与内置类型成员的区别:

内置类型 我们说过在定义的时候可以选择不初始化,编译器会进行"默认初始化 ",也就是生成一个随机值 ;但是对于 const 成员、引用成员不一样,这两个在定义的时候就必须立即绑定初始化,若没有初始化则程序报错:

对于类而言,成员变量的定义是发生在类定义时,而并不是在构造函数调用时定义:

cpp 复制代码
class Date
{
public:
	Date(int& x, int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
		, _ref(x)
		, _n(1)//初始化列表
	{

	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
    //声明
	int _year;
	int _month;
	int _day;
	int& _ref;
	const int _n; //这里并非是定义而只是声明,只是告诉你有这个东西
	              //而我们讲过 const 和引用在定义时就必须立即绑定初始化,但这里只是声明就不会报错
	              //真正定义初始化的地方是在构造函数的初始化列表中
};

所以类成员真正进行初始化 的地方就是在构造函数体之前的初始化列表 ,如果没有显示写初始化列表对于内置类型成员变量并没有影响(因为可以"默认初始化"成随机值),但对于 const 和引用成员变量而言也就相当于错过唯一一次初始化 的机会,此时当进入到函数体内时就已经变成了"未初始化的定义 "而报错,即使在函数体内写 _n = 1; 这种,也不是我们所认为的"初始化",而是上面我所讲的赋值,而且由于 const 成员一旦绑定了一个初始值就不能再进行修改了:

4、针对自定义类型成员

在之前吃透C++类和对象(中):构造函数与析构函数深度解析文章中我们讲过:对于自定义类型成员变量要求调用这个成员变量的默认构造函数初始化 。如果这个成员变量没有默认构造函数,那么就会报错。

那如果真就没有默认构造函数怎么去初始化呢?也就是在初始化列表中进行初始化:

cpp 复制代码
class Time
{
public:
	Time(int hour) //有参构造函数,此时_t没有对应的默认构造函数,就必须在初始化列表进行初始化,否则报错
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int& x, int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
		, _t(1) //对于自定义类型成员,如果存在了对于的默认构造函数是可以不在初始化列表进行初始化
		        //但是如果没有对于的默认构造函数则一定要在初始化列表进行初始化
		        //写了初始化相当于就变成了 Time _t(1); 此时有了实参也就能调用有参的构造函数了
	{

	}

private:
	int _year;
	int _month;
	int _day;

	Time _t;
};

当我们在初始化列表中写**_t(1)** 相当于就能理解为对 _t 定义初始化成:Time _t(1); 此时就能调用对应的有参构造函数 了。

但是如果既没有对应的默认构造函数也没有在初始化列表中进行初始化,编译器就找不到对应函数导致报错:

对于 const 成员、引用成员和自定义类型成员我们可以进行一个总结:

引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。

5、声明给缺省值

C++11支持在成员变量声明 的位置给缺省值 ,这个缺省值主要是给没有显示初始化列表初始化的成员使用的。

cpp 复制代码
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
	{
		
	}

	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	//C++11
	//声明位置给缺省值 -> 初始化列表用的
	int _year = 1;
	int _month = 1;
	int _day;

	int* _ptr = (int*)malloc(10); //这说明缺省值不一定就是值,也可以是表达式

	int i = 1;
	int& _ref = i;
	const int _n = 1;
	
	Time _t = 1;
	//如果给了缺省值则对于 const 成员、引用成员和没有默认构造函数的类成员都允许不显示在初始化列表中初始化
};


int main()
{
	Date d1(2025, 1, 15);//对于 _day 既没有给缺省值也没有在初始化列表进行初始化的内置类型成员
	                     //则就是生成一个随机值
	d1.Print();
	return 0;
}

尽量使用初始化列表初始化 ,因为那些你不在初始化列表初始化的成员也会走初始化列表 ,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。

cpp 复制代码
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
	{
		
	}

	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	//C++11
	//声明位置给缺省值 -> 初始化列表用的
	int _year = 1;
	int _month = 1;
	int _day = 1;

	int* _ptr = (int*)malloc(10);

	int i = 1;
	int& _ref = i;
	const int _n = 1;
	
	Time _t = 1;
};


int main()
{
	Date d1(2025, 1, 15); //但是给了缺省值并且没有在初始化列表进行初始化,
	                      //则初始化列表会用这个缺省值进行初始化
    //但是如果既有缺省值也在初始化列表进行初始化则仍以初始化列表进行初始化
	d1.Print();
	return 0;
}

缺省值总结

所以对于声明给缺省值进行一个总结:

每个成员都要走初始化列表

(1)在初始化列表中进行初始化的成员,不管有没有缺省值都以初始化列表进行初始化

(2)没有在初始化列表进行初始化的成员

a.声明的地方有缺省值则用缺省值进行初始化(对于自定义类型而言有了缺省值相当于就可以调用对应的有参构造函数了)

b.如果没有缺省值:

1)内置类型:不确定是否初始化(看编译器),大概率为随机值

2)自定义类型:调用自己的默认构造函数,如果没有则报错

(3)引用成员、 const 成员和没有默认构造的自定义类型,这三者声明如果没有给缺省值,则必须在初始化列表中进行初始化

6、构造函数形参缺省值与声明缺省值的区别和联系

前面我们已经把初始化列表讲解的差不多了,但是讲完后有人就对构造函数形参提供缺省值和声明成员变量提供缺省值,这两者之间发生了混淆。这里我就好好讲解一下两者的区别和联系:

构造函数形参有没有缺省值决定的是:一个对象能否调用这个构造函数。如果一个对象没有实参则需要调用默认构造函数,但是此时如果没有缺省值则就变成了有参构造函数,则对象无法进行调用而报错,就不存在后续分析了。

1、有初始化列表

如果显示写了初始化列表 则其实就没声明缺省值什么事了 ,即使给了缺省值也不会利用。原因是显示写了初始化列表则初始化的值由形参来决定,如果是全缺省构造函数且对象没有实参则初始化结果由缺省值来决定:

cpp 复制代码
class Time
{
public:
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
	
	}

	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	//C++11
	//声明位置给缺省值 -> 初始化列表用的
	int _year = 2026;
	int _month = 1;
	int _day = 15;
    //此时由于显示写了初始化列表,声明给缺省值也不会被利用了,由形参来决定
};


int main()
{
	Date d1; //但是给了缺省值并且没有在初始化列表进行初始化,
	         //则初始化列表会用这个缺省值进行初始化
	         //但是如果既有缺省值也在初始化列表进行初始化则仍以初始化列表进行初始化
	d1.Print();
	return 0;
}

我们会发现打印的结果并不是声明给的缺省值而是形参的缺省值 ,这就说明了如果显示写了初始化列表则初始化的值由形参来决定

2、没有初始化列表

2.1 有声明给缺省值

如果没有显示写初始化列表,但是声明提供了缺省值,则对象初始化的结果就完全受声明缺省值的影响了,不管函数形参有没有缺省值还是对象有没有提供实参都没有意义:

cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
	
	}

	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	//C++11
	//声明位置给缺省值 -> 初始化列表用的
	int _year = 1111;
	int _month = 1;
	int _day = 11;
};


int main()
{
	Date d1(2026, 1, 15); //但是给了缺省值并且没有在初始化列表进行初始化,
	                      //则初始化列表会用这个缺省值进行初始化
	                      //但是如果既有缺省值也在初始化列表进行初始化则仍以初始化列表进行初始化
	d1.Print();
	return 0;
}

通过打印的结果我们也能印证前面所说的:没有显示写初始化列表 ,如果这个成员在声明位置给了缺省值初始化列表会用这个缺省值初始化

2.2 没有声明给缺省值

但是如果既没有显示写初始化列表也没有声明给缺省值,我先说结论:就没有真正意义上的"初始化"

那有人就问了之前没有学习初始化列表和声明给缺省值对象也被初始化了这是为什么?

因为这里我们认为的"初始化"并不是成员变量真正的初始化,而是被函数体内再次进行了赋值 。如果既没有显示写初始化列表也没有声明给缺省值,当进入函数体时成员变量就已经完成了初始化,只是大概率为随机值 而已,正是在函数体内又进行了一次赋值打印的结果就变成了我们认为的"初始化"。

但是如果在函数体内也没有进行赋值,不管对象有没有提供实参还是形参有没有缺省值,则结果就是随机值:

cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		
	}

	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2026, 1, 15); //但是给了缺省值并且没有在初始化列表进行初始化,
	                      //则初始化列表会用这个缺省值进行初始化
	                      //但是如果既有缺省值也在初始化列表进行初始化则仍以初始化列表进行初始化
	d1.Print();
	return 0;
}

7、初始化列表练习

下面程序的运行结果是什么()

A.输出1 1

B.输出2 2

C.编译报错

D.输出1 随机值

E.输出1 2

F.输出2 1

cpp 复制代码
class A
{
public:
	A(int a)
		:_a1(a)
		,_a2(_a1)//先初始化_a2再初始化_a1
	{

	}
	void Print() 
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
	//初始化列表中按照成员变量在类中声明顺序进行初始化,
	//跟成员在初始化列表出现的的先后顺序无建议声明顺序和初始化列表顺序保持一致。
};

int main()
{
	A aa(1);
	aa.Print(); //1 随机值
}

在讲这道题之前我们还有讲一下初始化列表最后一个特点:

初始化列表中按照成员变量在类中声明顺序 进行初始化,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。

由题目可知先被初始化的成员变量是 _a2,再是 _a1,而在初始化 _a2 时 _a1 还没有被初始化仍是随机值,所以 _a2 被初始化的值就是随机值,但要注意的是这里 _a2 随机值是真正意义上的"初始化";再初始化 _a1,值为实参的值1。所以答案选D

这就又印证了我们所说的:当显示写了初始化列表时,声明是否提供缺省值已经没有意义了,初始化的结果完全由形参来决定。

结束语

到此,初始化列表就详细讲解完了,所以构造函数的所有知识点也就全部讲解了,初始化列表在理解上的确比较绕和麻烦,所以需要好好进行消化。希望这篇文章对大家学习C++能有所帮助!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

相关推荐
曼巴UE52 小时前
UE5 C++ GameInstanceSubsystem 在学习
c++·ue5·ue
Ethan Wilson2 小时前
VS2019 C++20 模块相关 C1001: 内部编译器错误
开发语言·c++·c++20
m0_748252382 小时前
Bootstrap 5 加载效果实现方法
c++
人工智能AI技术3 小时前
GitHub Copilot 2026新功能实操:C++跨文件上下文感知开发,效率翻倍技巧
c++·人工智能
大志若愚YYZ3 小时前
ROS2学习 C++中的this指针
c++·学习·算法
玖釉-4 小时前
[Vulkan 学习之路] 16 - 最终章:渲染循环与同步 (Rendering & Presentation)
c++·windows·图形渲染
狗狗学不会4 小时前
Pybind11 封装 RK3588 全流程服务:Python 写逻辑,C++ 跑并发,性能起飞!
c++·人工智能·python·目标检测
DYS_房东的猫4 小时前
《 C++ 零基础入门教程》第10章:C++20 核心特性 —— 编写更现代、更优雅的 C++
java·c++·c++20
Howrun7774 小时前
虚幻引擎_AController_APlayerController_AAIController
开发语言·c++·游戏引擎·虚幻