类和对象(下):初始化列表与隐式类型转换

本文代码已同步Github

一、再探构造函数

在前面我们实现构造函数时,初始化成员变量的方式是在函数体内进行赋值;

但除了在函数题内赋值的方式进行初始化,加油初始化列表这种方式

1、初始化列表的定义

初始化列表是构造函数定义中用于直接初始化类成员变量的一种语法结构;

  • 语法位置 :位于构造函数参数列表之后、函数体之前,以冒号 : 开头,多个成员初始项之间用逗号 , 分隔
  • 常见形式成员名(初始值)成员名{初始值}(C++11 起)

我们通过日期类来看初始化列表的基本使用:

cpp 复制代码
class Date
{
public:
	//在函数体内赋值
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//使用初始化列表
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

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

2、初始化列表的特点

  • 每个成员变量在初始化列表中只能出现一次,可以理解为初始化列表是每个成员变量定义初始化的地方;
  • 对于引用成员变量const成员变量没有默认构造的类类型变量,必须在初始化列表中初始化

我们通过代码来解释:

cpp 复制代码
Date(int year, int month, int day)
    : _year(year)
        , _month(month)
        , _day(day)
        ,_year(year)//error C2437: "_year": 已初始化
    {}

接着来看对于必须要在初始化列表进行初始化的成员变量:

cpp 复制代码
class B
{
public:
	//没有默认构造
	B(int x,int y)
	{
		_x = x;
		_y = y;
	}

private:
	int _x;
	int _y;
};

class A
{
public:
	A(int& a, int n, B b)
		:_ra(a)
		, _n(n)
		, _b(b)
	{
		//error C2530: "A::_ra": 必须初始化引用
		//error C2789: "A::_n": 必须初始化常量限定类型的对象
		//error C2512: "B": 没有合适的默认构造函数可用
	}

private:
	int& _ra;    //引用
	const int _n;//const
	B _b;        //没有默认构造
};

对于上述类型,如果不在初始化列表进行初始化,就会报错


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

在理解这句话之前,我们必须要明确构造函数中的参数的作用;

缺省参数控制的是调用能否省略实参;

而在成员变量声明位置给出的缺省值是给没有显示在初始化列表进行初始化的成员使用;

此处的缺省值缺省参数没有关联!

下面通过代码来理解:

cpp 复制代码
#include <iostream>

using namespace std;

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

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

private:
    //此处的缺省值是给没有显示初始化的成员变量使用
	int _year = 2026;
	int _month = 6;
	int _day = 13;
};

int main()
{
	Date d1;
	d1.Print();

	return 0;
}

分析:

采用无参方式调用默认构造函数,进行初始化时,_year,_month直接使用缺省值进行初始化;而_day由于没有在初始化列表中显示初始化,会采用类内初始值直接初始化


  • 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关
cpp 复制代码
class A
{
public:
	//先初始化_a,在初始化_b
	A(int a = 1, int b = 1)
		:_b(b)
		, _a(a)
	{ }

private:
	
	int _a = 0;
	int _b = 0;
};

所有成员变量在进入构造函数体之前都会被初始化,初始化的方式分为两种

  1. 在初始化列表中显式指定的成员 → 使用初始化列表中的表达式进行初始化。
  2. 未在初始化列表中出现的成员 → 编译器会自动 使用类内初始值 (如果提供了)或者默认初始化(对于内置类型且无类内初始值时,值为随机)。
  • 对于自定义类型,初始化列表可避免一次无意义的默认构造,效率更高;对于内置类型则没有效率差异,因此建议使用初始化列表来初始化

3、总结

注:引用成员必须在初始化列表中初始化;const 成员和类类型成员若未在初始化列表中显式初始化,则使用类内初始值,否则调用默认构造函数(对类类型而言)

成员变量进入初始化列表的逻辑:

首先,根据声明顺序进行初始化;

对于内置类型,如果有参数值,并且显示初始化,直接采用参数值进行初始化即可;如果没有显示初始化,则根据类内初始值来初始化;如果没有类内初始值,可能初始化为随机值

对于自定义类型,如果有参数值,并且显示初始化,则会调用自定义类型的匹配的构造函数;如果没有显示初始化,则根据类内初始值调用匹配的构造函数;如果没有显示初始化,也没有类内初始值,就会调用他的默认构造函数。

二、类型转换

1、隐式类型转换

  • C++支持内置类型隐式转换为类类型对象,需要有相关内置类型为参数的构造函数

下面通过代码来解释

cpp 复制代码
class A
{
public:
	A(int a)
		:_a(a)
	{ }

	void Print() const
	{
		cout << _a << endl;
	}

private:
	int _a;
};

int main()
{
	//1构造一个A的临时对象,再用这个临时对象拷贝构造给aa1
	//本质是整形隐式转换成类类型
	A aa1 = 1;
	aa1.Print();
	
	return 0;
}

2、类类型转换

  • 类类型对象之间也可以隐式转换,需要相应的构造函数支持

通过下面例子来理解:

cpp 复制代码
class A
{
public:
	A(int a)
		:_a(a)
	{ }

	int Get() const
	{
		return _a;
	}

	void Print() const
	{
		cout << _a << endl;
	}

private:
	int _a;
};

class B
{
public:
	B(int b = 0)
		:_b(b)
	{ }

	//转换构造函数
	B(const A& a)
		:_b(a.Get())
	{ }

	void Print() const
	{
		cout << _b << endl;
	}

	void Push(const A& a) const
	{ }
private:
	int _b = 0;
};

int main()
{

	A aa2(1);
	aa2.Print();

	B b1;
	b1 = aa2;
	b1.Print();

	B b2;
	A aa3 = 3;
	b2.Push(aa3);

	//上述Push等价于下面
	//3构造一个A的临时对象,然后直接Push到b2
	b2.Push(3);

	return 0;
}

3、explicit

  • 构造函数前加上explicit就不再支持隐式类型转换

我们通过一个简单的例子来说明:

cpp 复制代码
class A
{
public:
	explicit A(int a)
		:_a(a)
	{}
	//A(int a)
	//	:_a(a)
	//{}

	int Get() const
	{
		return _a;
	}

	void Print() const
	{
		cout << _a << endl;
	}

private:
	int _a;
};

int main()
{
	// error C2440: "初始化": 无法从"int"转换为"A"
	A aa = 1;
	return 0;
}

三、static成员

1、static的定义

static 是一个多义的关键字,其具体含义取决于使用的上下文。它可以修饰局部变量全局变量/函数类的成员变量类的成员函数

2、static成员

  • static修饰的成员变量称为静态成员变量,静态成员变量一定要在类外进行初始化
  • 静态成员变量为所有类共享,不属于某个具体对象中,不存放在对象中,存放在静态区
  • 静态成员变量不能在声明位置给缺省值初始化,此处缺省值是给初始化列表使用的,静态成员不属于某个对象,不会进入初始化列表
cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{ }

private:
	int _a;
	//静态成员变量不能给缺省值
	//类里面声明
	static int _sta;
};

//静态成员变量在类外初始化
int A::_sta = 0;
  • static修饰的成员函数称之为静态成员函数,静态成员函数没有this指针
  • 静态成员函数可以访问其他的静态成员,但不能访问非静态成员,因为没有this指针
  • 非静态成员函数可以访问静态成员变量和静态成员函数
  • 静态成员也是类的成员,受访问限定符限制
cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{ }

	
	static int Get()
	{
		//静态成员函数不能访问非静态成员变量
		//error C2597: 对非静态成员"A::_a"的非法引用
		// return _a;

		return _sta;
		
	}

private:
	int _a;
	//静态成员变量不能给缺省值
	//类里面声明
	static int _sta;
};

//静态成员变量在类外初始化
int A::_sta = 0;

int main()
{
	cout << A::Get() << endl;

	A aa;

	//error C2248: "A::_sta": 无法访问 private 成员(在"A"类中声明)
	//cout << aa._sta << endl;
	return 0;
}

四、友元

1、友元的定义

类的私有成员(private)和保护成员(protected)通常不允许外部函数或其它类访问,这是封装性的核心。但有时为了性能或语法的便利,需要让某个特定的外部函数另一个类 能够直接访问类的私有/保护成员,而不通过公有接口。友元(friend)就是为此设计的机制。

友元分为:友元函数友元类

2、友元函数

  • 友元函数是声明,不是类的成员函数
  • 友元函数可以在类定义的任何地方声明,不受访问限定符限制
  • 一个函数可以是多个类的友元函数
cpp 复制代码
class A
{
public:
	////友元函数可以在类的任意地方声明
	//friend void func();
	
	A(int m = 0,int n = 0)
		:_m(m)
		,_n(n)
	{ }

private:
	//友元函数可以在类的任意地方声明
	friend void func();
	int _m;
	int _n;
};

class B
{
public:
	//一个函数可以是多个类的友元函数
	friend void func();

	B(int b = 0)
		:_b(b)
	{ }
	
	
private:
	int _b;
};

void func()
{
	A a;
	//正常情况下类的私有成员不能被类外函数访问
	//error C2248: "A::_m": 无法访问 private 成员(在"A"类中声明)
	//error C2248: "A::_n": 无法访问 private 成员(在"A"类中声明)
	//cout << a._m << " " << a._n << endl;

	//加上友元声明之后即可访问
	cout << a._m << " " << a._n << endl;


	B b;
	cout << b._b << endl;
}

int main()
{
	func();
	return 0;
}

3、友元类

  • 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有保护成员
  • 友元类的关系不具有交换性,是单向的,比如A是B的友元,但B不是A的友元
  • 友元类不具有传递性,如果A是B的友元,B是C的友元,但A不是C的友元
cpp 复制代码
class Engine
{
	//car就是Engine的友元类
	friend class Car;

public:
	Engine(int eg = 0)
		:_eg(eg)
	{ }

private:
	int _eg;
};

class Car
{
public:
	void ShowCar(const Engine& eg)
	{
		cout << eg._eg << endl;
	}
};

五、内部类

1、内部类定义

如果一个类定义在另一个类的内部,这个内部类就叫做内部类

内部类是一个独立的类,跟定义在全局相比,只受外部类类域限制和访问限定符限制,因此外部类定义的对象不包含内部类。

2、内部类特点

  • 内部类默认是外部类的友元类
  • 内部类本质也是一种封装
cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{}

	//B默认就是A的友元类
	class B
	{
	public:
		void Print(const A& a)
		{
			cout << a._a << endl;
		}
	};

private:
	int _a = 1;

};

int main()
{
	//外部类定义的对象中不包含内部类
	cout << sizeof(A) << endl;
	//A类中只有一个整型变量,大小为4

	A aa;
	A::B bb;
	bb.Print(aa);

	return 0;
}

六、匿名对象

1、匿名对象的定义

用类型定义出来的对象称为匿名对象,前面用类型 对象名定义出来的称为有名对象

2、匿名对象的特点

  • 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可
cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}


	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
	
};

int main()
{
	//有名对象
	A aa1;

	//匿名对象
	A();
	A(1);
	return 0;
}

我们通过代码段来理解

首先定义有名对象 aa1,调用构造函数

接着定义匿名对象 ,调用构造函数 ,继续向下执行,匿名对象声生命周期结束,调用当前匿名对象的析构函数

然后再定义一个匿名对象 ,调用构造函数 并将_a初始化为1,继续向下执行,匿名对象生命周期结束,调用析构函数

最终调用有名对象aa1析构函数

七、对象拷贝时编译器的优化

现代编译器会为了尽可能提高程序效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值过程中可以省略的拷贝

我们通过代码来观察

cpp 复制代码
#include <iostream>

using namespace std;

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}

	A& operator==(const A& a)
	{
		cout << "A& operator=(const A& a)" << endl;
		if (this != &a)
		{
			_a = a._a;
		}

		return *this;
	}
	
private:
	int _a;
};

void f1(A aa)
{}

int main()
{
	//传值传参
	//构造+拷贝构造 -> 直接构造
	A aa1;
	f1(1);
	return 0;
}

程序运行后,按正常情况,首先会构造出aa1,接着进入f1函数,先调用构造函数构造出临时对象,接着将1调用拷贝构造函数传给临时对象,然后将临时对象拷贝构造给aa,其次析构临时对象,出函数f1作用域后析构aa,最终析构aa1

按照上述逻辑,不止会打印两个构造两个析构;而现在,只有两个构造函数,两个析构函数

说明编译器对其进行了优化

首先将传值传参优化为直接构造;

构造临时对象并调用拷贝构造函数将1传给临时对象直接优化为直接构造aa并将其初始化为1

然后f1函数结束后就调用析构函数,清理aa

最终再次调用析构函数,清理aa1


下面我们来看传值返回是否会进行优化

首先要明白,传值返回会构造出临时对象,将结果拷贝构造给临时对象,最终返回临时对;

cpp 复制代码
A f2()
{
	A aa;
	return aa;
}

int main()
{
	//构造+拷贝构造返回
	f2();
	return 0;
}

正常情况应该会有两个构造函数,一个构造函数体内的aa,一个构造临时对象

两个析构函数,一个析构aa,一个析构临时对象

一个拷贝构造函数,将aa拷贝构造给临时对象

但最终却只有一个构造函数,一个析构函数;

说明编译器直接优化为构造一个临时对象,省略了aa,并最终调用析构函数


如果觉得有帮助,可以关注Github项目持续更新