【c++】类和对象(五)赋值运算符重载

Alt

🔥个人主页Quitecoder

🔥专栏c++笔记仓

Alt

朋友们大家好,本篇文章带大家认识赋值运算符重载,const成员,取地址及const取地址操作符重载等内容

目录

  • 1.赋值运算符重载
    • 1.1运算符重载
      • 1.1.1特性:
    • 1.2赋值运算符重载
    • 1.3 赋值运算符的其他性质
    • 1.4前置++和后置++重载
  • 2.const成员函数
  • 3.取地址及const取地址操作符重载

1.赋值运算符重载

1.1运算符重载

运算符重载是一种编程语言特性,它允许开发者为已有的运算符提供自定义的实现。这意味着你可以改变某些运算符在你自定义的类或数据类型上的行为。比如,你可以定义加号运算符(+)如何在你自定义的数据结构上进行运算

什么意思呢,我们来讲解:首先我们定义日期类Date,并实例化两个对象:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2018, 9, 26);
	Date d2(2024, 3, 29);
	return 0;
}

我们如何判断两个年份相等呢?

如果是常规方法,我们会写一个比较函数,来判断是否相同:

bool issame(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}
int main()
{
	Date d1(2018, 9, 26);
	Date d2(2024, 3, 29);
	cout << issame(d1, d2) << endl;
	return 0;
}

那如果我们想直接通过用d1==d2来判断是否相同呢?这里就用到了操作符重载

运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似,注意这里说的重载与我们的函数重载不是一个意思

函数名字为:关键字operator后面接需要重载的运算符符号

bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}
int main()
{
	Date d1(2018, 9, 26);
	Date d2(2024, 3, 29);
	cout << (d1==d2) << endl;
	return 0;
}


我们发现,直接进行判断时,调用了比较函数

但是这里是全局的定义的operator==,这里会发现运算符重载成全局的就需要成员变量是公有的,即我的成员不能是private私有的,那么封装性如何保证?

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date & d2)
	{
		return _year == d2._year
		&& _month == d2._month
			&& _day == d2._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
bool operator==(const Date& d2)
{
    return _year == d2._year
           && _month == d2._month
           && _day == d2._day;
}

这部分是Date类中==运算符的重载。这个重载让你可以使用==来比较两个Date对象是否相等,即它们的年、月、日是否都相同

关键点讲解

  • 参数operator==函数接受一个类型为const Date&的参数d2,它是比较操作的右侧操作数。左侧操作数是调用这个函数的对象,this指针指向的对象
  • const关键字:参数使用const修饰符和引用传递来保证效率和避免不必要的拷贝,同时确保不会修改传入的对象
  • 函数体:函数体中,通过比较两个Date对象的年、月、日字段来决定这两个对象是否相等。如果所有字段都相等,则返回true;否则,返回false

我们接着调用这个函数:

int main()
{
	Date d1(2018, 9, 26);
	Date d2(2024, 3, 29);
	cout << d1.operator==(d2) << endl;
	cout << (d1==d2) << endl;
	return 0;
}

注意
注意这里的顺序,d1在前与在后是不同的,如果我们写一个小于函数的运算符重载,顺序不同,意思刚好相反

我们有两种方式进行调用,这两种方式是相同的:

在上面的讲解之后,相信大家对运算符重载有了一定的了解,他就是允许自定义对象使用运算符它的返回值是根据运算符来决定的比如完成加减操作,我们就返回int类型,判断是否大于小于,就用bool类型

1.1.1特性:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数(自定义类型参数)
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现

1.2赋值运算符重载


我们知道,拷贝赋值有两种,拷贝构造和赋值重载,我们看拷贝构造:

Date d1(2018, 9, 26);
Date d2(d1);

那如果我们用赋值运算符重载呢?可以写成下面的形式:

d2=d1;

关键区别

  • 拷贝构造函数在对象创建时使用,用于初始化新对象。赋值运算符重载在对象已存在时使用,用于将一个对象的值赋给另一个对象
  • 目的:拷贝构造函数的目的是创建一个新的、状态相同的对象副本。赋值运算符的目的是改变一个已存在对象的状态,使其与另一个对象的状态相同
  • 拷贝构造函数通常接收一个对同类对象的常引用。赋值运算符重载通常返回对象的引用,并接收一个对同类对象的常引用作为参数

我们接下来讲解赋值运算符重载的具体实现来体现上面的特点:

能不能直接这么写呢?:

void operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

这个在单个赋值是可以的,那如果,我想像c语言一样同时实现多个变量的连续赋值的场景呢?

int b;
int c;
b=c=10;

那我们这个函数就无法满足要求了,我们该如何修改呢?

我们不妨探讨连续赋值的本质:

b=c=10;

这里执行步骤:

  • 10赋值给c,c=10这个表达式返回值为左操作数c
  • c再作为b=c的有操作数给b赋值,返回值为左操作数b

所以,我们的自定义类型也要符合这里的行为

所以,我们需要对函数进行修改:

第一次修改

Date operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}

返回左操作数,返回*this

我们这里用的是传值返回,意味着这里返回的不是*this,返回的是*this的拷贝,则需要调用拷贝构造函数:


所以我们需要再次修改:

第二次修改:

Date& operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}

我们返回引用

还有一个问题,如果自身给自身赋值呢?

d1=d1;

为什么自赋值不行?

自赋值在大多数情况下是可以工作的,但是在特定的情况下,如果没有正确处理,它可能会引起错误或意外的行为。考虑自赋值的主要原因是为了确保当对象赋值给自身时,程序仍然能够正确、安全地运行
特别是在类中涉及到动态内存管理时,不正确处理自赋值可能会导致问题。例如,假设一个类内部分配了动态内存,如果在赋值操作中首先释放了这块内存(预备重新分配),而源对象和目标对象实际上是同一个对象,那么这个操作实际上会破坏源对象的状态,导致未定义行为

我们还需要再次修改一次:

Date& operator=(const Date& d)
{
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;
}

我们这里判断条件是地址的比较,如果地址不相同说明不是同一个对象,可以赋值

1.3 赋值运算符的其他性质

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

我们首先得把成员类型设置为公有的,发现还是报错,

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了故赋值运算符重载只能是类的成员函数

如果我们不写赋值运算符重载,编译器是否会默认生成呢?


结果是会生成的

所以这里与我们拷贝构造等函数性质一致:

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?

答案是需要的,如遇到下面的动态内存管理

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;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2;
	s2 = s1;
	return 0;
}

  1. s1对象调用构造函数创建,在构造函数中,默认申请了10个元素的空间,然后存了4个元素1 2 3 4
  2. s2对象调用构造函数创建,在构造函数中,默认申请了10个元素的空间,没有存储元素
  3. 由于Stack没有显式实现赋值运算符重载,编译器会以浅拷贝的方式实现一份默认的赋值运算符重载即只要发现Stack的对象之间相互赋值,就会将一个对象中内容原封不动拷贝到另一个对象中
  4. s2 = s1;当s1给s2赋值时,编译器会将s1中内容原封不动拷贝到s2中,这样会导致两个问题:
    • s2原来的空间丢失了,存在内存泄漏
    • s1和s2共享同一份内存空间,最后销毁时会导致同一份内存空间释放两次而引起程序崩溃

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

1.4前置++和后置++重载

在C++中,前置++和后置++运算符都可以被重载,以提供用户定义类型(比如类)的自增功能。它们之间的主要区别在于参数和返回值,这影响了它们的使用和效率

前置++

前置++直接对对象进行自增操作,并返回自增后的对象引用。这意味着它在自增后立即返回对象的状态,使得操作可以立即反映在对象上

Date& operator++()
 {
 _day += 1;
 return *this;
 }

后置++

前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递

注意:后置++是先使用后+1因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1

Date operator++(int)
 {
 Date temp(*this);
 _day += 1;
 return temp;
 }

temp是临时对象,因此只能以值的方式返回,不能返回引用

2.const成员函数

假如我们现在定义一个const对象,想访问它的Print函数,我们发现是调用不了的:

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << "Print()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

这里权限进行放大了,接着,我们来介绍const成员函数

原来是const Date*,而我的this类型是Date*,意味着需要将this指针也改为const Date*,所以才有了下面的函数

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

class Date
{
public:
 Date(int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 void Print()
 {
 cout << "Print()" << endl;
 cout << "year:" << _year << endl;
 cout << "month:" << _month << endl;
 cout << "day:" << _day << endl << endl;
 }
 void Print() const
 {
 cout << "Print()const" << endl;
 cout << "year:" << _year << endl;
 cout << "month:" << _month << endl;
 cout << "day:" << _day << endl << endl;
 }
private:
 int _year; // 年
 int _month; // 月
 int _day; // 日
};
void Test()
{
 Date d1(2022,1,13);
 d1.Print();
 const Date d2(2022,1,13);
 d2.Print();
}

我们查看结果:

如果没有const修饰的函数呢,我Date类型的对象能否调用const成员函数呢?

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print() const
	{
		cout << "Print()const" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
private:
	int _year; 
	int _month; 
	int _day; 
};
void main()
{
	Date d1(2022, 1, 13);
	d1.Print();
}


可以的,这里是权限的缩小

请思考下面的几个问题:

  1. const对象可以调用非const成员函数吗? 不可以,权限放大
  2. 非const对象可以调用const成员函数吗? 可以,权限缩小
  3. const成员函数内可以调用其它的非const成员函数吗? 不可以,权限放大
  4. 非const成员函数内可以调用其它的const成员函数吗?可以,权限缩小

指针和引用才存在权限放大

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

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{

		_year = year;
		_month = month;
		_day = day;
	}
	Date* operator&()
	{
		return this;
	}
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

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


这里是默认成员函数,我们删去这两个函数照样可以取地址

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{

		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};


所以,我们没有必要深究这个东西究竟有什么用,我们只进行简单的语法了解即可

当然,如果你想让它普通对象定义后只能返回空值,你可以这么写:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{

		_year = year;
		_month = month;
		_day = day;
	}
	Date* operator&()
	{
		return nullptr;
	}
private:
	int _year; 
	int _month; 
	int _day; 
};

日常并没有这种需要

本节内容到此结束!!!感谢大家阅读!!

版权声明:本文为博主作者:QuiteCoder原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/arf_dog/article/details/137155189

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2024年4月22日
下一篇 2024年4月22日

相关推荐