04.类和对象(下)(初始化列表、static静态成员、友元friend[类外函数使用类私有成员]、内部类、匿名对象等)

目录

一.再探构造函数

1.初始化列表概念及使用方法

2.初始化列表约束条件

二.类型转换

三.static静态成员

四.友元(friend声明:突破类的访问限定符)

五.内部类

六.匿名对象

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


一.再探构造函数

1.初始化列表概念及使用方法

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

cpp 复制代码
class Date
{
public:
	Date()			//在()和{之间以(冒号)开头(逗号隔开)}
		: _year(2025)	//不一定非要换行,这样也可以:_year(2025),_month(2),_day(14)
		, _month(2)		//不过换行看的比较清晰,是个好习惯
		, _day(14)

	{}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// 注意这里是声明,不是初始化
	int _year;
	int _month;
	int _day;
};

2.初始化列表约束条件

1.每个成员变量在初始化列表中只能出现一次 ,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方 。(类似普通变量定义,int a = 0; a只能初始化一次

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

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

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

5.初始化列表中按照成员变量在类中声明顺序进行初始化 (因为开空间是按声明的顺序开的空间),跟成员在初始化列表出现的先后 顺序无关。建议声明顺序和初始化列表顺序保持一致。(示例4)

初始化列表总结(理解完上面5条再看):

无论是否显示写初始化列表,每个构造函数都有初始化列表;

无论是否在初始化列表显示初始化,每个成员变量都要走初始化列表初始化;

(1).在初始化列表初始化的成员

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

a.声明的地方有缺省值用缺省值

b.没有缺省值

x.内置类型,不确定,看编译器,大概率是随机值

y.自定义类型,调用默认构造函数,没有默认构造则编译报错

(3).引用、const、没有默认构造的自定义类型、必须在初始化列表初始化

示例1:初始化列表使用方法;第2条

cpp 复制代码
#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour)		//此处不是默认构造函数
		:_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(12)	//以下三种类型必须在此处,初始化列表中初始化,原因见函数体内
		, _ref(x)
		, _n(x)
	{
		//以下三种类型,在构造函数里初始化会报错
		//_t = 12;		//Time类型
		//_ref = x;		//int&引用类型
		//_n = x;		//const类型
		// error C2512: "Time": 没有合适的默认构造函数可用
		// error C2530 : "Date::_ref" : 必须初始化引用
		// error C2789 : "Date::_n" : 必须初始化常量限定类型的对象
	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	//声明
	int _year;
	int _month;
	int _day;
	Time _t;		// 没有默认构造
	int& _ref;		// 引用
	const int _n;	// const
};
int main()
{
	int x = 0;
	Date d1(x, 2025, 2, 14);	//对象定义
	d1.Print();
	return 0;
}

示例2:第3条,第4条

cpp 复制代码
#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour)    //此处不是默认构造函数
		:_hour(hour)
	{
		cout << "Time():" << _hour << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date()
		:_month(2)    //初始化列表
	{
		cout << "Date():" ;
	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// 注意这里不是初始化,这里给的是缺省值,这个缺省值是给初始化列表的
	// 如果初始化列表没有显示(即没明确写出来)初始化,默认就会用这个缺省值初始化
	int _year = 2025;	//初始化列表中没有显示的初始化,默认调用 _year = 1
	int _month = 1;	    //初始化列表中显示的初始化了,则调用初始化的值,2
	int _day;		    //初始化列表中没有显示的初始化,但_day也没有缺省值,则_day是否初始化随机,取决于编译器,我的编译器给_day初始化了,且初始化为0
	Time _t = 1;	    //初始化列表中没有显示的初始化,默认调用 _t = 1
	const int _n = 1;
	int* _ptr = (int*)malloc(12);
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

示例3:第4条最后一句(默认构造函数定义见03.类和对象(中)(构造函数-函数名与类名相同的成员函数、析构函数、拷贝构造函数、赋值运算符重载等定义及使用)-CSDN博客构造函数第6条和示例2、示例3)

示例4:(第5条)看初始化顺序是按private那里声明的顺序先_year,再_month,再_day进行的

提问:下面代码输出结果是什么?

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		: _a1(a)	//在这里_a1才会初始化,调用声明处的缺省值
		, _a2(_a1)	//先初始化_a2,此时_a1还没有初始化
		, _a3(_a4)
	{}
	void Print() {
		cout << _a1 << " " << _a2 << " " << _a3 << " " << _a4 << endl;
	}
private:
	int _a4 = 2;
	int _a3 = 2;
	int _a2 = 2;	//这里给的2是缺省值,在没有显示的初始化时才会调用此处的缺省值
	int _a1 = 2;
};
int main()
{
	A aa(1);
	aa.Print();
}

答案:1 随机数 2 2

详解见:问题1.mp4 · 过云雨/cplusplus - 码云 - 开源中国

二.类型转换

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

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

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

意义:两步合成(通过类型转换)一步

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	// 构造函数explicit就不再支持隐式类型转换
	// explicit A(int a1)
	A(int a1 = 0)
	{
		_a1 = a1;
	}
	//explicit A(int a1, int a2)
	A(int a1, int a2)
	{
		_a1 = a1;
		_a2 = a2;
	}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
	int Get() const
	{
		return _a1 + _a2;
	}
private:
	int _a1 = 1;
	int _a2 = 2;
};
class Stack
{
public:
	void Push(const A& aa)
	{
		//...
	}
private:
	A _arr[10];
	int _top = 10;
};

int main()
{
	A aa1 = 1;
	aa1.Print();
	// 隐式类型转换
	// 1构造一个A的临时对象,再用这个临时对象拷贝构造aa1	1(int类型)->构造A的临时对象->临时对象拷贝构造aa1
	// 编译器遇到连续构造+拷贝构造->优化为直接构造

	A aa2(2);			//类型A (类型int)
	A& raa1 = aa2;		//类型A的引用 = 类型A
	const A& raa2 = 2;	//类型A的const引用 = 类型int
	// A& raa2 = 2;	
	// 不能直接引用2,2为整型,A&是自定义类型引用,存在类型转换,类型转换会有一个临时变量,临时变量具有常性
	// 所以,要加const				2(int类型)->构造A的临时对象->临时对象引用为raa2(引用就是取别名)
	// 前两个正常步骤,可以通过类型转换合成下面一步const A& raa2 = 2;

	//类比上面的类型转换,临时变量具有常性
	int i = 1;
	const double& d = i;

	Stack st;
	A aa3(3);			//类型A (类型int)
	st.Push(aa3);		//类型A的引用 = 类型A			void Push(const A& aa)
	st.Push(4);			//类型A的const引用 = 类型int
	//	void Push(const A& aa)
	//  st.Push(aa3) 是const A& aa = aa3;	aa3->A的临时对象->临时对象引用为aa
	//  st.Push(4)   是const A& aa = 4;		  4->A的临时对象->临时对象引用为aa
	//  类比上面,前两个正常步骤,可以通过类型转换合成下面一步st.Push(4);
	//  void Push(const A& aa), 再次体会这里的const

	//....................................................................................
	// C++11之后才支持多参数转化
	A aa5 = { 1,1 };
	// aa3隐式类型转换为Stack对象
	// 原理跟上面类似
	st.Push(aa5);
	st.Push({ 2,2 });
	
	return 0;
}

三.static静态成员

1.static静态成员变量

(1)用static修饰的成员变量,称之为静态成员变量 ,静态成员变量一定要在类外进行初始化。

(2)静态成员变量为所有类对象共享,不属于某个具体的对象,不存放在对象中,存放在静态区。

(3)静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。

2.static静态成员函数

(1)用static修饰 的成员函数,称之为静态成员函数 ,静态成员函数没有this指针

(2)静态成员函数 中可以访问其他的静态成员,但是不能访问非静态的成员,因为没有this指针。

(3)非静态的成员函数可以访问任意的静态 成员变量 和静态成员函数

(4)突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。

(5)静态成员也是类的成员 ,受public、protected、private 访问限定符的限制

示例1:1的(2),2的(2),(3),(4),(5)

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& t)
	{
		++_scount;
	}
	~A()
	{
		--_scount;
	}
	static int GetACount()	//静态成员函数,没有this指针
	{
		return _scount;    //静态成员可以访问其他静态成员_scount,2的(2)
	}
	void func()            //非静态的成员函数,2的(3)
	{
		cout << "非静态访问静态:";
		cout << GetACount() << " " << _scount;    //非静态的成员函数可以访问任意静态成员
		cout << "  访问结束" << endl;
	}
private:
	// 类里面声明
	static int _scount;
};
// 类外面初始化
int A::_scount = 0;
int main()
{
	A a1, a2;						//实例化了两个对象
	cout << A::GetACount() << endl;	
	//cout << A::_scount << endl;    2的(5)
	// 编译报错:error C2248: "A::_scount": 无法访问 private 成员(在"A"类中声明)
	// 编写GetACount()函数来访问_scount

	//静态成员函数可以通过类名访问		也可一通过任何一个对象访问
	cout << A::GetACount() << endl;	
	cout << a1.GetACount() << endl;	    //1的(2)和2的(4)
	cout << a2.GetACount() << endl;	

	//A::func()	非静态成员函数不能通过类名访问
	a1.func();
	return 0;
}

示例2:实现一个类,计算程序中创建出了多少个类对象?

cpp 复制代码
// 实现一个类,计算程序中创建出了多少个类对象?
#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_scount;    //默认构造函数调用,_scount++
	}
	A(const A& t)
	{
		++_scount;    //拷贝构造函数调用,_scount++
	}
	~A()
	{
		--_scount;    //析构函数调用,对象销毁,_scount--
	}
	static int GetACount()	//静态成员函数,没有this指针
	{
		return _scount;
	}
private:
	// 类里面声明
	static int _scount;
};
// 类外面初始化
int A::_scount = 0;
int main()
{
	// 用_scount来观察,代码中对象存在的个数
	cout << A::GetACount() << endl;	//输出 0,此时存在 0 个对象
	A a1, a2;						//实例化了两个对象

	cout << A::GetACount() << endl;	//输出 2,此时存在 2 个对象
	{
		A a3(a1);					//实例化第三个对象
		cout << a1.GetACount() << endl;	//输出 3 ,此时存在 3 个对象
	}
	cout << a1.GetACount() << endl;	//第三个对象已经销毁了,这里输出 2,目前还有 2 个对象

	return 0;
}

四.友元(friend声明:突破类的访问限定符)

友元提供了一种突破类访问限定符 封装的方式,友元分为:友元函数友元类用法:在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。

1.友元函数

(1)外部友元 函数可访问类私有和保护成员 ,友元函数仅仅是一种声明,他不是 类的成员函数

(2)友元函数可以在类定义的任何地方声明,不受类访问限定符限制

(3)一个函数可以是多个类的友元函数。

2.友元类

(1)友元类中的成员函数都是 另一个类的友元函数,都可以访问另一个类中的私有和保护成员。

(2)友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。

(3)友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。

友元有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

示例1:1的(2),(3)

cpp 复制代码
#include<iostream>
using namespace std;
// 前置声明,否则A的友元函数声明编译器不认识B
class B;

class A
{
	// 友元声明
	friend void func(const A& aa, const B& bb);	//指这里的B类型
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
private:
	// 友元声明
	friend void func(const A& aa, const B& bb);
	int _b1 = 3;
	int _b2 = 4;
};
//func函数是A和B类的友元。一个函数可以是多个类的友元函数。
void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}
int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}

示例2:2的(1),(2)

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
	// 友元声明
	friend class B;		//声明B为A的友元类,则B类中可以访问A中的所有私有和保护成员
	//void func1(const B& bb)	//友元类的关系是单向的,A不是B的友元类,则A不能访问B类中的私有和保护成员
	//{
	//	cout << bb._b1 << endl;
	//}
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
public:
	void func1(const A& aa)
	{
		cout << aa._a1 << endl;
		cout << _b1 << endl;
	}
	void func2(const A& aa)
	{
		cout << aa._a2 << endl;
		cout << _b2 << endl;
	}
private:
	int _b1 = 3;
	int _b2 = 4;
};
int main()
{
	A aa;
	B bb;
	bb.func1(aa);
	bb.func1(aa);
	return 0;
}

五.内部类

如果一个类定义在另一个类的内部,这个内部的类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

内部类默认是外部类的友元类。

内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。

示例:

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
private:
	static int _k;
	int _h = 1;
public:
	class B // B默认就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << _k << endl;		//	B是A的友元类,B可以直接访问A的私有
			cout << a._h << endl;	//
		}
	private:
		int _b = 1;
	};
};

int A::_k = 1;
int main()
{
	cout << sizeof(A) << endl;	
	//这里输出4,因为static变量存放在静态区,不存放在对象里面,所以只有_h占内存。见三.1.(2)

	A::B b;
	A aa;
	b.foo(aa);
	return 0;
}

六.匿名对象

用 类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象。

匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。

示例:

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

class Solution {
public:
	int Sum_Solution(int n) {
		//...
		return n;
	}
};
int main()
{
	A aa1;	//有名对象

	// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
	//A aa1();

	// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
	// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
	A();	//匿名对象
	A(1);	//匿名对象


	// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
	Solution st;
	cout << st.Sum_Solution(10) << endl;

	cout << Solution().Sum_Solution(10) << endl;
	return 0;
}

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

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

如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化

示例1:第1段,构造+拷贝构造优化为直接构造。

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)	//构造函数,输出"A(int a)"
		:_a1(a)
	{
		cout << "构造A(int a)" << endl;
	}
	A(const A& aa)	//拷贝构造函数,输出"A(const A& aa)"
		:_a1(aa._a1)
	{
		cout << "拷贝构造A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)	//赋值运算符重载
	{
		cout << "赋值运算符重载A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}
	~A()	//析构函数
	{
		cout << "析构~A()" << endl;
	}
private:
	int _a1 = 1;
};
void f1(A& aa)
{}
int main()
{
	A aa1 = 1;
	// 隐式类型转换
	// 1构造一个A的临时对象,再用这个临时对象拷贝构造aa1	1(int类型)->构造A的临时对象->临时对象拷贝构造aa1
	// 编译器遇到连续构造+拷贝构造->优化为直接构造	所以没有输出拷贝构造

	const A& aa2 = 2;	
	// 2构造一个A的临时对象,再用这个临时对象引用为aa2 	2(int类型)->构造A的临时对象->引用临时对象为aa2
	// 这里只有一个构造

	return 0;
}

可见,aa1和aa2没有拷贝构造,编译器将构造+拷贝构造->优化为了直接构造

示例2:第1段,编译器减少传参中的拷贝

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)	//构造函数,输出"A(int a)"
		:_a1(a)
	{
		cout << "构造A(int a)" << endl;
	}
	A(const A& aa)	//拷贝构造函数,输出"A(const A& aa)"
		:_a1(aa._a1)
	{
		cout << "拷贝构造A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)	//赋值运算符重载
	{
		cout << "赋值运算符重载A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}
	~A()	//析构函数
	{
		cout << "析构~A()" << endl;
	}
private:
	int _a1 = 1;
};
void f1(A aa)
{}

int main()
{
	A aa1(1);	//这里并没有连续构造
	f1(aa1);	//则f1这个函数会执行拷贝构造函数,如果要减少拷贝构造,将void f1(A aa)改为void f1(A& aa),即使用引用传参

	
	//匿名对象	优化		一个表达式中,连续构造 + 拷贝构造->优化为一个构造
	f1(A(1));
	cout << endl;

	//隐式类型转换	优化	 连续构造+拷贝构造->优化为直接构造
	f1(1);
	cout << endl;

	return 0;
}

aa1执行了构造,然后f1函数中将aa1拷贝构造给了aa。要减少拷贝构造,将void f1(A aa)改为void f1(A& aa),即使用引用传参,则减少掉了"f1函数中将aa1拷贝构造给了aa"这一步。

总结:引用传参可以减少拷贝(引用就是取别名)

补: 一个表达式中,连续拷贝构造+赋值重载->无法优化

相关推荐
勘察加熊人30 分钟前
wpf+c#路径迷宫鼠标绘制
开发语言·c#·wpf
小黄人软件2 小时前
C# ini文件全自动界面配置:打开界面时读ini配置到界面各控件,界面上的控件根据ini文件内容自动生成,点保存时把界面各控件的值写到ini里。
开发语言·c#
二进制人工智能2 小时前
【QT5 网络编程示例】TCP 通信
网络·c++·qt·tcp/ip
Android洋芋4 小时前
C语言深度解析:从零到系统级开发的完整指南
c语言·开发语言·stm32·条件语句·循环语句·结构体与联合体·指针基础
bjxiaxueliang4 小时前
一文详解QT环境搭建:Windows使用CLion配置QT开发环境
开发语言·windows·qt
莫有杯子的龙潭峡谷5 小时前
3.31 代码随想录第三十一天打卡
c++·算法
Run_Teenage5 小时前
C语言 【初始指针】【指针一】
c语言·开发语言
苹果.Python.八宝粥5 小时前
Python第七章02:文件读取的练习
开发语言·python
AaronZZH5 小时前
【进阶】vscode 中使用 cmake 编译调试 C++ 工程
c++·ide·vscode
J不A秃V头A5 小时前
Redis批量操作详解
开发语言·redis