深入篇【C++】类与对象:再谈构造函数之初始化列表与explicit关键字


在这里插入图片描述

Ⅰ.再谈构造函数

我们知道在创建对象时,编译器会通过调用构造函数,给对象中各个成员变量一个合适的初始值。

也就是我们可以通过构造函数来给对象中的成员变量赋值。不过给成员变量赋值其实有两种方式,一种就是在函数体内进行赋值,另一种是在初始化列表赋值。函数体内赋值我们是知道什么意思,那什么叫初始化列表呢?
我们知道创建一个对象,什么表示对象创建出来了呢?
对象实例化表示对象已经创建出来,这是对象整体定义的地方,然后对象就会调用构造函数进行初始化。
对象定义的地方是对象实例化,实例化后就会调用构造函数初始化。
那想一想对象成员变量是在哪里定义的呢?
对象实例化只是对对象整体定义的地方,而初始化列表才是对象成员变量定义的地方。
只有定义完后才可以初始化。
所以初始化列表是对象成员定义的地方。

①.构造函数体赋值

对象实例化后就会调用构造函数初始化对象。
在函数体内部进行赋值初始化。

```handlebars

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:
	int _year;
	int _month;
	int _day;
};

虽然上述构造函数调用之后,对象种已经有一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。
因为初始化只能初始化一次,但构造函数体内可以多次赋值。
本质上来说是因为对象成员变量不在函数体内定义,所以赋值后也不叫作初始化,只有在定义的地方赋值才可以叫做初始化,而对象成员变量定义的地方其实是初始化列表。

②.初始化列表赋值

初始化列表:以一个冒号开始,接着就是以一个逗号分割数据成员列表,每个成员变量后面跟上一个括号,括号里是初始值或者表达式。


class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)//这个就叫做初始化列表
		:_year(year)//以一个冒号开始,注意后面没有分号
		,_month(month)//逗号分割
		,_day(day)//每个成员变量后面都有一个括号,括号里是初始值或表达式
	{
	}
private:
	int _year;
	int _month;
	int _day;
};

【<特性分析>】

1.至多性

每个成员变量在初始化列表中至多出现一次,也可以不出现。
因为定义完后再初始化,而初始化只能初始化一次。
不出现的话那就会在函数体内进行初始化。

2.特殊成员必在性

类中包含以下成员时,必须放在初始化列表位置进行初始化。

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)

我们一个一个分析,为什么上面三个成员必须放在初始化列表初始化。
引用成员变量和const成员变量有什么特别之处呢?为什么会被要求放在初始化列表初始化呢?
引用成员变量和const成员变量都有一个特点:那就是在定义的时候必须初始化。
不然编译器会报错,而初始化列表正是变量定义的地方,在定义的地方给初始值才能成功的对引用成员变量和const成员变量初始化。如果在函数体内部进行赋值初始值,那这样不是初始化,因为函数体内部不是它们定义的地方,仅仅给个赋值是不能完成初始化的。


class B
{
public:
	
	B(int a, int ref)//初始化列表:成员变量定义的地方
		:_ref(ref)//引用
		, _n(1)//const修饰的
		
	{
	}
private:
	int& _ref;//引用成员变量
	//这两个特征就是必须在定义的时候就初始化
	const int _n;//const修饰的成员变量
};

第三种成员变量是自定义类型成员,并且当类中没有默认构造函数时,自定义类型成员必须放在初始化列表初始化,这是为什么呢?

class A
{
public:
	A(int a=0 )//有默认构造函数
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
private:
	int _a;
};
class B
{
public:
	//初始化列表:对象的成员定义的地方
	B(int a, int ref)
		:_ref(ref)//引用
		, _n(1)//const修饰的
	{
	}
private:
	A _obj;//有默认构造函数时,可以不用初始化
	int& _ref;
	const int _n;
	int _x = 1;
};

我们知道编译器生成的默认构造的工作是对自定义类型初始化,对内置类型不做处理。
所以当有默认构造函数时,自定义类型我们就不用去再初始化了。因为没有参数我也可以调用构造函数初始化。那没有默认构造函数呢?我们是不是就得手动给自定义类型成员变量进行初始化。但要注意的是函数体内不允许没有默认构造的自定义类型成员变量初始化,必须在初始化列表初始化。

在这里插入图片描述
道理其实是一样的,自定义类型在定义的时候也要进行初始化。
那怎么初始化呢? —调用构造函数。
如果有默认构造函数,那就可以之间使用默认构造函数初始化。
如果没有默认构造函数,那就必须在初始化列表进行初始化,因为初始化列表是成员变量定义的地方,如果要求在定义的时候进行初始化,那么必须得在初始化列表进行,函数体内部不是定义的地方,只是可以进行赋初始值,如果在函数体内部进行"初始化"其实是定义和赋初始值分开了,没有做到定义只是进行赋初始值。
而对于那些没有要求在定义时必须初始化的变量,既可以在初始化列表进行初始化,也可以在函数体内部进行初始化,可以做到定义和赋初始值分开。

class A
{
public:
	A(int a )//这个不是默认构造,这个是需要传参的构造函数
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
private:
	int _a;
};
class B
{
public:
	//初始化列表:对象的成员定义的地方
	B(int a, int ref)
		:_ref(ref)//引用
		, _n(1)//const修饰的
		,_obj(a)//自定义类型,无默认构造。
	{
		
	}
private:
	A _obj;//没有默认构造函数时,必须在初始化列表进行初始化。
	int& _ref;//引用
	const int _n;//const修饰的
	int _x = 1;
	
};

要区分默认成员函数和默认构造函数:
1.默认成员函数是C++规定的几种特殊的成员函数,是不写编译器可以自动生成的,有默认构造函数,默认拷贝函数,默认赋值函数等等。而默认构造函数是包含在内的。
2.默认构造函数有三种:总的特征就是不用传参就可以使用的函数。
无参的构造函数+全缺省的构造函数+编译器生成的默认构造函数都叫做默认构造函数。
3.并且默认构造函数只能有一个。

所以当自定义类型成员变量类型没有默认构造函数时(三种默认构造函数,自己写的带有参数构造函数)在函数体内部赋初始值是无法通过的,必须在初始化列表显式初始化,去调用自己写的构造函数初始化。

也就是当没有提供默认构造函数的自定义类型,必须在初始化列表初始化。

3.必走性:定义位置

尽量使用初始化列表初始化,为什么呢?
因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会显示有初始化列表初始化,并且所有成员变量都会经过初始化列表的。因为初始化列表是成员变量定义的地方。
每个成员变量在初始化列表至多出现一次,但也可以不出现,不出现的意思是不给定义的变量赋初始值,但这个变量是在初始化列表定义的,只不过没有给初始值,其实所有的成员变量都会走初始化列表。

class B
{
public:
	//初始化列表:对象的成员定义的地方
	B(int a, int ref)
		:_ref(ref)//引用
		, _n(1)//const修饰的
		,_obj(a)
		,_x(2)//对于那些没有要求必须在定义时初始化的既可以在初始化列表初始化
	{
	     //_x=2;
		//也可以在函数体内部初始化
	}
private:
	A _obj;
	int& _ref;
	const int _n;
	int _x ;
	//初始化列表没有显式定义_x就会使用这个缺省值
};

比如上面的内置类型_x既可以在初始化列表初始化,又可以在函数体内部初始化,在初始化列表初始化就是定义时就初始化了,而在函数体内部初始化就是在初始化列表定义完后到函数体内赋初始值完成初始化。

class B
{
public:
	//初始化列表:对象的成员定义的地方
	B(int a, int ref)
		:_ref(ref)//引用
		, _n(1)//const修饰的
		,_obj(a)
		
	{
	  
	}
private:
	A _obj;
	int& _ref;
	const int _n;
	int _x=1 ;//这个1是缺省值,缺省值是给初始化列表的
	

你们还记得缺省值吧,C++给默认构造函数打补丁就规定了可以在成员变量声明时给缺省时,这样对于内置类型,也可以完成初始化了,那现在看来这个缺省值是如何完成初始化的呢?
其实这个缺省值就是给初始化列表用的,因为每个成员变量都会走初始化列表,初始化列表会将缺省值赋值给已经定义好的成员变量,这样成员变量就完成了初始化了。
当显式的初始化时,初始化列表会优先选取显式给的初始值,而放弃掉缺省值,当没有显式的初始化时,初始化列表就会将缺省值赋值给成员变量 。

4.一致性

成员变量在类中声明次序就是在其初始化列表中的初始化顺序。
声明的顺序对应着要初始化的顺序,必须要一致,不然会出现问题。
比如下面这个问题:

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();
}

这个结果是什么呢?
在这里插入图片描述
为什么呢?注意到成员变量声明的次序,_a2先声明,_a1后声明,则表明_a2先进行初始化,_a1后进行初始化
在这里插入图片描述

5.不足性

初始化列表的使用虽然很方便,但也有它不足之处,比如当有些赋值后的成员需要检查是否赋值成功,再比如要求对数组进行初始化,初始化列表就无法完成这样的工作。

class Stack
{
public:
	Stack(int capacity=10)
		:_a((int*)malloc(sizeof(int)* capacity))
		,_top(0)
		,_capacity(capacity)
	{
		if (_a == nullptr)
		{
			perror("malloc");

			//要求数组初始化一下
			memset(_a, 0, sizeof(int) * capacity);
		}
	}
private:
	int* _a;
	int _top;
	int _capacity
};

再比如写一个动态二维数组:初始化列表就无法完成这样的工作,必须借助函数体来解决,所以总有一些工作是初始化列表完成不了的,这时就需要和函数体一起协同工作了。

class AA
{
public:
	AA(int row = 10, int col = 5)
		:_row(row)
		, _col(col)
	{
		_a = (int**)malloc(sizeof(int*) * row);
		for (int i = 0; i < row; i++)
		{
			_a[i] = (int*)malloc(sizeof(int) * col);
		}
	}
private:
	int** _a;
	int _row;
	int _col;
};

Ⅱ.explicit关键字

①.隐式类型转化

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值,其余均有默认值的构造函数,还具有类型转化的作用。

这其实就是隐式类型转化,看下面代码:

int main()
{
	int i = 0;
	double d1 = i;//这里发生了什么呢?
}

这里i是int类型,d1是double类型将i赋值给d1,会发生类型转化。发生什么样子的转化呢?
其实在这个过程中会生成一个临时变量,将i赋给临时变量,临时变量再赋给d1.
在这里插入图片描述

class A
{
public:
	A(int a)
		:_a(a)
	{

	}
	A(const A& a1)
	{
		_a = a1._a;
	}
private:
	int _a;
};
int main()
{
	A aa1(1);
	A aa2 = 4;
}

对象aa1调用构造函数初始化成1,那对象aa2在干嘛呢?
对象aa2其实是将整形的4赋给aa2,这里就涉及了隐式类型转化,将int类型隐式转换成A类型。
这里其实是4调用构造函数,构造一个A类型的临时对象,然后临时对象再拷贝构造给aa2。只不过编译器将这两步优化成一步,优化成直接构造。

如果不相信的话可以看看下面的代码:

int main()
{
	A aa1(1);
	A& aa2 = 4;
}

用对象aa2来引用4可以吗?在这里插入图片描述
这里肯定不行呀,这两个类型都不一样肯定不能引用。
但只要给对象aa2前面加上const修饰那么这样就可以引用了,这是为什么呢?
在这里插入图片描述

不知道你还记得临时变量具有常性这个特点吗?
正是因为这个特点,加上const修饰后就允许引用了,因为4会调用构造函数,构造出一个A类型的临时变量,而因为临时变量具有常性,相比较正常的A类型权限缩小了,所以不能相互引用,但一旦加上const修饰后,权限相同,就可以相互引用了。
所以在隐式类型转化时会产生一个临时变量的。

②.作用

如果不想在调用构造函数时构造出一个临时变量的话,就可以用关键字explicit。

使用explicit后构造函数就无法构造出临时变量了,这样的隐式类型转化就不会发生了。

class A
{
public:
	explicit A(int a)
		:_a(a)
	{

	}
	A(const A& a1)
	{
		_a = a1._a;
	}
private:
	int _a;
};
int main()
{
	A aa1(1);
	A aa2 = 4;
}

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小陶来咯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值