humid1ch blogs

本篇文章

手机用户建议
PC模式 或 横屏
阅读


C++ 2022 年 6 月 26 日

[C++] 类和对象(3): 赋值重载、运算符重载...

C++ 中,引入了 运算符重载,使同类对象之间可以使用运算符,进而提高代码可读性

一、运算符重载

C语言中,运算符(操作符) 是只能对 内置类型数据或表达式 进行操作的
而 C++ 中,引入了 运算符重载,使同类对象之间可以使用运算符,进而提高代码可读性 否则对象之间,一个简单的比较大小就得需要长代码实现. 如果需要多次比较,将十分痛苦

1.1 运算符重载定义

运算符重载 其实是一种具有特殊函数名的函数,其 返回值类型参数类型 同一般函数一样 但是 运算符重载函数的函数名格式为:operator运算符 即,operator + 需要重载的运算符 则函数的原型格式就为:返回值类型 operator运算符(参数列表)
并且,运算符重载还需要严格遵守一些规则:
  1. 不能创造新的运算符:比如 通过operator@ 想要赋予@一定的功能
  2. 重载运算符必须至少有一个自定义类型的操作数 :操作符必须要有操作数
  3. 重载后的运算符不能改变原有含义:不能将原来内置类型+的功能,重载之后改为 对象之间相减的功能
  4. .*::sizeof? :. 以上5个运算符不能重载(笔试、面试会考)
  5. 由于类内存在隐含的 this指针,所以参数要比类外少一个,参数要比运算符所需操作数少一个,且默认 this指针 为第一个参数 (这也是重载需要严格遵守的第五条规则)
日期类判断大于 为例:
bool operator>(const Date& d1, const Date& d2)	//比较大小不需要改变数值,所以const修饰,&可以节省资源
{
	// 日期比较大小需要遵循实际
    return (d1._year > d2._year
            || (d1._year == d2._year && d1._month > d2._month)
            || (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day));
}
当然因为C++类有封装特性,所以运算符重载函数是可以定义在类内的,不过与定义在类外有一定的区别:
在类内定义操作符重载:
操作符重载是一种函数,一般函数应该传参使用
但是,操作符这样用非常的反逻辑,所以其实操作符正常逻辑使用也是没有问题的

重载后的操作符,可以按照 正常的逻辑 使用 但其实,编译器还是会把使用操作符的语句,自动转换成这样 d1.operator>(d2) 或 这样operator>(d1,d2),不用手动操作

1.2 赋值运算符重载

赋值运算符重载函数 其实与 拷贝构造函数 类似
日期类的赋值运算符重载,内容也非常的简单,但是有一些需要非常注意的问题
赋值运算符重载,确实与拷贝构造函数相似,但是 为什么赋值运算符重载函数,需要返回赋值后的结果
当然是因为,内置类型的赋值运算符(=)结果也是需要当作返回值返回的,因为需要实现连等(连续赋值)操作:
所以,赋值操作的结果要作为返回值返回,以便下一次赋值使用,所以,将 *this 作为返回值返回

返回值是 Date& 类型的,使用引用是因为可以节省资源 但是并不是任何时候都可以使用引用类型作为返回值类型,因为 函数内的局部变量出了作用域就销毁了,如果再用&只会造成错误 这里使用 &,是因为 this指针 没被销毁


其实,在介绍类的默认成员函数时,有提到赋值重载函数也是 类的默认成员函数
这也就意味着,一个类,如果没有显式定义赋值重载函数,编译器也会自动生成一个赋值重载函数,赋值操作按字节拷贝完成
也就是说,对于成员变量简单的类来说,其实大部分都不用手动实现 默认成员函数
但是对于成员变量稍微复杂一点的变量来说,默认成员函数都是要手动实现的

1.2.1 思考

思考 此语句调用 赋值重载函数 还是 拷贝构造函数(d1已存在):
Date D = d1;
回答之前,先对比一下 赋值重载 和 拷贝构造 具体用于什么环境下:
  1. 赋值重载:将一个对象的内容 赋值于 另一个对象
  2. 拷贝构造:在对象实例化时,将一个对象的内容 初始化至 正在实例化的对象
这样一对比,其实就已经知道 Date D = d1 调用的是哪个函数了
这个语句调用的是 拷贝构造函数,因为这是在对象实例化时,要进行的初始化。编译器会分析出来并自动调用拷贝构造函数
可以看到, Date d2 = d1; 这个语句被执行时调用的是 拷贝构造函数; 而 d2 被实例化之后,再执行 d2 = d1,则调用 赋值重载函数.

1.3 日期类及运算符重载实现

上面已经对日期类实现了 大于与赋值运算符的重载
待实现的还有:==>=!=<<=+-+=-=++--
下面就从 == 开始逐一实现

==

判断两对象是否相等非常的简单:
其实,在实现了>< 的重载 和 ==的重载之后,其他的逻辑判断运算符,都可以直接复用 >==<== 来实现

>=、!=、<、<=:

+ (日期 + 天数)

对于日期类,实现 计算用运算符的重载,需要存在一定的意义
比如,两个日期相加 是没有什么实际意义的:2002.1.1 + 2000.1.1 有什么实际意义呢?
所以,日期类重载 + 一般实现的是,日期 + 天数 的功能
不过,日期的计算需要参考实际情况: 1、3、5、7、8、10、12月:31天; 4、6、9、11月:30天; 2月:一般28天,闰年29天
由于日期的多变,所以日期计算实现起来稍微复杂一点:

具体思路是:

  1. 先把需要加的天数全部加到对象的_day上
  2. 然后逐月计算,完成月份和年份的进位
在实现 + 的重载之前,先实现一个通过年月来计算月份天数的函数:
然后实现 + 重载:
年月份的进位逻辑是:
  1. 先将要加的天数加到 _day
  2. 循环判断当前的 _day 是否大于 当前月的实际天数
  3. 如果大于,_day 就减去当前月的实际天数,并且 月份加一(_month++)
  4. 每次_month == 13 时,表示本年过完,需要年份加一(_year++),且月份归一(_month = 1)
  5. 直到 _day 不大于 当前月的实际天数,返回临时对象

需要将临时对象作为返回值返回,因为需要保证连加操作

实现过程中需要注意的就是:
  1. 不能直接操作 this指针 所指对象,否则会导致原对象数值改变
  2. 拷贝的临时对象出函数会被销毁,所以不能用 &类型 作为返回值

+= (日期 += 天数)

+ 的重载一样,+= 通常也是实现 日期 += 天数
实现逻辑与 + 完全相同,不过 因为+=需要改变原对象数值,所以可以直接操作 this指针 指向的对象 也就意味着,可以将 *this 作为返回值返回,并且可以使用 &类型 节省资源

- (日期 - 天数)

根据实际情况,日期类 - 的重载其实可以有两种重载方式:
  1. 日期 - 天数 ---> 日期
  2. 日期 - 日期 ---> 天数
先实现,日期 - 天数的重载:
年月日进位逻辑:
  1. 如果需要减去的天数(day)比当前_day 大或两者相等,进循环;否则直接拿 _day - day就可以结束了
  2. 进循环之后,先用 day - 当前的 _day,并且 月份 - 1(--_month),代表减一个月
  3. 减一个月后,如果_month == 0,就代表这一年没了,年份需要减一(--_year),并且设置月份从12月开始(_month = 12)
  4. 再将当前天数,设置为当前月份天数,继续循环判断
  5. day 不再大于 _day 时,退出循环,然后 _day - day. 至此,天数减完毕. 然后返回临时变量

需要将临时对象作为返回值返回,因为需要保证连减操作

实现过程中需要注意点与 +的重载相同:
  1. 不能直接操作 this指针 所指对象,否则会导致原对象数值改变
  2. 拷贝的临时对象出函数会被销毁,所以不能用 &类型 作为返回值

-= (日期 -= 天数)

重载 -= 的逻辑与 - 相同,也不过是直接操作*this 而已

+、+=、-、-= 之间的复用及完善

在了解了 ++=--= 的重载函数之后,其实可以发现 ++=--= 之间其实是有一定的关系的
先以 ++= 为例对比:
两重载函数之间的代码几乎一模一样,只不过是一个操作 ret的成员变量,另一个操作 this指向的成员变量 所以,这++=其实是可以互相复用的:
这两种复用,都可以达成效果
不过,有一个问题:+= 复用 ++ 复用 += 哪个更优一点?
答案是:+ 复用 += 更优一点; 因为,+ 的重载需要调用两次拷贝构造函数。直接实现+=重载,是不需要调用拷贝构造函数的;如果 += 复用 +,就意味着 += 同样需要调用两次拷贝构造函数; 如果是,+ 复用 += ,则只有 + 需要调用拷贝构造函数
--= 同样是这样的关系
所以,+ 的重载 一般复用 += 实现,-的重载 一般复用-= 实现

前面实现的 + += - -= 的重载 都只实现了对正数的操作,无法实现 对象 与 负数 的运算
不过,实现过 + += - -= 的重载之后,添加对负数的运算也只不过是加一个条件的事:
由于,复用了 +=-=,所以只需要在 +=-= 的重载函数中添加条件就可以了

—、++

--++ 分有前置和后置
C++ 语法规定,后置--++,要在参数列表中添加一个 int 类型
所以--++ 的重载非常的简单:

- (日期 - 日期)

日期类中 - 的重载还有一种意义,就是 两日期对象相减求相差多少天
不过,这个 - 的重载相对另一个,稍微简单一些:

二、const 成员

const 修饰的变量无法被修改
而之前介绍过,变量的权限可以缩小但是不能放大 比如:不能对被const修饰的变量,起一个不被const修饰的别名. 不能放大权限
由此,对于一个类来说,也会可能实例化一个被const修饰的对象,或者函数的参数使被const修饰的类 当一个对象 被const修饰 时,表示对象的成员不能被修改,则其成员函数很可能就不能正常的使用了
比如这样:
为什么呢?
因为,对象调用成员函数时,编译器会自动 将对象的地址作为this指针的内容传参到成员函数中 this指针 的类型(此类中)为 Date* const this; 而此时的对象是被const修饰的对象,此对象传参作为this指针的内容,是使其从const Date转换到Date是属于权限的放大,是不能成立的
this指针是由编译器控制的,无法手动更改
那么怎么样才能使 const对象 也正常的使用成员函数呢?

2.1 const修饰成员函数

const 其实可以修饰成员函数。当 const 修饰成员函数时,此函数被称为 const 成员函数
而,const修饰成员函数时,所处的位置是在参数列表的后边,即:
当成员函数被 const 修饰时,即使是 const对象也能够正常的使用成员函数了
这就也说明,const修饰对象的地址可以作为 this指针 的内容了,这又是为什么呢?
其实是因为,const修饰成员函数 就是 修饰了成员函数的 this指针,即:
虽然无法直接改变 this指针 的类型,但是 const 修饰成员函数就是 const 修饰了 this指针。 编译器会将 const 转换为修饰 this指针

思考1 *

  1. const对象 可以调用 非const成员函数吗?

    经过上面的介绍,可以得出答案是:不可以

    因为 非const成员函数 意味着this指针未被const修饰

    const对象 无法传参,所以不可以

  2. const对象 可以调用 const成员函数吗?

    答案是可以,因为const对象可以传参给const成员函数(const修饰的this指针)

  3. const成员函数 可以调用其他 非const成员函数吗?

    答案是不可以,const成员变量,其this指针也是被const修饰的

    调用其他成员函数,也就是使this指针的内容作为参数

    又是一种 const对象无法传参,所以不可以

  4. const成员函数 可以调用其他 const 成员函数吗?

    答案是可以,const对象 可以传参给 const 修饰的this指针

  5. const什么时候应该修饰成员函数?

    const修饰成员函数,其实本质是:const 类* const this

    使 this指针 的内容无法改变

    所以,其实当成员函数不改变对象的成员变量时,一般用const修饰此成员函数

思考2 *

(d1 + 100).print(); 结合上面+的重载,这个语句有什么问题?(假设d1为已实例化的对象,print()是其成员函数且未被const修饰)
Date operator+(int day);
+的重载的返回值是 Date类型的,即+ 的重载是传值返回,而这意味着 计算完成的结果 其实被作为了一个临时的对象存储了起来

在之前的文章中介绍过:

其实,函数一个局部变量作为返回值传值返回时,由于局部变量会被销毁,所以返回结果会被作为临时变量存储起来 这个临时变量是无法被修改的,具有常性,即可看作为const修饰的

而临时对象具有常性,所以是无法调用 非const成员函数的

当前 VS 编译器无法演示 (Linux) 好像也无法演示

但是应是如此

三、&(取地址) 与 const&重载

类的六大默认成员函数,已经介绍了四个:构造函数、析构函数、拷贝构造函数、赋值重载函数
剩下的两个就是,取地址重载函数 和 const修饰的取地址重载函数
这两个 默认成员函数绝大多数情况是不需要手动实现的,编译器自动生成的就已经能够解决绝大多数的情况 除非,不想取地址成功
这两个重载函数的实现也非常的简单:

这两个默认成员函数,都是在取对象地址时自动调用的.


版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

作者: 哈米d1ch 发表日期:2022 年 6 月 26 日