运算符重载(上)

简介: 运算符重载

前言:


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


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


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


注意:


1、常用的符号有+、-、*、/、++、--、==、=、>、<、>=、<=等符号,不能通过连接其他符号来创建新的操作符比如operator@、operator$等。


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


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


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


5、注意:.*   ::   sizeof   ?:   .  这5个运算符不能重载,这是经常要考察的内容。


🏆一、赋值运算符重载


1、赋值运算符重载格式


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


       ·返回值类型: T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。


       ·检查是否自己给自己赋值。


       ·返回*this:要符合连续赋值的含义。


赋值运算符重载的声明格式是这样的:


类名 & operator=(const  类名& 类类型参数)


我们以日期类为例:

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


👓1.1参数设计细节


1、传引用并且用const限制的原因是,传值会调用拷贝构造,const限制可以防止被修改。


2、返回值返回引用主要是为了支持连续赋值。


假如我返回void类型:

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

1669268978319.jpg

可以发现,我们的确完成了拷贝的任务,但是返回值是void类型的话会出现什么问题呢,无法连续赋值!

1669268989611.jpg

因为没有返回值是无法连续赋值的,可能这样说还不够清楚,我就举个简单的例子:

int main()
{
  int i, j;
  i = j = 10;
  (i = j = 10)++;//可以修改
  cout << i << "  " << j << endl;
  return 0;
}

1669269026693.jpg

观察编译器默认的连续赋值是怎样的呢?我们可以总结出如下规律:


1、赋值从右往左:10赋给j,j=10有一个返回值,然后赋给i。


2、返回的值是可以修改的,可以基本判定返回的值是左值(赋值运算符=的左边),因为右值是作为const常量传参的,返回右值就不可被修改且缩小了权限。


所以我们就可以写出:

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

1、可能有的老铁困惑:出了函数,this就要被销毁,这里为什么不返回类类型,而是返回类引用呢?因为这里this虽然被销毁,但是作为别名,我们要赋值的类对象是存在的。所以使用引用是可以的,而且可以少一次拷贝(传值返回是需要拷贝构造的)。


2、至于返回*this而不返回d的原因也很简单,d是被const限制的,如果返回d意味着返回值不可被改变,权限缩小,这是不合适的。


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


// 赋值运算符重载成全局函数,注意重载成全局函数时没有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 =”必须是非静态成员

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


而且我们在类外是访问不了private内的类型的。

1669269044566.jpg


👓1.3需要写赋值重载函数的场景


我们知道赋值运算符重载是编译器默认生成的函数,那么如果我们不写这个函数,编译器自动生成的能否完成赋值拷贝的功能呢?


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


class Date
{
public:
  int GetMonthDay(int year, int month)
  {
  static int monthDayArray[13] = { 0,31,28,31,30,31,30,
  31,31,30,31,30,31 };
  if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
  {
    return 29;
  }
  else
  {
    return monthDayArray[month];
  }
  }
  Date(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = day;
  //检查日期的合法性
  if (!(year >= 1
    && (month >= 1 && month <= 12)
    && (day >= 1 && day <= GetMonthDay(year, month))))
  {
    cout << "非法日期" << endl;
  }
  }
  Date(const Date& d1)
  {
  _year = d1._year;
  _month = d1._month;
  _day = d1._day;
  }
  /*Date& operator=(const Date& d)
  {
  _year = d._year;
  _month = d._month;
  _day = d._day;
  return *this;
  }*/
  void Print()
  {
  cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
void TestDate1()
{
  Date d1;
  Date d2(2022, 10, 8);
  Date d3;
  d1.Print();
  d1 = d2;
  d1.Print();
}
int main()
{
  TestDate1();
  return 0;
}

1669269087521.jpg

我们可以看到,它是可以完成赋值重载的功能的,那么是不是我们就不用赋值重载或者,哪些情况需要我们去写呢?


🖊②自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值


这里和拷贝构造函数是十分相似的,不写析构函数的不用写赋值重载函数。


什么意思?就是说不涉及动态开辟内存或者文件管理的不需要写赋值重载。


比如说一个自定义类型栈,如果我不去写它的赋值重载,会发生什么?


1669269106121.jpg


我们可以看到直接崩掉,崩掉的原因是什么呢?


1669269114521.jpg


我们可以看到,没有写赋值重载函数st1的_a和st2的_a指向了同一块空间。那会有什么危害呢?


1669269123414.jpg


指向同一块空间不仅会导致在进程结束的时候,对栈st2的_a空间析构两次(free两次),也会发生内存泄露。即st1的_a开辟的空间没有被free掉,这是十分危险的!!


有的老铁可能要说,那不简单,我直接realloc一下不就行了?真的是这样吗?我们再来分析一下。


这里的情况有三种:


1、栈st1的_a开辟的空间小于st2的_a。


2、栈st1的_a开辟的空间等于st2的_a。


3、栈st1的_a开辟的空间大于st2的_a。


那可能我们能这样分析:如果小于,就realloc后拷贝复制。如果等于直接覆盖赋值,如果大于就直接赋值。这样判断是否麻烦了?而且如果我st1的_a开辟的空间是10000个字节,而栈st2的_a开辟的空间只有1000个字节,只将st2的_a赋值给st1的_a是否太过于浪费空间?


所以这里的操作是直接free掉,然后拷贝复制。


1669269135920.jpg


我们看到这样好像赋值重载成功了。但是这里还是有老六的情况出现。


🖊③自己赋值自己

1669269146385.jpg


如果把st1赋值给st1,就会出现这样的状况,因为我们先把st1的_a给free掉了,然后再去赋值st1的_a,这样显然会出错,所以为了防止这样的状况,我们就加一层判断。

1669269153656.jpg

👓 1.4赋值重载和拷贝构造函数的对比


通过对赋值重载的介绍,大家是否发现它和拷贝构造很是相似,那么我们就来对比一下。

Date d1;
  Date d2(2022, 10, 8);
    Date d3(d2);//拷贝构造(初始化)  一个初始化另一个,还没初始化
  d1 = d2;

我们发现赋值重载的特点是这两个对象都已经存在,而且初始化构建好了,是已经存在的两个对象之间的拷贝。而拷贝构造函数是初始化,一个初始化另一个且这个类对象还没有创建。

Date d2(2022, 10, 8);
Date d4=d2;

这种情形算作是拷贝构造还是赋值重载呢?单看符号似是赋值重载,但我们要看本质,它的意义是初始化且拷贝(因为d4原来不存在),所以严格上讲它是拷贝构造。


拷贝构造和赋值重载的区别:


1、拷贝构造是初始化,一个初始化另一个,这个类对象还没创建;


2、赋值重载的特点是这两个对象都已经存在,并且初始化构建好了,是已经存在的两个对象之间的拷贝。


3、赋值重载函数和拷贝构造函数还有一个共同特点是当需要写析构函数时,它们都需要写。


🏆二、运算符重载


上面只是介绍了赋值运算符重载,当然还有很多运算符重载,它们是如何实现的呢?博主以日期类为例,介绍各类运算符重载。


日期类的创建和声明:

class Date
{
public:
  int GetMonthDay(int year, int month)
  {
  static int monthDayArray[13] = { 0,31,28,31,30,31,30,
  31,31,30,31,30,31 };
  if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
  {
    return 29;
  }
  else
  {
    return monthDayArray[month];
  }
  }
  Date(int year = 1, int month = 1, int day = 1)
  {
  _year = year;
  _month = month;
  _day = day;
  //检查日期的合法性
  if (!(year >= 1
    && (month >= 1 && month <= 12)
    && (day >= 1 && day <= GetMonthDay(year, month))))
  {
    cout << "非法日期" << endl;
  }
  }
  //fopen fclose
  Date(const Date& d1)
  {
  _year = d1._year;
  _month = d1._month;
  _day = d1._day;
  }
  //运算符重载的意义是可读性
  Date& operator=(const Date& d)//赋值可以传值,不会出现无穷递归,但是尽量使用传引用
  {
   _year = d._year;
   _month = d._month;
   _day = d._day;
   return *this;//这样写不好,返回Date,传值返回也是拷贝构造,
  }
  //赋值重载
  void Print()
  {
  cout << _year <<"/"<<_month<<"/"<<_day<< endl;
  }
  Date& operator+=(int day);
  //日期+天数
  Date& operator-=(int day);
  Date operator-(int day);
  Date operator+(int 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;
  else
    return false;
  }
  //==
  bool operator==(const Date& d)
  {
  if (_year == d._year && _month == d._month && _day == d._day)
    return true;
  else
    return false;
  }
  //>=
  bool operator>=(const Date& d)
  {
  return (*this) > d || (*this) == d;
  }
  //<
  bool operator<(const Date& d)
  {
  return !(*this > d);
  }
  //<=
  bool operator<=(const Date& d)
  {
  return !(*this >= d);
  }
  //!=
  bool operator !=(const Date& d)
  {
  return !(*this == d);
  }
  Date& operator++();
  Date operator++(int);
  //后置多两次拷贝,所以避免使用后置
private:
  int _year;
  int _month;
  int _day;
};


👓2.1、基本运算符重载


🖊①==和!=


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

🖊②>和<

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;
  else
    return false;
  }
bool operator<(const Date& d)
  {
  return !(*this > d);
  }
bool operator>=(const Date& d)
  {
  return (*this) > d || (*this) == d;
  }
bool operator<=(const Date& d)
  {
  return !(*this >= d);
  }

这些都比较简单,唯一需要注意的就是博主十分推荐复用,可以方便很多。


🖊③+=和+


这个有点意思,如果给我们一个日期,让我们计算比如100天后是什么日期,我们如何计算呢?

Date& Date::operator+=(int day)
{
  if (day < 0)
  {
  return *this -= -day;
  }
  _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
  _day -= GetMonthDay(_year, _month);
  _month++;
  if (_month == 13)
  {
    _year++;
    _month = 1;
  }
  }
  return (*this);
}
//日期+天数
Date Date::operator+(int day)
{
  Date ret(*this);
  ret += day;
  return ret;
}

1、对于+=,返回的还是自身,所以我们选择返回引用,对于函数体较大且不经常调用的我们一般建议放在类外实现。对于+,复用即可。


2、我们需要注意的一个细节是如果+=的天数是一个负数,我们需要注意,虽然不太可能出现,但还是应该考虑到。

相关文章
|
5月前
|
存储 弹性计算 网络协议
5 分钟构建企业级域控:AD DS 一键部署与实战指南
Active Directory 域服务(AD DS)不仅是企业级身份管理的基石,更是您实现**集中认证、策略统管、安全高效**IT架构的核心引擎。
|
5月前
|
监控 数据可视化 物联网
告别进度失控与成本超支!追踪复杂工程的高效工具选型指南
复杂工程管理面临进度、资源、成本与协同难题,传统工具难以为继。本文系统解析超大型基建、EPC、BIM施工等场景的精准工具选型:从轻量协同到企业级管控,覆盖Primavera、BIM5D、SAP等方案,并提炼多级计划、4D模拟、EVM等核心能力,提供四步落地策略与快速选型口诀,助力实现项目全过程数字化闭环管理。(238字)
|
5月前
|
存储 人工智能 图形学
阿里云无影 GPU 云电脑(NVIDIA RTX 5880 显卡)收费价格表:月付与年付费用详解
阿里云无影 GPU 云电脑凭借搭载的 NVIDIA RTX 5880 专业显卡,成为高性能计算场景的热门选择 —— 无论是 3D 建模、工业设计这类图形密集型任务,还是 AI 推理、机器人仿真等计算需求,都能依靠其强劲的硬件配置高效完成。对有这类需求的用户来说,最关心的就是不同配置的具体收费标准,尤其是月付和年付的费用差异,以及如何根据自身场景选择性价比最高的方案。本文结合最新的价格信息和配置细节,用通俗的语言拆解各规格的收费情况,同时补充适用场景和计费方式说明,帮大家清晰掌握成本构成与选型逻辑。
|
11月前
|
JavaScript 前端开发 API
鸿蒙5开发宝藏案例分享---Web加载时延优化解析
本文深入解析了鸿蒙开发中Web加载完成时延的优化技巧,结合官方案例与实际代码,助你提升性能。核心内容包括:使用DevEco Profiler和DevTools定位瓶颈、四大优化方向(资源合并、接口预取、图片懒加载、任务拆解)及高频手段总结。同时提供性能优化黄金准则,如首屏资源控制在300KB内、关键接口响应≤200ms等,帮助开发者实现丝般流畅体验。
|
存储 缓存 JavaScript
NPM怎样清空缓存
NPM怎样清空缓存
|
人工智能
替代你的不是AI,而是会使用AI的人——生成式人工智能(GAI)认证成为职场新宠
在AI技术飞速发展的数字化时代,职场环境正经历深刻变革。生成式人工智能(GAI)认证的出现,为职场人士提供了提升自我、证明能力的新途径。由培生推出的GAI认证,涵盖核心技能与伦理知识,助力求职者脱颖而出。它不仅是职场晋升的加速器,还为企业认可的专业能力背书。拥抱AI、学习AI,通过GAI认证,让自己成为掌握AI技术的领先者,在竞争中保持优势。
|
数据可视化 数据挖掘
ECharts综合案例一:近七天跑步数据
使用ECharts进行一周跑步数据分析,通过雷达图展示多维度指标(如距离、速度、时间),颜色对比体现个人与平均表现。折线图则清晰显示每日里程趋势,代码示例展示了自定义的`radar`和`line`图表配置。图表交互性强,支持点击高亮,动画流畅,提供完整代码资源。#ECharts #跑步数据 #数据可视化
531 3
ECharts综合案例一:近七天跑步数据
|
Java 关系型数据库 MySQL
如何安装系统必备开发环境:JDK 1.8+、MySQL 5.7+ 与 Maven 3.0+
【7月更文挑战第1天】搭建Java开发环境:安装JDK 1.8+,MySQL 5.7+,Maven 3.0+。访问官方源下载对应软件,配置Windows或Linux/macOS的环境变量,包括`JAVA_HOME`, `PATH`, `MYSQL_ROOT_PASSWORD`及`MAVEN_HOME`。测试安装成功分别用`java/javac -version`, `mysql -u root -p`和`mvn -v`检查版本。完成后,即可开始Java项目开发。
1337 0
|
Java 关系型数据库 MySQL
实时计算 Flink版操作报错合集之同步tidb到hudi报错,一般是什么原因
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
前端开发 NoSQL JavaScript
Websocket 替代方案:如何使用 Firestore 监听实时事件
Websocket 替代方案:如何使用 Firestore 监听实时事件