Bootstrap

C++类和对象(3)

前言:

以后的文章在目录中会使用红色的来标注难理解的小标题

1赋值运算符重载

先来介绍运算符重载

1.1运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名函数,也具有其 返回值类型函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。(比如赋值运算符重载的函数名字为:operator=)

函数原型:返回值类型 operator操作符(参数列表)

1.1.1运算符重载规则(易遗忘)

1.不能通过连接其他符号来创建新的操作符:比如operator@

2.重载操作符必须有一个类类型参数

3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义

4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

5..* :: sizeof ?: . 注意以上5个运算符不能重载。

1.1.2运算符重载的使用场景(例子)

// 全局的operator==
 class Date
 { 
public:
 Date(int year = 1900, int month = 1, int day = 1)
    {
 _year = year;
 _month = month;
 _day = day;
    }    
//private:
 int _year;
 int _month;
 int _day;
 };
 // 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
bool operator==(const Date& d1, const Date& d2)
 {
 return d1._year == d2._year
 && d1._month == d2._month
 && d1._day == d2._day;
 }

可以有以下的解决方案:

1.写一些获取成员变量的成员函数如:GetYear(),GetMonth()

2.声明运算符重载函数为Date的友元函数(之后介绍友元函数)

3.直接将运算符重载写成成员函数(重点介绍)

如果是第一种写法,那应该是这样子的

代码:

运行结果:

其实这种方式也不错,但你还要写三个函数,所以在这我想重点讲一下把运算符重载写成成员函数的写法!!

注:若存在多个类的运算符重载,比如class A的operator==和class B的operator==,并不会引起冲突,因为函数参数的类型就不一样,这两个operator==构成函数重载!!

第二种写法:

可以直接在类内声明友元函数,声明位置任意,不受访问限定符限制,但得在类域内,友元函数可以访问对象的成员变量,但这样写一定程度上破坏了封装性,更多关于友元函数的内容以后再讲。

第三种写法:

class Date
{

public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& d)const//在函数后加const修饰this指针意味着this指针不能被修改
	{                                  //原来的this指针类型为const Date* this
	return _year == d._year            //加const后变为const Date* const this
		&& _month == d._month
		&& _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2025, 3, 8);
	Date d2(2025, 3, 9);
	bool ret = d1 == d2;
	cout << ret << endl;
	return 0;
}

由于我们要访问对象的成员变量,最直接的方法就是将这个函数写成成员函数,但要注意,写成成员函数后,由于非静态成员函数存在一个隐含的参数(this指针),所以相较于前面两种写法要少一个参数

.*的应用场景(易遗忘)

第五点规则内的五个运算符分别为:1.*    2::    3sizeof     4?:   5. 这五个操作符,后四个还比较常见,那第一个情景比较少见,这里举个例子

1.2赋值运算符重载格式

参数类型:const T&,传递引用可以提高传参效率

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值

返回*this :要复合连续赋值的含义

1.参数类型的选择

对比下下列两个类的operator=

class A
{
public:
	A()//构造
	{
		cout << "A()" << endl;
	}
	A(const A& a)//拷贝构造
	{
		cout << "A(const A& a)" << endl;
	}
	void operator=(A a)
	{
		//
	}
private:
	int _a;
};
class B
{
public:
	B()//构造
	{
		cout << "B()" << endl;
	}
	B(const B& b)//拷贝构造
	{
		cout << "B(const B& b)" << endl;
	}
	void operator=(B& b)//
	{
		//
	}
private:
	int _b;
};

会发现如果operator=的参数类型是引用可以减少拷贝构造的调用,原因是形参是实参的一份临时拷贝,如果不写引用则要拷贝一份实参给形参,调用一次拷贝构造,返回值也是如此

那么一个标准的operator=的写法应该是这样的

1.3赋值运算符的特殊性

1.赋值运算符只能重载成类的成员函数不能重载成全局函数

因为赋值运算符是六大默认成员函数,如果我们不在类内显示写出,而在全局定义,那么编译器会在类内生成一个operator=,导致同一个类的operator=重定义,编译失败。

2.用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝

注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。

所以对于Date这种全是内置类型的类,我们可以不写赋值重载,编译器自己生成的即可完成任务,但如果是自己申请内存的类,比如栈和链表,那么默认生成的operator=无法胜任赋值任务,会导致对同一空间的两次释放运行崩溃

那我们创建一个简单的栈

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};

可以看到运行崩溃了,原理图如下

由于他是值拷贝,所以s2=s1的时候,s2的_array会指向s1的_array

那么函数结束的时候就会对同一块空间进行两次析构,因为他们是自己申请空间的自定义类型,在析构时需要对自己申请的内存进行释放,如果不释放就会引发内存泄漏

所以如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现

1.4前置++和后置++重载(前置--和后置--)

由于这两个运算符都是单操作数,理论上是都不需要传值就可以直接使用,因为存在this指针,但如果都写无参的,那么便无法区分到底调用哪个函数,也没有什么特别好的办法来解决这个问题,所以C++规定operator()为前置++operator(int)为后置++,没有什么原因,只是当时的设计问题,记住就好

2.const成员函数

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

3..取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容!

;