类和对象中-拷贝与赋值

博主博客链接:https://blog.youkuaiyun.com/Beforeaa?spm=1000.2115.3001.5343。欢迎大家讨论,不懂的可私信,也欢迎大家指正。


前言

上一章,我们学习了类中默认成员函数中的构造与析构,接下来,我们将继续对剩下的函数进行讲解

一、拷贝构造函数

1.拷贝构造是什么

拷贝大家都不陌生,就是拿一个现存的东西弄出一份与之一模一样的,拷贝构造即是如此。我们来看看拷贝构造的简单实现。代码如下:

class Date
{
public:
    Date(const Date & d)
    {
       _year = d._year;
       _month = d._month;
       _day = d._day;
    }
    Date(int year = 1,int month = 1,int day = 1)
    {
       _year = year;
       _month = month;
       _day = day;
    }
private:
    int _year;
    int _month;
    int _day
};
int main()
{
    Date d1(2025,8,11);//先创建一个d1变量
    Date d2(d1);//后创建的d2变量与d1变量的各成员变量的数值相同,这就是拷贝构造
    Date d3 = d1;//这种也是拷贝构造的一种写法
    return 0;
}

如上述代码,我们先构造一个d1变量,给予其一定的值,要是我们想要再创建一个变量与d1各值一样,我们就可以使用拷贝构造。

2.拷贝构造的特点

1.拷贝构造函数是构造函数的一个重载

即说明拷贝构造函数可以与其他与之不构成重载的构造函数并存,也说明了拷贝构造的函数名也是类名

2.拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用

这点比较难,我画个图方便大家理解
在这里插入图片描述

3.自定义类型传值传参和传值返回都会调用拷贝构造完成

传值传参:就是我上面举的Date (Date d)的例子
传值返回:

Date test()
{
Date d1(2025,8,11);
return d1;
}
4.若我们未显示定义拷贝构造函数,编译器会自动生成拷贝的构造函数。其行为:对内置类型进行浅拷贝(一个字节一个字节的拷贝);对自定义类型会调用其的拷贝构造。

Date里面的成员变量year,month,day都是简单的数值表示,所以我们自己不写拷贝构造函数的话,编译器自动生成的拷贝函数可以简单满足我们的值拷贝需求。

那么要是像栈一样的自定义类型呢,栈的成员变量有指针,容量和栈的大小。一个栈它的指针指向一块动态内存开辟的空间,假设这块空间记为s1,那么用这个栈去拷贝构造另一个栈,那么这个新的栈中的指针也指向了这块s1(假设我们不显示的去写拷贝构造函数,就会进行浅拷贝),也就是说,两块不同的栈中的成员变量的指针都是一样的,都指向了同一块空间,那么这就达不到我们的效果,所以,类似需要从堆中申请空间的类类型,基本都需要自己显示的去写拷贝构造

在这里插入图片描述
那么,Stack的拷贝构造函数应该怎么实现呢?请看下面代码

typedef int Stackdatatype;
//....
class Stack
{
   Stack(Stack & st)
   {
      _a = (Stackdatatype *)malloc(sizeof(Stackdatatype)*st._capacity);//_a指向一块新的空间
      memcpy(_a,st._a,sizeof(Stackdatatype)*st._top);//新旧空间存放的内容是一致的
      _top = st._top;
      _capacity = st._capacity;
   }
};

Tips:memcpy函数的作用是拷贝空间,在上述代码的作用是把一块空间拷贝给给另外一块空间,把st._a上sizeof(Stackdatatype)*_top的字节大小的空间拷贝到_a中的。

3.注意事项

传引用返回可以减少拷贝,因为如果是传值返回的话,系统会产生一个临时对象调用拷贝构造。传引用返回的话,返回的是该对象的别名,就不会调用拷贝构造,产生内存了。
但是需要注意的是,如果你要使用传引用返回,必须确保该返回对象出了作用域还在(即出了作用域,不会被销毁),如果该对象出了作用域就被销毁,那么返回它的别名,就类似于野引用(但是即使是野引用,大概率也不会出错)。

下面有两个例子:

int & f1()
{
   int ret = 10;
   return ret;
}

上面的代码,返回的是ret的别名,但是我们都知道,局部变量出了局部作用域就会被销毁,所以说上面的代码是不完美的,但是一般也不会出错,所以该程序会被正常执行。

(这里的程序之所以不会出错,是因为ret只是明面上被销毁了,但是得有人重新使用这块空间并修改了,程序才会崩溃)
就像我们住酒店,房间被我们弄得杂乱无章(相当于我们使用了ret这块空间),然后我们去退房(退房就相当于销毁)。有新的客人来了,客人在房间这么乱的基础上也弄得很乱(相当于再次使用了这块空间)

Stack & f2()
{
   Stack st;
   st.push_back(1);
   return st;
}

在这部分代码中,我们创建了栈类型的st变量,往st放了一个1,然后后面返回了st的别名。但是结合我们前面所学的知识,一个类,它有构造与析构函数,而且都是自动调用的,也就是说,刚创建出st这个变量就会自动初始化,那么,同理,出了这个作用域,st就会被析构函数所销毁,但是这里的销毁是指把整块st空间都给摧毁掉,所以返回st的别名,就不知道是返回什么了,所以程序会崩溃。

这里提供两个解决方法

①使用static修饰

Stack & f()
{
    static Stack st;
    st.push_back(1);
    return st;
}

注:static修饰的变量,出了作用域也不会销毁,保持原态。
②传值返回

Stack f2()
{
    Stack st;
    st.push_back(1);
    return st;
}

注:传值返回相当于返回st的拷贝,先赋值一份st,然后st再销毁,反正我已经得到一份st了,st销不销毁已经没什么关系了。

二、运算符重载

运算符重载是一个大类,我们可以重载很多运算符,而重载赋值运算符(=),才是我们类中的一个默认成员函数

1.什么是运算符重载

前景引入:内置类型,比如像int,double这样的类型,比较大小就很简单,就是单纯的比较数值大小。那要是自定义类型呢?就比如我们上面所写的Date类型,其成员变量一共有三个,无法进行简单的数值相比较。所以运算符重载就像是我们自己写一个函数,以我们自己定的规矩来确定哪个变量大,哪个变量小之类的一个方式。

2.赋值运算符重载

在这里,我们将通过重载赋值运算符这个案例来教会大家怎么进行重载运算符。

class Date
{
public:
    void operator=(const Date & d)
    {
       _year = d._year;
       _month = d._month;
       _day = d._day;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1(2025,8,12);
    Date d2(2025,1,1);
    d1 = d2;
    return 0;
}

如上,如果要重载一个运算符返回值+operator+需要重载的运算符即可就像我们重载d1=d2一样,d1通过隐含的this指针可以指向,重载函数中的d就用来接受d2,Date类中的相等,就是各成员变量均相等,所以我们让d1的各个变量都等于d2的各个变量。

Tips:赋值和拷贝构造的区别
①拷贝构造是用一个已初始化的对象去初始化一个刚定义的对象,如
Date d1(2025,1,1);
Date d2 = d1;//d2刚定义出来就拷贝了
②赋值运算是两个已经初始化了的对象之间的一种行为,如
Date d1(2025,1,1);//d1已初始化
Date d2(2025,8,12);//d2已初始化
d1 = d2;//两个已初始化对象之间的行为

这样看来,我们实现的赋值运算符重载是不是已经达到完美了呢?其实还有进步的地方,请随着我的脚步来改进。

我们上面的代码,已经可以将d2赋值给d1,那么这样的=只能作用于两个对象之间,要是我想实现连续赋值呢?就比如d1 = d2 =d3,我想把d3赋值给d2,然后再把d2赋值给d1怎么办呢?所以我们应该调整我们的代码实现逻辑。

Date & operator(const Date & d)
{
   if(this != &d)//这一步是检验是否自己给自己赋值,就是说假如d2跟d3一样,就无需赋值,this是指向d2的指针,d是d3的别名,那么&d就是指向d3的指针
   {
      //走到之类,说明两者不相等,需要进行赋值操作
      _year = d._year;
      _month = d._month;
      _day = d._day;
   }
   return *this;
}

这里非常的巧妙,我来为大家讲解,这里d2与d3已经完成赋值,d2与d3已经相等,而this又是指向d2的指针,返回*this就相当于返回d2,而且还使用了Date & 返回,因为d2出了作用域不会消失,从而减少了一份拷贝,节约了内存。
在这里插入图片描述

3.其他运算符的重载

咱们一定要明白一个大前提,就是我们重载的这个运算符要有意义,比如说一个日期+天数,那肯定有意义,那要是一个日期+一个日期,那么就没有意义。

为了方便计算,我们需要自己实现一个能获取某个月的具体天数的函数,即下面代码为函数名为Getmonthday的函数

①日期+天数
这里我们需要重载+这个运算符,即类似d1+5,d2+100之类的表达式,下面看代码实现。

class Date
{
public:
   Date operator +(int day)
   {
      Date tmp(*this);
      tmp._day += day;
      while(tmp._day > Getmonthday(tmp._year,tmp._month))
      {  
         tmp._day = tmp._day - Getmonthday(tmp._year,tmp._month);
         ++tmp._month;
         if(tmp._month == 12)
         {
           ++tmp._year;
         }
      }
      return tmp;
   }
   int Getmonthday(int year,int month)
   {
      static int monthdayarray[13] = {-1,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];
      }
   }
private:
   int _year;
   int _month;
   int _day;
}
int main()
{
   Date d1(2025,8,13);
   Date d2 = d1+5;//d2是在d1的基础上多了五天,即,2025,8,18。
}

在上述代码中,我有几点想跟大家讲解一下
1.d1+5之后赋值给d2,所以我们的d1是不改变的,只是把这个表达式的结果赋值给d2,所以我们在重载+这个运算符的时候,要定义一个中间变量“tmp”,这个tmp跟d1是一样的(拷贝构造)让“tmp”去进行运算,然后把运算的结果返回给d2,这样我们的d1就不会受影响。
2.关于这个Getmonthday这个函数,我创建了一个数组,一年有12个月,但是数组的下标是从0开始的,所以0下标随机赋值了一个数,这样就能把下标跟月份对应起来了,直接返回数组下标就能返回该月份的天数。
为什么要用static修饰呢?因为这个函数我们需要经常调用,每次都要创建这个数组的空间,后面又要销毁,会很麻烦且影响效率,所以不妨直接static修饰让数组成为静态成员变量

②日期+=天数
相关的表达式为d2 = d1+=5,这个跟上一个不同的是这次的d1是要改变的,d2 = d1 = d1 + 5。所以我们要对我们的实现逻辑做一个更改。

Date & operator+=(int day)
{
   _day+=day;
   while(_day > Getmonthday(_year,_month))
   {
      _day-= Getmonthday(_year,_month);
      ++_month;
      if(_month == 12)
      {
         ++_year;
      }
   }
   return *this;
}
int main()
{
   Date d1(2025,8,13);
   Date d2 = d1+=5;
   return 0;
}

这里就可以实现**Date &**传引用返回了,因为跟上一个不同的是,上一个tmp是出了作用域就会消失的。这个*this本身就是存在的,代表的是d1。

③前置++与后置++
前置++,是要改变自己的,而后置++是不改变自己的。

int main()
{
  int a = 5;
  int b = a++;
  int c = ++b;
  cout << b << c ;
  return 0;
}

程序运行的结果为 5 6,因为前置++是先++再使用,后置++是先使用再++。
那前置与后置的操作符都是一样的,该如何去区分呢。
C++规定了,在重载后置++是,在函数的参数中参加一个int型形参
重载前置++,Date& operator++()
重载后置++,Date operator++(int)
代码实现如下:

class Date
{
public:
//...
      Date & operator++()
      {
         *this+=1return *this;
      }
      Date operator++(int)
      {
         Date tmp(*this);
         tmp+=1;
         return tmp;
      }
      void Print()
      {
        cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
      }
private:
//....
}
int main()
{
   Date d1(2025,8,13);
   Date d2 = d1++;
   Date d3(2025,8,13);
   Date d4 = ++d3;
   d1.Print();
   d2.Print();
   d3.Print();
   d4.Print();
   return 0;
}

可见,打印的结果为
2025/8/14
2025/8/13
2025/8/14
2025/8/14

4.提示

博主已经教大家如何重载+,+=,++,后面的>,<,-,-=之类的,需要大家自己下去动手实现

总结

本篇知识量比较大,望各位多次学习,多打代码,实践出真知。

评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值