再谈构造函数

146 阅读3分钟

「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战

一、再谈构造函数

💦 构造函数体赋值

❓ 引出初始化列表 ❔

class A
{
public:
	A(int a = 0)
	{
		_a = a;	
	}
private:
	int _a;
};
class B
{
private:
	int _b = 1;
	A _aa;
};
int main()
{
	B b;
	return 0;
}

📝 说明

对于 B,我们不写构造函数,编译器会默认生成 —— 内置类型不处理,自定义类型会去调用它的默认构造函数处理 (无参的、全缺省的、编译器默认生成的),注意无参的和全缺省的只能存在一个,如果写了编译器就不会生成,如果不写编译器会默认生成。这里 C++ 有一个不好的处理 —— 内置类型不处理,自定义类型处理。针对这种问题,在 C++11 又打了一个补丁 —— 在内置类型后可以加上一个缺省值,你不初始化它时,它会使用缺省值初始化。这是 C++ 早期设计的缺陷。

class A
{
public:
	A(int a = 0)
	{
		_a = a;	
		cout << "A(int a = 0)" << endl;
	}
	A& operator=(const A& aa)//不写也行,因为这里只有内置类型,默认生成的就可以完成
	{
		cout << "A& operator=(const A& aa)" << endl;
		if(this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
private:
	int _a;
};
class B
{
public:
	B(int a, int b)
	{
		//_aa._a = a;//err:无法访问private成员
		
		/*A aa(a);
		_aa = aa;*/ 
		_aa = A(a);//简化版,同上
		
		_b = b;
	}
private:
	int _b = 1;
	A _aa;
};
int main()
{
	B b(10, 20);
	return 0;
}

📝 说明

对上,_b只能初始化成1,_a只能初始化成0 ❓

这里可以显示的初始化,利用匿名对象来初始化 _a。

但是这种方法代价较大 (见下图)。

在这里插入图片描述

💦 初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 "成员变量" 后面跟一个放在括号中的初始值或表达式。

class A
{
public:
	A(int a = 0)
	{
		_a = a;	
		cout << "A(int a = 0)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if(this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
private:
	int _a;
};
class B
{
public:
	B(int a, int b)
		:_aa(a)
	{
		_b = b;
	}
private:
	int _b = 1;
	A _aa;
};
int main()
{
	B b(10, 20);
	return 0;
}

📝说明

在这里插入图片描述

可以看到对比函数体内初始化,初始化列表初始化可以提高效率 —— 注意对于内置类型你使用函数体或初始化列表来初始化没有区别;但是对于自定义类型,使用初始化列表是更具有价值的。这里还要注意的是函数体内初始化和初始化列表是可以混着用的。 在这里插入图片描述

❓ 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化 ❔ 在这里插入图片描述

什么成员是必须使用初始化列表初始化的 ❓

class A
{
public:
	A(int a)
		:_a(a)
 	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
 		:_aobj(a)
 		,_ref(ref)
 		,_n(10)
	{}
private:
	A _aobj;//没有默认构造函数
	int& _ref;//引用
	const int _n;//const 
};

⚠ 注意

1️⃣ 每个成员变量在初始化列表 (同定义) 中只能出现一次 (初始化只能初始化一次)。

2️⃣ 类中包含以下成员,必须放在初始化列表位置进行初始化:

  1、引用成员变量 (引用成员必须在定义的时候初始化)

  2、const 成员变量 (const 类型的成员必须在定义的时候初始化)

  3、自定义类型成员 (该类没有默认构造函数)

❓ 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中出现的先后次序无关 ❔

#include<iostream>
using namespace std;
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  随机值

如上程序的输出结果是 D 选项,因为 C++ 规定成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其初始化列表中出现的先后次序无关。实际中,建议声明顺序和初始化列表顺序保持一致,避免出现这样的问题。

💦 explicit关键字

class A
{
public:
	A(int a)
		:_a(a)
	{
		cout << "A(int a)" << endl;	
	}
	A(const A& aa)
	{
		cout << "A(const A& aa)" << endl;	
	}
private:
	int _a;
};
int main()
{
	A aa1(1);
	A aa2 = 1;
	return 0;
}

📝 说明

A aa2 = 1; 同 A aa1(1); 这是 C++98 支持的语法,它本质上是一个隐式类型转换 —— 将 int 转换为 A,为什么 int 能转换成 A 呢 ? —— 因为它支持一个用 int 参数去初始化 A 的构造函数。它俩虽然结果是一样的,都是直接调用构造函数,但是对于编译器而言过程不一样。

在这里插入图片描述

🍳验证

在这里插入图片描述

🔑拓展

针对于编译器优化、底层机制这类知识可以去了解一下《深度探索C++对象模型》

❓ 如果不想允许这样的隐式类型转换的发生 ❔

这里可以使用关键字 explicit

explicit A(int a)
	:_a(a)
{
	cout << "A(int a)" << endl;	
}

error C2440:无法从 int 转换成 A

❓ 多参数隐式类型转换 ❔

class A
{
public:
	A(int a1, int a2)
		:_a(a1)
	{
		cout << "A(int a1, int a2)" << endl;	
	}
	A(const A& aa)
	{
		cout << "A(const A& aa)" << endl;	
	}
private:
	int _a;
};
int main()
{
	A aa1(1, 2);
	//A aa2 = 1, 2;//???
	A aa2 = {1, 2};
	return 0;
}

📝说明

A aa2 = 1, 2; ???

明显 C++98 不支持多参数的隐式类型转换,但是 C++11 是支持的 —— A aa2 = {1, 2}; ,同样编译器依然会优化。

当我们使用 explicit 关键字限制时,它会 error C2440:无法从 initializer-list 转换为 A