【C++】类和对象(第二篇)(二)

简介: 【C++】类和对象(第二篇)(二)

5.赋值运算符重载

接下来我们要来学习赋值运算符重载,那赋值运算符重载呢是属于运算符重载的,所以在学习之前,我们要先来了解一下C++的运算符重载。

5.1 运算符重载

我们还来看上面实现过的那个日期Date类:

class Date
{
public:
  //构造函数
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

那我们现在用Date类实例化出两个对象:

int main()
{
  Date d1(2023, 4, 13);
  Date d2(2023, 4, 12);
  return 0;
}

现在有两个对象d1,d2,大家思考一个问题,现在我们想比较这两个对象是否相等,要怎么搞?

🆗,那我们是不是可以考虑实现一个函数来判断两个对象是否相等:

bool Equal(Date x1, Date x2)
{
  //...
}

大家看该函数的参数这样写好不好,是不是不太好啊。

这里是传值传参,形参是实参的拷贝,那对象的拷贝还要调用拷贝构造。
所以这里我们是不是可以考虑传引用啊,这样就不用拷贝了,另外呢,这里只是去比较两个对象,我们并不想改变它们,所以是不是再加一个const比较好:

bool Equal(const Date& x1, const Date& x2)
{
  //...
}

写一个函数,这是一种方法。


那C++引入了运算符重载之后呢,就使得我们能够这样去玩:


比较两个日期类对象d1,d2是否相等,直接这样:

d1==d2

但是我们首先要知道自定义类型是不能直接作为这些操作符的操作数的。

不像我们的内置类型可以直接进行加减乘除比较相等这些运算,为什么自定义类型不可以啊?

因为自定义自定义,是不是我们自己写的啊,就比如我们实现的这个日期类,是我们按照自己的想法实现出来的,编译器肯定不知道比较这样两个对象应该怎么做。

而且,有些自定义类型不是进行所有的运算都有意义的,就比如日期类,两个日期对象如果相加,有意义吗,是不是没啥意义啊,如果两个日期相减还有点意义,可以理解为两个日期之间差了多少天。

所以这个是由我们自己决定的,我们觉得它可以进行什么样的运算有意义,然后去实现。


那我们要怎么做才能让我们的自定义类型像这样d1==d2直接进行一些运算和比较呢?


这就需要我们对这些运算符进行重载。


概念

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

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

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


那我们接下来就来练习一下:


上面我们不是相比较两个日期类对象是否相等嘛,那我们就来重载一下==运算符。

根据上面的概念,我们可以写出:

bool operator==(const Date& x1, const Date& x2)
{
}

f149dc853f3144079bef51ed37984420.png

那函数体的实现,即比较的逻辑,其实也很简单:

只要两个对象的三个属性(成员变量)_year,_month,_day全部相同,就说明两个对象相等。

bool operator==(const Date& d1, const Date& d2)
{
  return d1._year == d2._year
    && d1._month == d2._month
    && d1.day == d2.day;
}

这样是不是就行了,但是现在有一个问题:

f3bb3946fb9e4ebf886e99861944c2b1.png

什么原因呢?

因为我们Date类的这3个成员变量是私有的(private),所以在类外面是不能访问的。

那怎么解决?

我们可以在类里写一个Get方法(函数),通过Get方法来访问,或者呢,直接把private访问限定符去掉。

我们这里先把private注释一下:

de05b1b9c19d4f01baf920dd6be6fc35.png

然后就不报错了。

那重载好,我们就可以直接用了:a5d79903e6fc419e9563b55d5540863d.png

当然,我们也可以像普通函数那样去调用:

7b381866c85649f4ba05ecd58efb7575.png

当然正常情况下我们不会像普通函数那样去调用,因为我们重载就是为了可以直接d1 == d2这样用。

所以我们直接写成这样就行:

d1 == d2

剩下的工作就由编译器去做,编译器看到这样的代码,就会去看你有没有重载,如果进行了重载,就会转化成去调用这个函数operator==(d1, d2)。


那我们可以打印一下这个结果:

cout << d1 == d2 << endl;
cout << operator==(d1, d2) << endl;

但是我们会发现又报错球了:

1b77d4a8c4b6495fb3a3ae93417d8eb8.png

cout << d1 == d2 << endl;这一句报错了。

什么原因呢?

🆗,是因为这里<<的优先级比==高,所以加个括号就行了:

int main()
{
  Date d1(2023, 4, 13);
  Date d2(2023, 4, 12);
  cout << (d1 == d2) << endl;
  cout << operator==(d1, d2) << endl;
  return 0;
}

feca23245ac44c3aab1a031552aab084.png

0为假,而这两个对象也确实是不相等的。


那我们就把==重载好了,但是:


刚才我们是直接重载到了全局,我们把成员变量变成了共有的才能这样的。

那么问题又来了:我们把成员变量全部公有了,封装性又如何体现呢?

那当然是有办法解决的,我们刚才上面已经提了一种,就是提供一些共有的get方法,那除此之外呢,我们还可以用友元函数解决,但是我们还没学,而且不推荐用这个。

所以这里比较好的一种方法是:

我们直接重载到类里面,即重载成成员函数。


但是呢?我们直接把它放到类里面的话:

a41ae36e67e64ba8966c55bfa5532a71.png

嗯???又报错了,说此运算符函数的参数太多。


怎么回事啊?


🆗,这里我们重载的是==运算符,正常情况下只有两个操作数,所以只需要两个参数就够了。

那大家可能会疑惑了,这里不就是两个参数嘛?

那大家不要忘了,这里是不是还有一个隐藏参数啊。

什么隐藏参数,是不是就是this指针啊。

这是不是我们上一篇文章学习的知识啊。

C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象)

所以我们这里只需给一个参数就够了。

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

7f500144d66d4ed2beb1d69e137c6bbc.png

那调用的时候,this指针接收d1的地址,形参d就是d2(引用传参)。


注意

下面我们一起来看一下,在运算符重载这一块,需要注意的一些内容:


不能连接其他符号来创建新的操作符:比如operator@

重载操作符至少有一个类类型的参数

用于内置类型的运算符,其含义不能重载改变,例如:内置的整型+,不能改变其含义

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

.* :: sizeof ?: .注意这5个运算符不能重载,这个经常在笔试选择题中出现。

.*其中这个运算符大家可能都没见过也没用过,没关系,大家可以记一下就行了。

练习

那上面我们对==运算符进行了重载,接下来我们再来练习几个。


那就还是上面那个日期类,现在我们来尝试重载一下<好吧:


那其实逻辑也不难,就是判断两个日期的大小嘛。

我们可以只判断小于的情况返回true,其它情况一律false:

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

就是这样嘛。

测试一下:b24eb26191db4ca19e95fab3fffc063e.png

没毛病。

那我们再重载一下日期类的>吧:

那是不是很简单啊,那<里面的<换成>就行了嘛:

fd1dd2c9a6624f8e8eb9da553641d9f3.png

这样当然是可以的。

但是呢,我们可能还会实现大于等于,小于等于…


所以呢,接下来给大家说一个简单的方法,对所有的类都适用:


怎么做呢?

🆗,我们现在是不是已经重载了==和<了,那现在我在想去重载什么大于,大于小于之类的,其实根本没必要再自己写了,可以直接复用我们写好的那两个。


那我们现在想重载>的话,其实可以考虑先重载<=:


那怎么复用:

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

这样搞是不是很爽啊。

5.2 赋值重载

赋值运算符重载呢 是属于运算符重载的一种,但是,它还是我们类的6个默认成员函数的其中一个。

实现

那我们就先来重载一下赋值=运算符吧:

那经过了刚才的学习,重载一个=,是不是简简单单啊。

//d1=d2(this就是d1,d就是d2)
  void operator=(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }

这是不是就好了啊,测试一下:

baa068e4670848e39be9734625184d1d.png

可以完成赋值。


但是呢,我们当前的这个实现还有一些缺陷:


什么缺陷呢?

大家回忆一下,我们之前用内置类型进行赋值操作时是不是支持像这样的连续赋值啊:

i = j = k;

这句代码怎么执行的,是不是从右向左啊,先把k赋给j,然后再把表达式 j = k的结果,就是k赋给i。

当然还可以连续的更多。

而对于我们刚才对日期类重载的=,可以支持连续赋值吗:

c95ad5ffc54f4e949da2c3c937ce4a0f.png

额,是不行的,这里直接报错了。

那这里为啥报错了啊:

c8196cb5d75f4cb5a16a98eda3d1e437.png

因为正常情况下d2赋给d1是不是应该有一个结果啊,然后把这个结果再赋给d3。
但是我们这里d1 = d2是不是调了我们重载的函数,而我们上面实现的函数并没有返回值。

所以我们要加一个返回值来支持连续赋值:

那我们返回的话是不是还是返回对象的引用比较好啊:

//d1=d2(this就是d1,d就是d2)
  Date& operator=(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
    return *this;
  }

48efcb146a3f4e33b92dfadfabfabf03.png

那这下我们的连续赋值就可以了。

但是有时候呢不排除有人可能会写出这样的代码:

6978a41f5524492e8875a45c7c5aab1f.png

把自己赋给自己。

这样可以吗?

可以当然是可以的,但是它调用函数是不是白白进行了一次拷贝啊,所以呢,我们一般还会加一点东西:

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

加一个判断,如果它们是同一个对象,就不用进行拷贝了。


🆗,那我们来简单总结一下赋值运算符重载:


参数类型:const 类对象的引用,传递引用可以提高传参效率

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

最好检测一下是否是自己给自己赋值,并进行一下处理

返回*this:返回的结果用于支持连续赋值


那我们说了赋值运算符重载是属于6个类默认成员函数的其中一个,所以它还有一些属于自己的特性。


赋值重载的特性

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

注意:默认生成的赋值重载对于内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用其对应类的赋值运算符重载完成赋值。


那有了这个特性的话,对于我们上面的日期类,我们还需要自己写赋值重载吗?


是不是不用啊,用编译器自动生成的是不是就可以完成啊。

因为日期类的成员变量是不是都是内置类型啊,而且赋值不涉及深拷贝的问题,浅拷贝就可以完成。


那我们试一下,把我们自己写的赋值重载注释掉:

ea8b4bc699294915820dcbd62fed13f0.png

然后运行:f617a5f5fe9747828aa0dddde5c9ab32.png

是不是可以啊。


那这里的问题是不是就和拷贝构造一样了:


编译器生成的默认赋值运算符重载函数已经可以完成浅拷贝赋值了,所以像日期类这样的我们就没必要自己实现赋值重载了,因为默认生成的就可以帮我们搞定了。

那同样,如果涉及深拷贝的问题,像栈Stack这样的类,是不是就得我们自己实现去完成深拷贝了。

和拷贝构造一样,如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要自己实现。


然后我们再来看一个代码:

34018717e4b44580b3aa8c906cbd30b3.png

大家看这里会调用拷贝构造还是赋值重载?

这里是不是拷贝构造啊,这个我们上见过的嘛:

988db2c056754566a3260cb0283fb49d.png

那为啥这里用了赋值=,但是是拷贝构造呢?


🆗,我们来简单总结一下:


什么时候是调赋值重载呢?

是我们用已经实例化出来的对象进行相互赋值的时候,调用赋值重载。

而当我们用一个已经实例化出来的对象去初始化一个新对象的时候,调的是拷贝构造。


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

我们上面重载的一些什么等于、大于、小于、大于等于之类的运算符是不是可以重载到类外也可以重载到类里面啊。

那赋值重载也是运算符重载,我们刚才是定义在类里面的,那它可以重载到外面吗?


我们试一下:


先把成员变量的private注释掉,确保在类外能访问。

然后我们在类外实现一下赋值重载:

Date& operator=(Date& left, const Date& right)
{
  if (&left != &right)
  {
    left._year = right._year;
    left._month = right._month;
    left._day = right._day;
  }
  return left;
}

重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数

那这就实现好了。

行不行呢:

38e380f8ae9341f0a0e3d9374c78fe04.png

还没运行直接就看到报错了,说必须是成员函数。

为什么这样不行呢?解释一下:

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

7a42f926752f475f8ca7f04b8d8de2f2.png

6. const成员函数

我们来看这样一个类:

class A
{
public:
  void Print()
  {
    cout << _a << endl;
  }
private:
  int _a = 10;
};

然后:

int main()
{
  A a;
  a.Print();
  return 0;
}

定义一个对象a,并调用成员函数Print。

f2466beea279447f8bd92625f69aabef.png

没有什么问题。

那这样呢?

50488cb2c7194c8a97f2c1b9bf9533ac.png

加一个const修饰对象a。

8c5a1ed7f2e242f8979e2ae46b8999e6.png

然后我们发现调用Print就出错了。


那为什么呢?


其实呢是因为这里存在了一个权限放大的问题。

这也是我们之前学习过的:对于引用,还有指针来说,对它们进行赋值和初始化时,权限可以缩小,但不能放大。

我们来分析一下:

对于我们的成员函数Print,虽然看起来没有参数,但是是不是有一个隐藏参数,就是我们熟悉的this指针嘛。

那this指针的类型是啥?

this指针的类型:类类型* const

那对于当前这个类来说就是A* const this,const 修饰的是指针this,即指针this不能被修改,但this指向的内容可以被修改。

那我们传过来的参数是啥,是调用函数的对象的地址,即a的地址,但我们的对象a是const修饰的,所以传过来的地址的是const A* &a,const修饰的是该地址指向的内容,即对象a不能被修改。

那这样的话,传给this,this可以修改其指向的内容即对象a,所以就是权限放大了。

所以这里报错了。


那怎么解决呢?


🆗,如果我们可以把this指针的类型也变成const A*是不是就可以了啊。

但是this指针的类型是我们想改变就能改变的吗?

this指针是类成员函数中的一个隐藏参数,我们是没法直接改变它的。

那就没有办法了吗?

办法肯定是有的:cb56b0df3e2c436ab34737d46e77174a.png

我们只需在对应成员函数的括号后面加一个const 就行了。

这就是我们要学的const成员函数:

const修饰的“成员函数”称之为const成员函数。

const修饰类成员函数,实际修饰的是*this,这样this指向的对象将不能被修改。


9c828125108249669be1d1b7fa7c9b94.png

那这样this指针的类型就也变成了const A*了,这样就可以传了。


0d548dc8fe0045e1ad09bd84d9b83967.png

但是我们平时定义一个对象好像一般也不会在前面加一个const,那这个用处是不是不大啊?

🆗,虽然定义对象时我们一般不加const,但是我们是不是可能经常会这样搞:

void Func(const A& x)
{
  x.Print();
}

首先这里传引用与传值相比减少拷贝,然后如果我们不想对象被改变的话,不是一般会加一个const嘛。

那当前这种情况:

class A
{
public:
  void Print()
  {
    cout << _a << endl;
  }
private:
  int _a = 10;
};
void Func(const A& x)
{
  x.Print();
}
int main()
{
  A a;
  Func(a);
  return 0;
}

x是a的引用(别名),a没有被const修饰,然后在Func里,x是被cosnt修饰的,x去调用Print,这里是不是也是权限放大了。

1de4094b6e4d40dc9d4d51f1a24d92f7.png

089c4297970b4c6dac789974783a4116.png

那这是不是跟我们开始讲的那个例子一样啊,怎么解决?
把Print变成const成员函数就行了:

63b1bf9f3e6646bdab0ab36299e7c13e.png像这种情况其实还是比较常见的。


所以说:


对于类的成员函数,如果在成员函数内部不需要改变调用它的对象,最好呢都可以把它写成const成员函数。

另外,如果const成员函数的声明和定义是分开的,声明和定义都要加const。


7. 取地址及const取地址操作符重载

类的6个成员函数呢,比较重要的前4个我已经学完了,最后还剩两个。


我们一起来看一下:


那剩下的两个默认成员函数呢都是取地址重载,包括对普通对象的取地址和对const对象取地址。

这两个默认成员函数呢一般不需要我们自己去实现,编译器会自动生成,绝大多数情况下我们用编译器自动生成的就行了。


我们可以试一下:


对普通对象取地址7891776153ff43b296c1346f7b266526.png

对const对象取地址

9ab5d58cc848411fab4edf418c70521f.png

所以这两个默认成员函数一般不需要我们自己写,用编译器默认生成的取地址的重载即可

但是,如果你想自己去重载一下的话当然也是可以的:


9e643bb1eddc40739142cf7292efde11.png

13398d2776ff41f1bbfb40f2a06087a2.png你可以自己指定一个地址返回。

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

🆗,那我们这篇文章的内容就先到这里,欢迎大家指正!!!

e172fe2629c048b9ab2f75d8eb9db931.png



目录
相关文章
|
1天前
|
编译器 C语言 C++
|
1天前
|
编译器 C++
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
【C++】详解初始化列表,隐式类型转化,类静态成员,友元
|
4天前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
4天前
|
编译器 C++
【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 )
本文探讨了C++中类的成员函数,特别是取地址及const取地址操作符重载,通常无需重载,但展示了如何自定义以适应特定需求。接着讨论了构造函数的重要性,尤其是使用初始化列表来高效地初始化类的成员,包括对象成员、引用和const成员。初始化列表确保在对象创建时正确赋值,并遵循特定的执行顺序。
|
4天前
|
C语言 C++
【C++】日期类Date(详解)③
该文介绍了C++中直接相减法计算两个日期之间差值的方法,包括确定max和min、按年计算天数、日期矫正及计算差值。同时,文章讲解了const成员函数,用于不修改类成员的函数,并给出了`GetMonthDay`和`CheckDate`的const版本。此外,讨论了流插入和流提取的重载,需在类外部定义以符合内置类型输入输出习惯,并介绍了友元机制,允许非成员函数访问类的私有成员。全文旨在深化对运算符重载、const成员和流操作的理解。
|
4天前
|
C++
【C++】日期类Date(详解)②
- `-=`通过复用`+=`实现,`Date operator-(int day)`则通过创建副本并调用`-=`。 - 前置`++`和后置`++`同样使用重载,类似地,前置`--`和后置`--`也复用了`+=`和`-=1`。 - 比较运算符重载如`&gt;`, `==`, `&lt;`, `&lt;=`, `!=`,通常只需实现两个,其他可通过复合逻辑得出。 - `Date`减`Date`返回天数,通过迭代较小日期直到与较大日期相等,记录步数和符号。 ``` 这是236个字符的摘要,符合240字符以内的要求,涵盖了日期类中运算符重载的主要实现。
|
4天前
|
定位技术 C语言 C++
C++】日期类Date(详解)①
这篇教程讲解了如何使用C++实现一个日期类`Date`,涵盖操作符重载、拷贝构造、赋值运算符及友元函数。类包含年、月、日私有成员,提供合法性检查、获取某月天数、日期加减运算、比较运算符等功能。示例代码包括`GetMonthDay`、`CheckDate`、构造函数、拷贝构造函数、赋值运算符和相关运算符重载的实现。
|
4天前
|
编译器 C++
【C++】类和对象③(类的默认成员函数:赋值运算符重载)
在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
|
4天前
|
存储 编译器 C++
【C++】类和对象③(类的默认成员函数:拷贝构造函数)
本文探讨了C++中拷贝构造函数和赋值运算符重载的重要性。拷贝构造函数用于创建与已有对象相同的新对象,尤其在类涉及资源管理时需谨慎处理,以防止浅拷贝导致的问题。默认拷贝构造函数进行字节级复制,可能导致资源重复释放。例子展示了未正确实现拷贝构造函数时可能导致的无限递归。此外,文章提到了拷贝构造函数的常见应用场景,如函数参数、返回值和对象初始化,并指出类对象在赋值或作为函数参数时会隐式调用拷贝构造。
|
4天前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。