类和对象(下)

发布于:2025-07-31 ⋅ 阅读:(16) ⋅ 点赞:(0)

1.再探构造函数

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

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

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

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

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

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

初始化列表总结:

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

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

#define _CRT_SECURE_NO_WARNINGS

#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(1)
	{}
	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 i = 0;
	Date d1(i);
	d1.Print();
	return 0;
}


#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date()
		:_month(2)
	{
		cout << "Date()" << endl;
	}

	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// 注意这⾥不是初始化,这⾥给的是缺省值,这个缺省值是给初始化列表的
	// 如果初始化列表没有显⽰初始化,默认就会⽤这个缺省值初始化

	int _year = 1;
	int _month = 1;
	int _day;
	//_day没有初始化列表显示初始化,在声明位置也没有缺省值,可能初始化为随机数,或默认数0,主要看编译器
	Time _t = 1;
	const int _n = 1;
	int* _ptr = (int*)malloc(12);
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}



#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};
int main()
{
	A aa(1);
	aa.Print();
}

初始化列表初始化顺序按声明顺序,和初始化顺序无关
初始化列表初始化顺序最好和声明顺序一致


2.类型转换

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

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

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


#include<iostream>
using namespace std;

class A
{
public:

	//构造函数前面加explicit就不再支持隐式类型转换
	//explicit A(int a1)
	A(int a1)
		:_a1(a1)
	{}

	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 B
{
public:
	B(const A& a)
		:_b(a.Get())
	{}
private:
	int _b = 0;
};

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

	const A& aa2 = 1;

	//C++11以后才支持多参数转化
	A aa3 = { 2,2 };

	//aa3隐式类型转换为b对象
	//原理跟上面类似
	B b = aa3;
	const B& rb = aa3;

	return 0;
}

3.static成员

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

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

用static修饰的成员变量,称之为静态成员变量,静态成员变量没有this指针。

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

非静态的成员函数,可以任意的访问静态成员变量和静态成员函数。

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

静态成员也是类的成员,受public\protected\private访问限定符的限制。

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

//实现一个类,计算程序中创建了多少个类对象
#include<iostream>
using namespace std;

class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& t)
	{
		++_scount;
	}

	~A()
	{
		--_scount;
	}
	static int GetACount()
	{
		return _scount;
	}
private:
	//类里面声明
	static int _scount;
};

//类外面初始化
int A::_scount = 0;

int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;

	return 0;
}

4.友元

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

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

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

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

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

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

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

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

//友元函数

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

class A 
{
	//友元声明
	friend void func(const A& aa, const B& bb);

private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
	//友元声明
	friend void func(const A& aa, const B& bb);
	
private:
	int _b1 = 3;
	int _b2 = 4;
};

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

//友元函数

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

class A
{
	//友元声明
	friend class B;
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.func2(aa);
	return 0;
}

5.内部类

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

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

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

#include<iostream>
using namespace std;

class A
{
private:
	static int _k;
	int _h = 1;
public:
	class B//B默认就是A的友元
	{
	public:
		void func(const A& a)
		{
			cout << _k << endl;
			cout << a._h << endl;
		}

		int _b1;
	};
};

int A::_k = 1;

int main()
{
	cout << sizeof(A) << endl;
	A::B b;
	A aa;
	b.func(aa);
	return 0;
}

6.匿名对象

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

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

#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);

	A aa2(2);

	//匿名对象在这样场景下就很好用当然还有一些其他使用场景
	Solution().Sum_Solution(10);


	return 0;
}

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

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

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

linux下可以将下面代码拷贝到test.cpp文件,编译时用g++ test.cpp -fno-elide constructors的方式关闭构造相关的优化。


#include<iostream>
using namespace std;

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

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)";
		if (this != &aa)
		{
			_a1 = aa._a1;
		}

		return *this;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1=1;
};


void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
}



int main()
{

	//传值传参
	//构造+拷贝构造
	A aa1;
	f1(aa1);
	cout << endl;

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

	//一个表达式中,连续构造+拷贝构造->优化成一个构造
	f1(A(2));
	cout << endl;

	cout << "**********" << endl;

	//传值返回
	//不优化的情况下,传值返回,编译器会生成一个拷贝返回的对象的临时对象
	// 作为函数调用表达式的返回值

	//无忧化
	//一些编译器会优化得更厉害,将构造的局部对象和拷贝构造的临时对象
	// 优化为直接构造(VS2022 debug)

	f2();
	cout << endl;

	//返回时一个表达式中,连续拷贝构造+拷贝构造->优化成一个拷贝构造(VS 2019 debug)
	//一些编译器会优化得更厉害,进行跨行合并优化,
	// 将构造的局部对象aa和拷贝的临时对象和接收值对象aa2
	// 优化成一个直接构造(VS2022 debug)

	A aa2 = f2();
	cout << endl;

	//一个表达式中,开始构造,中间拷贝构造+赋值重载->无法优化(VS 2019 debug)
	aa1 = f2();
	cout << endl;
	return 0;
}

很高兴您能看到这里。

每个技术点都是通往卓越编程的垫脚石。也许今天的代码会让你皱眉调试到深夜,但请记住:每个报错都是计算机在认真教你新知识

欢迎交流讨论。


网站公告

今日签到

点亮在社区的每一天
去签到