[C转C++之路]拷贝构造和运算符重载

79 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第30天,点击查看活动详情

前言

       本文就来分享一波作者对C++的拷贝构造函数和运算符重载的学习心得与见解。

       笔者水平有限,难免存在纰漏,欢迎指正交流。

拷贝构造函数

概念

       在现实生活中,可能存在两个长得几乎一模一样的人,我们称其为双胞胎。那在创建对象时,可否创建一个与已存在对象一模一样的新对象呢?

       拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰防止修改),在用已存在的类类型对象创建新对象时由编译器自动调用.

特征

       拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
 class Date
 {
 public:
     Date(int year = 1900, int month = 1, int day = 1)
     {
         _year = year;
         _month = month;
         _day = day;
     }
     // Date(const Date& d) // 正确写法
     Date(const Date d) // 错误写法:编译报错,会引发无穷递归
     {
         _year = d._year;
         _month = d._month;
         _day = d._day;
     }
 private:
     int _year;
     int _month;
     int _day;
 };
 int main()
 {
     Date d1;
     Date d2(d1);
     return 0;
 }

image-20220923102811300

       为什么会这样呢?因为形参要拷贝实参的内容,这一拷贝行为是通过一个临时对象来实现的,创建临时对象就要调用拷贝构造函数,一旦调用就要创建另一个形参,同样的这另一个形参也要拷贝实参内容,所以又要调用拷贝构造函数,然后又要......现在明白为什么直接传值会引起无穷递归调用了吧。

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对于对象内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

       注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其默认拷贝构造函数完成拷贝的。

       但是浅拷贝不一定能满足我们的需求,就比如成员变量是指针时,如果浅拷贝的话两个对象的指针就指向同一块空间了,会互相影响,并且在销毁时会两次调用析构函数销毁这块空间,这样就会出问题。

 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(s1);
     return 0;
 }

image-20220923111833711

       改成深拷贝(简单而言,深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响),需要我们自己写拷贝构造函数来实现。

 Stack(const Stack& st)
 {
     _array = (DataType*)malloc(st._capacity * sizeof(DataType));
     if (nullptr == _array)
     {
         perror("malloc申请空间失败");
         return;
     }
     _size = 0;
     _capacity = st._capacity;
     memcpy(_array, st._array, sizeof(DataType) * _capacity);
 }  

       注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,由我们自己实现深拷贝才行,否则就是浅拷贝。

       还可以这么看:需要写析构函数的类都需要写深拷贝的拷贝构造函数;不需要写析构函数的类,默认生成的浅拷贝的拷贝构造函数就够用了。

  1. 拷贝构造函数典型调用场景: 使用已存在对象创建新对象 函数参数类型为类类型对象 函数返回值类型为类类型对象

运算符重载

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

       运算符重载能让自定义类型对象使用运算符,一般来说对于内置类型的操作都内置到编译器了,就比如整型加减乘除直接使用运算符即可,而用户自定义类型则不然,是否需要和如何实现某些操作都是由用户来决定的,如果想要让自定义类型的变量也能使用运算符,那就需要自己设计运算符重载函数。

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

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

注意:

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

       我们看看如何重载==,先放在全局域:

 // 全局的operator==
 class Date
 {
 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;
 }
 void Test ()
 {
     Date d1(2022, 9, 22);
     Date d2(2022, 9, 23);
     cout<<(d1 == d2)<<endl;//这样一来使用==就相当于使用operator==(d1, d2);
 }

       我们考虑把该操作符重载为成员函数:

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

       其实像上这样写编译器还是会报错:

image-20220923123657987

       为什么呢?不是只传了两个参数吗?==运算符本身也就对应两个操作数而已啊。还记得前面讲过的this指针吗?每一个成员函数其实都隐含了一个形参this指针,所以这里就相当于有三个参数,要削减一个。

 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& d)
     // 这里需要注意的是,左操作数是this指针,指向调用函数的对象
     bool operator==(const Date& d)
     {
         return _year == d._year;
         && _month == d._month
         && _day == d._day;
     }
 private:
     int _year;
     int _month;
     int _day;
 };
 ​
 void Test ()
 {
     Date d1(2022, 9, 22);
     Date d2(2022, 9, 23);
     cout<<(d1 == d2)<<endl;//这样一来使用==就相当于使用d1.operator==(d2);
 }

       继续看看operator!=怎么写:

 bool operator!=(const Date& d)
 {
     return _year != d._year
         || _month != d._month
         || _day != d._day;
     //或者还可以这么写:
     //return !(*this == d);//反正只要相等的取反就是不相等
 }

       看看operater>怎么写:

 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;
 }

       那operater>=怎么写:

 bool operator>=(const Date& d)
 {
     return *this > d || *this == d;
 }

       直接复用前面的函数,轻松愉快就搞定~

       operator<和operator<=也是只需要复用>和==的重载用逻辑判断弄一下就行了:

 bool Date::operator<(const Date& d)
 {
     return !(*this > d || *this == d);
 }
 ​
 bool operator<=(const Date& d)
 {
     return *this < d || *this == d;
 }

       我们这里举例的类是日期类,那要是日期类对象要加上或减去一个天数该怎样用运算符重载实现呢?就比如要实现d1+100。

       函数形参就是一个整型,返回值类型是Date类,日期加天数不还是一个日期嘛。关键在于日期的日、月和年的进位退位的关系如何考虑,而且月的天数各异,平年闰年还有影响。

       我们先写一个获取不同月份的天数的函数。

 int GetMonthDays(int year, int month)
 {
     //因为要频繁使用,设成静态的高效些
     static int days[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 days[month];
 }

       然后就是+运算符重载,实际上我们这里考虑先实现+=运算符重载,然后再通过它来实现+运算符重载。

       加天数就要考虑进位问题,不仅天数会进位,月数也会进位,只要原日期的_day加上天数后大于当月天数就要进位,同时还要注意月数满12就要进位,放到循环里直到_day不超出当月天数即可。

 Date& operator+=(int day)//返回类型最好用引用,因为函数调用完毕后对象仍存在
 {
     if(day < 0)//处理传入负数的情况
     {
         return *this -= -day;
     }
     _day += day;
     while (_day > GetMonthDays(_year, _month))
     {
         _day -= GetMonthDays(_year, _month);
         ++_month;
         if (_month == 13)
         {
             ++_year;
             _month = 1;
         }
     }
     return *this;
 }
 ​
 Date operator+(int day)//因为ret是临时对象,所以返回时不要用引用类型,直接传值返回
 {
     //拷贝构造一个来运算后返回
     Date ret(*this);
     ret += day;
     return ret;
 }

       对于-=和-也是同理,很容易就搞定了:

 Date& Date::operator-=(int day)
 {
     if(day < 0)//处理传入负数的情况
     {
         return *this += -day;
     }
     _day -= day;
     while (_day <= 0)
     {
         --_month;
         if (_month == 0)
         {
             --_year;
             _month = 12;
         }
         _day += GetMonthDays(_year, _month);
 ​
     }
     return *this;
 }
 ​
 Date Date::operator-(int day)
 {
     Date ret(*this);
     ret -= day;
     return ret;
 }

以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~

src=http___c-ssl.duitang.com_uploads_item_201708_07_20170807082850_kGsQF.thumb.400_0.gif&refer=http___c-ssl.duitang.gif