C++奇迹之旅:深入理解赋值运算符重载

请添加图片描述

文章目录

  • 📝赋值运算符重载
  • 🌠 运算符重载
    • 🌉特性
  • 🌠 赋值运算符重载
  • 🌠传值返回:
  • 🌠传引用赋值:
    • 🌉两种返回选择
    • 🌉赋值运算符只能重载成类的成员函数不能重载成全局函数
  • 🚩总结


📝赋值运算符重载

🌠 运算符重载

运算符重载是C++中的一个重要特性,他允许我们为自定义的类型定义自己的运算符行为。通过运算符重载,我们可以使用与内置数据类型相同的语法来操作自定义类型,从而提高代码的可读性和可维护性

还是我们熟悉的日期函数:

class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:// 给缺省值int _year = 1;int _month = 1;int _day = 1;
};

然后我们定义两个日期对象d1和d2:

int main()
{Date d1(2024, 2, 17);Date d2(2024, 6, 27);return 0;
}

当你想要比较两个对象d1和d2的数据是否一样,这是通常的比较方法:
创建一个专门的比较函数来比较两个Date对象是否相同。

class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}bool isSame(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 2, 17);Date d2(2024, 6, 27);if (d1.isSame(d2)){cout << "d1 and d2 are the same date" << endl;}else{cout << "d1 and d2 are different dates" << endl;}return 0;
}

很明显的可以看出这是个比较函数,能不能直接通过像内置类型那样d1==d2来比较相同呀,因此运算符重载就来了:
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通函数类似

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

class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}bool isSame(const Date& d){return _year == d._year&& _month == d._month&& _day == d._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;
}
int main()
{Date d1(2024, 2, 17);Date d2(2024, 6, 27);//d1.isSame(d2)if (d1 == d2){cout << "d1 and d2 are the same date" << endl;}else{cout << "d1 and d2 are different dates" << endl;}return 0;
}

这样使用运算符重载是不是直接可以使用d1==d2,方便了,但是这里有个注意点:此时bool operator==(const Date& d1,const Date& d2)这个函数我是写在全局变量中,因此private也去掉,才能够访问。

这样一来,安全性降低了,可读性升高了,有点得不偿失,运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
在这里插入图片描述
补充:两种调用方式直接写和显示调用是一样的:

int main()
{Date d1(2024, 2, 17);Date d2(2024, 6, 27);// 显式调用operator==(d1, d2);// 直接写,装换调用,编译会转换成operator==(d1, d2);d1 == d2;return 0;
}

两者的call指令是一样的:
在这里插入图片描述
bool operator==(const Date& d1, const Date& d2) 重载成全局,无法访问私有成员,解决办法有三种:

  1. 使用 getter 和 setter 函数的方案:
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int GetYear() { return _year; }int GetMonth() { return _month; }int GetDay() { return _day; }void SetYear(int year) { _year = year; }void SetMonth(int month) { _month = month; }void SetDay(int day) { _day = day; }bool operator==(const Date& d) const{return _year == d._year && _month == d._month && _day == d._day;}private:int _year;int _month;int _day;
};
  1. 使用友元函数的方案:
    • 这种方式可以直接访问私有成员变量,不需要额外的 getter 和 setter 函数。
    • 但是,将 operator== 函数声明为友元函数会破坏类的封装性,需要谨慎使用。
class Date
{friend bool operator==(const Date& d1, const Date& d2);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. 重载为成员函数的方案:
    • 这是最常见和推荐的方式。
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)// 这里需要注意的是,左操作数是this,指向调用函数的对象bool operator==(const Date& d) {return _year == d._year && _month == d._month && _day == d._day;}private:int _year;int _month;int _day;
};

这里我们本节主要学习第三种重载为成员函数的方案:bool operator==(const Date& d) 这里需要注意的是,左操作数是this,指向调用函数的对象。
这里的参数使用const修饰,确保传入的原对象不被修改

函数的调用方法:

int main()
{Date d1(2024, 2, 17);Date d2(2024, 6, 27);//显示调用d1.operator==(d2);//转换调用,等价与d1.operator==(d2);d1 == d2;cout << d1.operator==(d2) << endl;cout << (d1 == d2) << endl;//注意这里的()括号,因为符号优先级问题return 0;
}

在这里插入图片描述

对于自定义对象我们可以使用运算符,返回值是根据运算符来决定,加减返回int类型,判断大小,使用bool类型:一个类要重载哪些运算符是看需求,看重载有没有价值和意义

🌉特性

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
    藏的this
  5. .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出
    现。
    这里主要注意的是.*,看看这个代码:
class OB
{
public:void func(){cout << "void func" << endl;}
};typedef void(OB::*ptrFunc)();//成员函数指针类型int main()
{//函数指针//void(*ptr)();//成员函数要加&才能取到函数指针ptrFunc fp = &OB::func;//定义函数指针fp指向funcOB temp;//定义OB类对象temp(temp.*fp)();return 0;
}

首先这是普通函数指针的定义:

//函数指针
void(*ptr)();

其次是成员函数指针类型:

typedef void(OB::*ptrFunc)();//成员函数指针类型

在这个代码中,typedef void(OB::*ptrFunc)() 定义了一个新的类型 ptrFunc,它是一个指向 OB 类的成员函数的指针类型。

  1. void(OB::*)()

    • 这是一个函数指针类型,它指向一个返回值为 void 且没有参数的成员函数。
    • OB::* 表示这个函数指针是指向 OB 类的成员函数。
  2. typedef void(OB::*ptrFunc)();

    • 使用 typedef 关键字定义了一个新的类型 ptrFunc
    • ptrFunc 就是这个指向 OB 类成员函数的指针类型的别名。

这样定义之后,我们就可以使用 ptrFunc 这个类型来声明指向 OB 类成员函数的指针变量了。

//成员函数要加&才能取到函数指针ptrFunc fp = &OB::func;//定义函数指针fp指向func

main() 函数中,我们使用 &OB::func 获取了 OB 类的 func() 成员函数的地址,并将其赋值给 ptrFunc 类型的变量 fp

	OB temp;//定义OB类对象temp(temp.*fp)();

然后,我们创建了一个 OB 类的对象 temp。最后,使用 (temp.*fp)(); 语法调用了 temp 对象的 func() 成员函数。这里的 .* 运算符用于通过成员函数指针调用成员函数。

🌠 赋值运算符重载

在这里插入图片描述

上节我们学了拷贝构造来进行数据的复制:一个已经存在的对象,拷贝给另一个要创建初始化的对象

Date d1(2024, 4, 20);
// 拷贝构造
// 一个已经存在的对象,拷贝给另一个要创建初始化的对象
Date d2(d1);
Date d3 = d1;

当然那还有赋值拷贝/赋值运算符重载也可以进行复制:
一个已经存在的对象,拷贝赋值给另一个已经存在的对象

Date d1(2024, 4, 20);
d1 = d4;

在这里插入图片描述

这里是单个赋值,能不能连续像内置类型那样赋值?
	int i, j, k;i = j = k = 1;

连续赋值的本质是:从右向左开始,1赋值给k,k=1表达式返回值为左操作数k,接着j赋值给k,j=k表达式返回值为左操作数,再接着i就拿到了1,连续赋值完毕。

同理,自定义类型也是一样的,有两种方式传值和引用:

🌠传值返回:

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

由于每次返回的都是左操作数,我们需要this,但是这是传值返回,返回的是this的临时拷贝,不是这个函数里局部变量*this

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){cout << "Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}// d1 = d2 = d4;// ->d2 = d4// ->d1 = d2;Date operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;return *this;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 4, 14);// 拷贝构造// 一个已经存在的对象,拷贝给另一个要创建初始化的对象Date d2(d1);Date d3 = d1;Date d4(2024, 5, 1);// 赋值拷贝/赋值重载// 一个已经存在的对象,拷贝赋值给另一个已经存在的对象d1 = d4;d1 = d2 = d4;return 0;
}

当从右向左开始,d4赋值给d2d2=d4表达式返回值为左操作数d2,这里的d2*this,但是传值返回,会生成一个临时的拷贝,返回的是Date *this,此时此刻,如果你一步一步调试,他会跳转到Date的构造函数里Date(const Date& d),然后刷新private的值,然后进行第二步的连续赋值d2赋值给d1,和上面的步骤是一样的。

🌠传引用赋值:

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

这里按引用传递,无需再创造临时变量拷贝,再调用拷贝构造函数,当从右向左开始,d4赋值给d2d2=d4表达式返回值为左操作数d2,这里的d2*this,*this直接返回的是左操作数的d2的别名。

这里还需注意的一点是,我们可能会出现写错,避免不必要的操作:比如自赋值

d1=d1

处理自赋值问题主要有以下几个原因:

  1. 如果不检查自赋值的情况,当执行d1 = d1时,会进行不必要的赋值操作。这可能会造成性能的浪费,尤其是对于大型对象而言。
  2. 某些情况下,对象的成员变量可能依赖于其他成员变量的值。如果在赋值过程中,这些成员变量的值被覆盖,可能会导致对象处于不一致的状态,从而引发错误。

修改自赋值问题:使用地址来判断

Date& operator=(const Date& d)
{if (this != &d)//使用的是地址判断{_year = d._year;_month = d._month;_day = d._day;}return *this;
}

因此我们总结一下赋值运算符重载格式

  1. 参数类型:const T&,传递引用可以提高传参效率
  2. 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  3. 检测是否自己给自己赋值
  4. 返回*this :要复合连续赋值的含义

🌉两种返回选择

  1. 传值返回
Date func()
{Date d(2024, 4, 14);return d;
}int main()
{const Date& ref = func();return 0;
}

选择传值,还是传引用呢?

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){cout << "Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}// d1 = d2 = d4;// d2 = d4// d1 = d2// Date operator=(const Date& d)Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}~Date(){cout << "~Date()" << endl;_year = -1;_month = -1;_day = -1;}private:int _year;int _month;int _day;
};Date func()
{Date d(2024, 4, 14);return d;
}int main()
{const Date ref = func();return 0;
}

在这里插入图片描述

  1. 传引用返回
Date& func()
{Date d(2024, 4, 14);return d;
}int main()
{const Date& ref = func();return 0;
}

在这里插入图片描述

总结一下:返回对象是一个局部对象或者临时对象,出了当前func函数作用域,就析构销毁了,那么不能用引用返回用引用返回是存在风险的,因为引用对象在func函数栈帧已经销毁了
虽然引用返回可以减少一次拷贝,但是出了函数作用,返回对象还在,才能用引用返回

理解了func函数,那么operator=重载赋值函数返回选择哪种方式也是同样的方法:
*thisd2,在main函数传参的时候,this指针是存放栈空间的,当operator函数生命周期结束时,*this回到的是回到的是main函数的,也就是*this离开operator时生命周期未到,不会析构,因此按引用返回。
在这里插入图片描述

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

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int _year;int _month;int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
}

在这里插入图片描述

编译失败:
error C2801: “operator =”必须是非静态成员

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
在这里插入图片描述
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time& operator=(const Time& t){if (this != &t){_hour = t._hour;_minute = t._minute;_second = t._second;}return *this;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d1;Date d2;d1 = d2;return 0;
}

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
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;
}

直接无法加载发生异常!
在这里插入图片描述
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
在这里插入图片描述


🚩总结

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/855.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

unity学习(86)——细节优化

东西已经做出来了&#xff0c;现在需要的是优化&#xff0c;说得简单&#xff0c;做起来难。 1.122包的优化&#xff0c;避免重复创建&#xff01; 2.为何会出现一边动&#xff0c;一边不动的情况。重复登录后依旧是unity可以看到移动&#xff0c;但是exe那边看不到移动&#…

关于图像YUV格式分类和排布方式的全学习

【学习笔记】关于图像YUV格式分类和排布方式的全学习_yuv图像-CSDN博客 下图是将多个yuv420p图像(A和B)&#xff0c;拼接成一个画面的思路 A大小:416*64 B大小:416*208 将A和B合并到一个416*416的尺寸上&#xff0c;代码如下 //整合char * ptd;ptd (char * ) malloc (416*41…

C#通用类库封装实战

数据库查询 特性方式获取数据库列的别名 数据库更新 使用简单工厂配置的方式

矽塔SA8321 单通道 2.7-12.0V 持续电流 3.0A H 桥驱动芯片

描述 SA8321是为消费类产品&#xff0c;玩具和其他低压或者电池供电的运动控制类应用提供了一个集成的电机驱动器解决方案。此器件能够驱动一个直流无刷电机&#xff0c;由一个内部电荷泵生成所需的栅极驱动电压电路和4个功率 NMOS组成H桥驱动&#xff0c;集成了电机正转/反…

2024上海国际半导体制造设备材料与核心部件展览会

2024上海国际半导体制造设备材料与核心部件展览会 2024 Shanghai International Semiconductor Manufacturing Equipment Materials and Core Components Exhibition 时间&#xff1a;2024年11月18日-20日 地点&#xff1a;上海新国际博览中心 详询主办方陆先生 I38&#…

2024蓝桥杯嵌入式模板代码详解

文章目录 一、STM32CubeMx配置二、LED模板代码三、LCD模板代码 一、STM32CubeMx配置 打开STM32CubeMx&#xff0c;选择【File】->【New Project】&#xff0c;进入芯片选择界面&#xff0c;搜索到蓝桥杯官方的芯片型号&#xff0c;并点击收藏&#xff0c;下次直接点击收藏就…

【LeetCode: 39. 组合总和 + 递归】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

比特币减半倒计时:NFT 生态将受到怎样的影响?

BTC 减半倒计时仅剩不到 1 天&#xff0c;预计在 4 月 20 日迎来减半。当前区块奖励为 6.25 BTC&#xff0c;减半后区块奖励为 3.125 BTC&#xff0c;剩余区块为 253。比特币减半无疑是比特币发展史上最重要的事件之一&#xff0c;每当这一事件临近&#xff0c;整个加密社区都充…

Linux下SPI设备驱动实验:测试读取ICM20608设备中数据是否正常

一. 简介 前面文章实现了 SPI设备的读写功能&#xff0c;也对ICM20608设备中&#xff08;即SPI设备&#xff09;寄存器里的数据进行了读取。文章如下&#xff1a; Linux下SPI设备驱动实验&#xff1a;读取ICM20608设备的数据-CSDN博客 本文对驱动功能进行测试&#xff0c;即…

大数据平台搭建2024(二)

二&#xff1a;Hive安装 只在node01上操作 1 安装MySQL 8.0 最小化安装需要安装这个 yum install -y wget1-1 下载MySQL的yum源 wget http://dev.mysql.com/get/mysql80-community-release-el7-7.noarch.rpm检查是否安装成功 rpm -qpl mysql80-community-release-el7-7.n…

c++使用spdlog库打日记

打日记 打日志的本质就是多输出&#xff0c;c没有自带的日志库&#xff0c;只能使用第三方库实现&#xff0c;当然&#xff0c;直接cout输出也可以&#xff0c;但是一般日志库都进行了优化&#xff0c;比我们使用cout输出的效率更高&#xff0c;同时效果也更好&#xff0c;这里…

vue3【详解】 vue3 比 vue2 快的原因

使用 Proxy 实现响应式 vue3使用的 Proxy 在处理属性的读取和写入时&#xff0c;比vue2使用的defineProperty 有更好的性能&#xff08;速度加倍的同时&#xff0c;内存还能减半&#xff01;&#xff09; 更新类型标记 Patch Flag 在编译模板时&#xff08;将vue语法转换为js描…

基于Springboot的影城管理系统

基于SpringbootVue的影城管理系统的设计与实现 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatis工具&#xff1a;IDEA、Maven、Navicat 系统展示 用户登录 首页展示 电影信息 电影资讯 后台登录页 后台首页 用户管理 电影类型管理 放映…

基于TCC的分布式事务

优质博文&#xff1a;IT-BLOG-CN 一、分布式事务简介 分布式的架构中&#xff0c;分布式的事务是一个绕不过的挑战&#xff0c;微服务理念的流行让分布式的问题日益突出。 在公司内部&#xff0c; 笔者所接触的管理系统中实际上也存在着分布式事务。 这里假设有这三个系统&…

解线性方程组——直接解法:(Gauss)高斯消去法、列主元、全主元 | 北太天元

一、问题描述 对于线性方程组 A x b , A ( a 11 a 12 ⋯ a 1 n a 21 a 22 ⋯ a 2 n ⋮ ⋮ ⋮ a n 1 a n 2 ⋯ a n n ) , b ( b 1 b 2 ⋮ b n ) Axb,\quad A\begin{pmatrix} a_{11} & a_{12} &\cdots &a_{1n}\\ a_{21} & a_{22} &\cdots &a_{2n}\\…

win11家庭中文版安装docker遇到Hyper-V启用失败,如何解决??

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

PyQt程序:实现新版本的自动更新检测及下载(FTP服务器实现)

一、实现逻辑 本实例采用相对简单的逻辑实现,用户在客户端使用软件时点击“检测升级”按钮,连接至FTP服务器检索是否有新版本的.exe,如果有,下载最新的.exe安装升级。 本实例服务端待下载.exe所在目录结构 本实例客户端待更新.exe所在目录结构 二、搭建服务器 可以参考…

3. 无重复字符的最长子串/438. 找到字符串中所有字母异位词/560. 和为 K 的子数组

3. 无重复字符的最长子串 给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: s "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc"&#xff0c;所以其长度为 3。 思路&#xff1a;想象一下我们…

90天精通Psim仿真--经典实战教程--第10天 Simcode DSP28335 LED控制

PSIM (Power Simulation) 是一款电力电子和电机控制仿真软件,而DSP28335是德州仪器(TI)的一款数字信号处理器(DSP)。如果你想要在PSIM的SimCoder环境中为DSP28335生成LED闪烁的代码,遵循以下步骤: 打开PSIM并创建模型: 首先,在PSIM中创建一个电路模型,该模型应包括DS…

贪心(贪婪)算法

主要思想 贪心算法的思想主要可以概括为“总是做出当前看起来最优的选择”&#xff0c;也就是不从整体上进行考虑&#xff0c;所得到的答案是某种意义上的局部最优解&#xff0c;不一定是整体最优解。 贪心算法没有固定算法框架&#xff0c;算法设计的关键是贪心策略的选择。…