【c++】类和对象(三)运算符重载、赋值运算符重载、const成员函数、取地址运算符重载

小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

书接上文【c++】类和对象(二)构造函数、析构函数、拷贝构造函数详情请点击<—
本文由小编为大家介绍运算符重载、赋值运算符重载、const成员函数、取地址运算符重载


一、运算符重载

1. 概念讲解

c++为了增强代码的可读性增加了运算符重载,运算符重载是一种具有特殊函数名的函数,其也具有返回值类型,函数名称以及参数列表,其中返回值类型和参数列表和普通函数很相似

  1. 为什么小编讲它的函数名特殊呢?因为它的函数名全都是由operator加需要重载的运算符符号构成
  2. 它的函数原型为:返回值类型加operator加需要重载的运算符符号加参数列表
  1. 运算符重载中的参数列表中的参数可以包含内置类型参数,但是必须至少有一个类类型参数
  2. 以下五个运算符不能重载:第一个点星 .*第二个域作用访问限定符 ::第三个sizeof运算符 sizeof 第四个三目运算符?:第五个用于访问类成员点运算符 .

2. 运算符重载定义在类外

  1. 小编将使用一个简单的日期类在类外重载一个简单的运算符<,进而可以让我们的自定义类型日期类的对象可以像内置类型一样可以直接使用<进行比较大小,进而提高我们代码的可读性
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;
};

bool operator<(const Date& d1, const Date& d2)
{
    
    
	if (d1._year < d2._year)
	{
    
    
		return true;
	}
	else if (d1._year == d2._year && d1._month < d2._month)
	{
    
    
		return true;
	}
	else if (d1._year == d2._year && d1._month == d2._month && d1._day < d2._day)
	{
    
    
		return true;
	}

	return false;
}

int main()
{
    
    
	Date d1(2025, 4, 1);
	Date d2(2025, 4, 2);

	cout << (d1 < d2) << endl;
	cout << (operator<(d1,d2)) << endl;


	return 0;
}
  1. 由于在类外进行比较类类型对象d1和d2需要进行比较类类型对象的成员变量,而在类外面又无法进行访问类类型对象的成员变量,所以这里小编先将日期类中的private注释掉,以便可以在类外访问到成员变量
  2. 那么我们使用运算符重载的命名规则命名我们需要重载的<函数,即为operator<即可,采用引用接收类类型对象,并且我们只是简单的访问比较类类型对象的成员变量,不需要对成员变量进行修改,所以我们将参数定义为const类型进行引用接收,比较结果无非就是true或false,那么采用bool值进行返回即可
  3. 接下来就是小于运算符进行比较的主逻辑,年小则小,年相等月小则小,年相等月相等日小则小,这些情况都符合小于的情况返回true,采用 if — else if 语句进行判断即可,其余情况或则等于或者大于,此时不符合我们预期的小于,那么都统一返回false即可
  4. 那么进行运算符重载了之后可以直接使用<进行比较日期类类型对象d1和d2的大小,这种方式的代码可读性强便于理解,同时我们也可以显示调用operator<函数进行比较大小相对来讲可读性差一点
    在这里插入图片描述

思考这种方式的运算符重载固然可以,但是却破坏了日期类的封装性,将成员变量变为公有让类外也可以进行访问了,这样的做法不好,那么怎么样才能优化呢?

  1. 将小于运算符重载定义在日期类里面,这种方式就很香,不破坏类的封装性,小编将讲解这种方式
  2. 采用友元类,这种方式一定程度上也破坏了封装性
  3. 也可以采用在类的成员函数中定义GetYear(),GetMonth(),GetDay()用于获取类类型对象的成员变量,通常java喜欢这种写法,小编这里采用第一条将小于运算符重载定义在日期类里面进行讲解

3. 运算符重载定义在类里面

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

	bool operator<(const Date& d)
	{
    
    
		if (_year < d._year)
		{
    
    
			return true;
		}
		else if (_year == d._year && _month < d._month)
		{
    
    
			return true;
		}
		else if (_year == d._year && _month == d._month && _day < d._day)
		{
    
    
			return true;
		}

		return false;
	}

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

int main()
{
    
    
	Date d1(2025, 4, 1);
	Date d2(2025, 4, 2);

	cout << (d1.operator<(d2)) << endl;

	return 0;
}
  1. 这种定义方式将运算符重载定义在了日期类里面,那么要进行调用也就是需要借助隐藏的this指针,那么由于this指针是由编译器进行传入,那么我们成员函数的参数列表要比我们实际的操作数要少一个,即成员函数原型就变为了 bool operator(const Date& d)
  2. 以下方调用为例,1. 采用 d1<d2 进行调用,实际上被编译器转换为了 operator<(&d1,d2)进行了调用,2. 采用 d1.operator<(d2) 进行调用,实际上也被编译器转换为了 operator<(&d1,d2)进行了调用两者调用的本质也都是相同的,观察效果相同,两者相比,前一种方式的可读性强,注意:前一种方式的左操作数必须为类类型对象

在这里插入图片描述
观察反汇编,两种写法的反汇编指令完全相同,调用函数地址也完全相同
在这里插入图片描述

二、赋值运算符重载

1. 概念介绍

赋值运算符重载是6大默认成员函数,所以当我们要显示写赋值运算符重载的时候,必须将赋值运算符重载写在类里面或者声明在类里面实现在类外面,如果声明和实现都在外面,那么当我们想调用赋值运算符重载的时候,编译器会先在类类型的成员函数中去找,此时的声明和实现都在外面,必然会找不到,那么此时编译器会生成默认的赋值运算符重载,此时编译器会看向类外面,那么此时声明和实现写在类外面的赋值运算符重载就会跟编译器默认生成的赋值运算符重载冲突,所以必须将赋值运算符重载写在类里面或者声明在类里面实现在类外面
赋值运算符重载格式

  1. 函数参数为const加类类型加引用&加形参,这种方式可以减少拷贝提高效率
  2. 返回值参数为类类型加引用&,因为出了函数this指针指向的类类型对象还在所以可以使用引用,这种方式可以减少拷贝提高效率,并且还支持连续赋值
  3. 当进行赋值的时候,如果是自己给自己赋值,那么没有必要,这时候使用this指针(this指针就是地址)和形参的地址判断是否相等即可,如果不想等才进行赋值
    this指针是地址,指向的是类类型对象,由于要进行连续赋值,所以我们要返回类类型对象才可以进行连续赋值,即返回this指针的解引用*this
class Date
{
    
    
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}
	
	Date& operator=(const Date& d)
	{
    
    
		if (this != &d)
		{
    
    
			_year = d._year;
			_month = d._month;
			_day = d._month;
		}

		return *this;
	}

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


int main()
{
    
    
	Date d1(2025, 4, 1);
	Date d2;
	Date d3;

	d3 = d2 = d1;

	return 0;
}

调试观察,此时调用赋值运算符重载进行d1对d2和d3的连续赋值成功
在这里插入图片描述

  1. 当我们没有显示实现赋值运算符重载,编译器会为我们生成一个默认赋值运算符重载,此时这个默认赋值运算符重载会将类类型对象的内置成员变量按照内存存储的字节序进行赋值,对于自定义类型的成员变量会调用它类类型对应的赋值运算符重载或编译器自动生成的的赋值运算符重载完成赋值

这里的过程会涉及深浅拷贝的问题,该问题与拷贝构造函数极为类似,小编就不展开讲解了,拷贝构造讲解详情请点击<—

  1. 所以类类型对象中的成员变量需要进行动态申请资源的要调用赋值运算符重载时不能使用系统生成的默认赋值运算符重载,而是我们要自己实现赋值运算符重载
  2. 不需要实现的情况1:需要进行动态资源申请的全是自定义类型成员变量且自定义类型成员变量对应的类中显示实现了可以完成动态资源申请的赋值运算符重载
  3. 不需要实现的情况2:类类型对象中的成员变量全都是内置类型,这时候只需要系统生成的默认赋值运算符重载就可以完成按照内存存储的字节序进行的赋值工作

2. 容易混淆的拷贝构造和赋值构造

观察下面代码,透过现象看本质,请读者友友们思考,你认为类类型对象d4这个地方编译器调用的是什么,类类型对象d5这个地方编译器调用的又是什么?

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

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

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

		return *this;
	}

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


int main()
{
    
    
	Date d1(2025, 4, 1);

	Date d4 = d1;
	Date d5(d1);

	return 0;
}
  1. 毫无疑问类类型对象d5必然是调用的拷贝构造
  2. 小编猜测以下读者友友的内心活动,嘶~,这类类型对象d4这个地方的调用方式怎么这么像赋值运算符重载的调用方式,拿应该就是赋值运算符重载了吧
  3. 实则不然,类类型对象d4这个地方的调用的是拷贝构造函数仔细思考,我们内置类型变量的赋值是两个已存在的变量之间进行的赋值,前提是变量已存在,那么类比以下这里的赋值运算符重载进行赋值的时候应该也是两个已存在的类类型对象进行的赋值,那么这里的类类型对象还没进行初始化吧,存在就更谈不上了,所以一定不是调用的赋值运算符重载
  4. 那么抛开等号=,我们想一下思想,类类型对象d1是已经存在的,类类型对象d4是即将要被创建初始化的,那么这里不就正好和拷贝构造函数作用一样,使用一个已存在的类类型对象d1创建初始化一个新对象d4,所以这里类类型对象d4这个地方调用的是拷贝运算符重载
  5. 下面请观察类类型对象d4和d5的反汇编,在指令下,一切痕迹都将无所遁形
    在这里插入图片描述
  6. 所以类类型对象d4这个地方和类类型对象d5这个地方编译器调用的都是拷贝构造函数

三、前置++和后置++的运算符重载

  1. 当我们使用运算符重载的时候,难免会碰到要重载运算符前置++和后置++,都有++,那么应该如何区分呢?
  2. 规定关于前置++函数名使用operator++进行运算符重载
  3. 后置++函数名同样使用operator++进行运算符重载,为了和前置++进行区分,后置++的参数列表额外加入int与前置++构成函数重载
  4. 同理前置- -和后置- -重载也是相同的道理
	返回值类型 operator++();
	返回值类型 operator++(int);
	返回值类型 operator--();
	返回值类型 operator--(int);

四、const成员函数

  1. 被const修饰的成员函数称为const成员函数,const修饰类的成员函数,实际上修饰的是类的成员函数的隐形的this指针指向的内容即*this,在该成员函数中不能对类的成员变量进行修改,例如小于<运算符重载的作用仅仅是访问类类型对象的的成员变量并进行比较,不进行修改其成员变量,那么为了避免类类型对象的成员变量被修改我们要加上const进行修饰该小于<运算符重载成员函数
  2. 同时使用const修饰的成员函数除了可以被普通对象调用(权限的缩小)还可以被const修饰的对象进行调用(权限的平移),即d1和d2都可以调用小于<运算符重载
  3. 当我们实现的类的成员函数要对类的成员变量进行修改的时候不能使用const对该成员函数进行修饰,这样会造成无法对类的成员变量进行修改
	bool operator<(const Date& d) const;
	Date d1(2025,4,1);
	const Date d2(2025,4,2);
	d1<d2;

五、取地址及const取地址运算符重载

  1. 取地址运算符重载和const取地址运算符重载是6大默认成员函数之一,那么就是我们不显示写,编译器会生成默认的取地址运算符重载和const取地址运算符重载
  2. 同时还应该注意由于取地址运算符重载和const取地址运算符重载是6大默认成员函数之一,所以必须将赋值运算符重载写在类里面或者声明在类里面实现在类外面
  3. 针对当我们没有显示写其它4个默认成员函数编译器生成的默认成员函数实现的功能可能不符合我们的预期,所以对于其它的4个默认成员函数我们大部分情况都要自己实现
  4. 可是对于取地址运算符重载和const取地址运算符重载的实现却很简单,编译器知道如何实现,就是将存储类类型对象的那块空间的第一个字节的地址取出即可,所以通常对于我们都不写取地址运算符重载和const取地址运算符重载,而是使用编译器生成的默认取地址运算符重载和const取地址运算符重载

如下小编进行测试一下编译器默认生成的默认取地址运算符重载和const取地址运算符重载

class Date
{
    
    
public:

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

int main()
{
    
    
	Date d1;
	const Date d2;

	cout << &d1 << endl;
	cout << &d2 << endl;


	return 0;
}

可以正常取出地址
在这里插入图片描述

进行测试一下我们显示定义的的取地址运算符重载和const取地址运算符重载

class Date
{
    
    
public:
	Date* operator& ()
	{
    
    
		return this;
	}
		
	const Date* operator& () const
	{
    
    
		return this;
	}

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

int main()
{
    
    
	Date d1;
	const Date d2;

	cout << &d1 << endl;
	cout << &d2 << endl;

	return 0;
}

可以正常取出地址
在这里插入图片描述

  1. 那么对于取地址运算符重载和const取地址运算符重载的应用场景就只有一个,就是我们不想让外部用户取得类类型对象的地址,我们显示写取地址运算符重载和const取地址运算符重载时,编译器就不会生成默认的取地址运算符重载和const取地址运算符重载,而是会去调用我们显示写的取地址运算符重载和const取地址运算符重载,这时候我们将其返回的地址由this指针换为nullptr,这样外部用户在调用取地址运算符重载和const取地址运算符重载的时候,拿到的就只能是无效的空nullptr地址
class Date
{
    
    
public:
	Date* operator& ()
	{
    
    
		return nullptr;
	}
		
	const Date* operator& () const
	{
    
    
		return nullptr;
	}

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

int main()
{
    
    
	Date d1;
	const Date d2;

	cout << &d1 << endl;
	cout << &d2 << endl;

	return 0;
}

如图外部用户调用取地址运算符重载和const取地址运算符重载的时候,拿到的是无效的空nullptr地址
在这里插入图片描述


总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

猜你喜欢

转载自blog.csdn.net/2301_80751958/article/details/146916308
今日推荐