【C++】类和对象(中)(2)

简介: 【C++】类和对象(中)(2)

operater==

22e1f885423e41abbab88d310c53b56d.png


注: == 的优先级比 << 的优先级低。


上图的operator==函数就是比较两个日期是否相等的函数。如果该函数不再类中定义的话,那么就需要将成员变量改成公有public。如果这样子做的话,那封装的意义就不存在了。


如果我们既想要运算符重载,又想成员变量为私有private。那如何解决呢?这时候我们可以借助友元(类和对象下的内容)或者在类中定义一个辅助的函数(Java经常使用这种方式),还可以将运算符重载定义在类中了。在这里,我们采用将运算符重载定义在类中这种方式。那operator==如何定义呢?见下图代码:


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


aef9f452f9dc4b38918114e11fbcd3ba.png


看到上面的代码,可能就会有细心的小伙伴发现,怎么operator==的参数只有一个,是不是写错了呀。其实并没有写错,因为每个成员函数会有一个隐藏参数this,该参数占据成员函数的第一个参数的位置。


注:编译非常的智能:如果我们运算符重载在类中定义了,编译器就不会去全局中找;如果运算符重载没有在类中定义,那么编译器就会去全局中找。


operator>


有时候,我们需要比较两个日期的大小,那我们来看一下operator>的代码。


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


operator>=


比较一个日期 d1 是否大于或等于另外一个日期 d2,那么就可以赋用上面的operator==operator>


operator>=
比较一个日期 d1 是否大于或等于另外一个日期 d2,那么就可以赋用上面的operator==和operator>。


注:*this 就是日期 d1。


operator<=


operator<=重载运算符也可以赋用operator>运算符,因为operator<=operator>的反面,那我们一起来看一下代码。


//..
bool operator<=(const Date& d)
{
  return !(*this > d);
}
//...


operator<


因为operator<operator>=的反面,所以可以赋用operator>=


//...
bool operator<(const Date& d)
{
  return !(*this >= d);
}
//...


operator!=


因为operator!=operator==的反面,所以可以赋用operator==


//...
bool operator!=(const Date & d)
{
  return !(*this == d);
}
//...


operator+= 和 operator+


如果我们想算一个某一天的 N 天后是哪一天,这时候就需要借助operator+=或者operator+。注意,operator+=operator+的返回值为Date。日期的加法是比较复杂的,因为每个月有多少天是没有规律的。


f64868b76bef428abb9245493d0779ce.png


因为每个月的天数是没有规律的,所以我们就写一个函数来得到每个月的天数,然后再进行日期的加法。


获取每个月的天数


//...
// 获取每个月的天数
int GetMonthDay(int year, int month)
{
  // static修饰数组避免频繁创建
  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];
  }
}
//...


operator+=


//...
Date& operator+=(int day)
{
  // 处理 day < 0的情况
  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;
}
//...


注:+=运算符会修改变量的值,而且出了函数的作用域,+=后的对象还存在,所以operator+=的函数返回值为Date&。注:如果是值返回也会存在拷贝。


operator+


因为+运算符不会影响变量的值,所以我们借助拷贝构造来创建一个对象ret,然后赋用operator+=重载运算符让ret += day,最后将ret返回。注意:出了函数作用域,ret就不存在了,所以operator+的返回值为Date,不能是Date&。


//...
Date operator+(int day)
{
  Date ret(*this);
  ret += day;
  return ret;
}
//...


operator-= 和 operator-


operator-=


//...
Date& operator-=(int day)
{
  // 处理 day < 0的情况
  if (day < 0)
  {
    return *this += -day;
  }
  _day -= day;
  while (_day <= 0)
  {
    --_month;
    if (_month == 0)
    {
      --_year;
      _month = 12;
    }
    _day += GetMonthDay(_year, _month);
  }
  return *this;
}
//...


f61c2f91a5a249ccb87eddbc5deccaa4.png


operator-


Date operator-(int day)
{
  Date ret(*this);
  ret -= day;
  return ret;
}

2a7148b4852b420784f3ab0d1cd095b3.png


前置++ 和后置++ 重载


前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确的函数重载。C++规定:后置++重载时多增加一个int类型的参数来区分前置++和后置++,但调用函数时该参数不用传递,编译器自动传递。


前置++


前置++:返回+1之后的结果。注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率。


//...
// 前置++
Date& operator++()
{
  *this += 1;
  return *this;
}
//...


后置++


注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将*this保存一份,然后给*this+1。而 tmp 是临时对象,因此只能以值的方式返回,不能返回引用。


3832ccbccf6f4d8a80335604e8ca6c6d.png


注:对于内置类型,使用前置++或后置++的区别不大;但对于自定义类型需要++时,建议使用前置++,因为使用后置++会多两次拷贝构造。


前置-- 和后置-- 重载


前置–


//...
// 前置--
Date operator--()
{
  *this -= 1;
  return *this;
}
//...


后置–


//...
// 后置--
Date operator--(int)
{
  Date tmp(*this);
  *this -= 1;
  return tmp;
}
//...


1a1ded54de77427c91342e7355d26acd.png


日期 - 日期


//...
// 日期 - 日期
int operator-(const Date& d)
{
  Date max = *this;
  Date min = d;
  int flag = 1;
  // 注意:不要写出 d > *this
  // 这样子写涉及引用权限的放大和缩小
  // 后面的内容会讲解这个知识点
  if (*this < d)
  {
    max = d;
    min = *this;
    flag = -1;
  }
  int n = 0;
  while (min != max)
  {
    ++min;
    ++n;
  }
  return n * flag;
}
//...

e34dbb142ecc46d08674915c18d19e48.png

operator<< 和 operator>>


现在日期类的功能已经实现了差不多了,现在就还差cin >>cout <<的功能了。我们现在把这两个功能实现一下,不过这个知识点相对来说比较难。不过也不要太担心,有我在呢!


学习这个之前,我们需要知道一些前置知识。cin是头文件istream里的对象,而cout是头文件ostream里的对象。>>是流提取运算符,而<<是流插入运算符。istream和ostream也是类。

d6848cd6d5934b09b35cf2fa361c35f6.png


知道了这些,我想问大家一个问题:为什么cin和cout能够自动识别类型呢?其实这背后的原理就是函数重载和运算符重载。如下图所示:


e24a0f6cba344ce5860c83fb77741545.png


f080a353d3914312a9d9c576eabd61f3.png

operator<<


cincout默认就支持内置类型的函数重载和运算符重载,而不支持自定义类型的函数重载和运算符重载。这时候,就要发挥我们智慧的大脑了,自己动手丰衣足食。


//...
// d1 << cout
void operator<<(ostream& out)
{
  out << _year << "年" << _month << "月" << _day << "日" << endl;
}
//...


446acc844aa44bb899c99cce858bc620.png


上面operator<<虽然可以实现输出日期的功能,但是和我们的使用习惯相反且不具有可读性。所以一般情况下,流提取重载和流插入重载都不会定义在类中。那怎么解决这个问题呢?我们可以将operator<<定义成全局函数。这样又会带来应该问题,就是封装的问题。如果我们将operator<<定义成全局函数,就需要将成员变量的属性改成公有public。那我们先试试先吧,实现出来再看看还有没有更好的办法。


//...
void operator<<(ostream& out, const Date& d)
{
  out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
//...

9bcf7c4796574655b5b072ae7552bf77.png


这个operator<<的写法还有可以优化的地方。因为这个写法不能输出多个日期,那么将函数的返回改成ostream&就可以了。


//...
ostream& operator<<(ostream& out, const Date& d)
{
  out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
  return out;
}
//...

4363d261905d43beac9e814725da408b.png


虽然operator<<这样子写解决了可读性和使用习惯的问题,但是又带来了更大的问题——封装的问题。那怎么解决呢?接下来,我们的友元就要上场表演了。


注:如果不借助友元,operator>>也会出现上述的问题。


友元函数


友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。 通俗来讲,就是声明这个函数是友好的,不会直接修改成员变量的值。


注:友元函数将会在类和对象下详细讲解。友元声明可以在类中的任意位置。



//...
class Date
{
  //友元声明  
  friend ostream& operator<<(ostream& out, const Date& d);
  //...
}
// cout << d1  operator<<(cout, d1)
inline ostream& operator<<(ostream& out, const Date& d)
{
  out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
  return out;
}


有了友元函数,就算类的成员变量的属性为私有private,也可以在类外访问类的成员变量了。

a3d43beba9c347e7b0aa8331cbf73a1c.png



注:operator<<运算符重载很有可能会经常被调用,那么我们可以将它改成内联函数。友元声明时不需要加上inline,定义的时候需要加上inline。


有了operator<<运算符重载,那么成员函数Print也就可以退休了。关于operator<<运算符重载的知识点就这些了,我们现在来学习一下operator>>运算符重载。


operator>>


//...
Date
{
  //友元声明
  friend istream& operator>>(istream& in, Date& d);
  //...
}
// cin >> d1 operator>>(cin, d1)
inline istream& operator>>(istream& in, Date& d)
{
  in >> d._year >> d._month >> d._day;
  return in;
}

0b66f049c7a747e0bec8d0fdd8703f76.png


注:有关operator>>的知识点和operator<<的知识点相似。


👉赋值运算符重载👈


operator=运算符为赋值运算符重载,那我们来看一下赋值运算符重载的格式。


赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义


注:赋值运算符重载既是默认成员函数,又是运算符重载。


//...
Date& operator=(const Date& d)
{
  if(this != &d) // 避免 d1 = d1 的情况
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  return *this; // 返回左操作数
}
//...


fb777b8642e6447780b23eaf6d81a768.png


注:赋值运算符重载的参数也可以是类对象,但传参的时候需要调用拷贝构造函数。返回值为类对象的引用,既可以提高返回的效率(值返回时会调用拷贝构造函数),又可以实现连续赋值。赋值运算符重载的引用返回一般不用 const 修饰,因为有可能该对象还要修改。


赋值运算符重载是一个默认成员函数,如果自己不写编译器会自动生成。那我们现在就不写赋值运算符重载,看看会有什么情况发生。


ce82261f996b49409aefc182fea3576d.png


将赋值运算符重载屏蔽后,再将程序运行起来,我们可以发现还是可以完成赋值的。那为什么呢?其实是,如果我们不写赋值运算符重载,对于内置类型会完成值拷贝,对于自定义类型会调用该自定义类型的运算符重载。 所以如果我们不写日期类的赋值运算符重载,也能完成拷贝。


那如果栈Stack和队列MyQueue不写赋值运算符重载,又会发生什么呢?

0b6691696a2540c68c97399482f8c967.png


上面的栈Stack还没有写赋值运算符重载,然后运行起来就崩溃了。因为栈Stack的成员都是内置类型,那么编译器生成的赋值运算符重载会完成值拷贝。那么赋值过后,st1 和 st2就都执行了同一块空间。到了析构的时候,就会对同一块空间析构多次,然后程序就崩溃了。而且还会带来一个很严重的问题就是内存泄漏。


08b1722fefea49528f292aab515f9b75.png


9340e02672d7487781c8dfd6dfb111a0.png


对于栈Stack来说,编译器默认生成的赋值运算符重载不能用,那么就需要我们自己写了。因为 st1 和 st2 的空间大小情况不清楚,所以我们先把 st1 原来的空间先释放掉,再申请一块和 st2 一样大的空间,然后再把数据拷贝过去。


2642a51daf364a2ebbad4199442605b9.png

//...
Stack& operator=(const Stack& st)
{
  if(this != &st) // 避免 st1 = st1 的情况
  {
    free(_a);
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (_a == nullptr)
    {
      perror("malloc fail");
      exit(-1);
    }
    memcpy(_a, st._a, sizeof(int) * st._top);
    _top = st._top;
    _capacity = st._capacity;
  }
  return *this;
}
//...

b6b1a72dafad4390ae7d3b95f9d45aae.png


日期类不需要自己写赋值运算符重载,栈Stack需要自己写赋值运算符重载,那队列MyQueue需不需要自己写呢?我们一起来看一下。


e7eb3724a129474ba963882855da5120.png


可以看到,队列MyQueue也不需要写赋值运算符重载。那我们来总结一下什么类需要写赋值运算符重载。像拷贝构造函数一样,如果该类需要写析构函数,那么就需要写赋值运算符重载;如果该类不需要写析构函数,那么就不需要写赋值运算符重载。


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


628475a8377640d19b2c74e5197233b3.png

cee0439a9fbf499fa63f1d29d2418e57.png


👉const 成员函数👈


将const修饰的成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。const 修饰成员函数时,只会修饰 this 指针,并不会修饰成员函数的其它参数。


0b2c673b784e4edebda343988ef40287.png


知道了const修饰成员函数,我们现在来看一个例子:


2d58fdb589814f20974ee6642d4d2589.png


可以看到,当用 const 修饰一个日期时,该日期就不能再修改了。当 d2 调用Print函数时, 编译器会将 d2 的地址转换成 this 指针,该 this 指针的类型为Date* const,相当于 this 不能被修改,但是 this 指针指向的空间里的内容可以修改。又因为 d2 用了const修饰,&d2 的类型是const Date*,所以这就涉及指针权限的权限放大和缩小。


0f66ff0d89344f13b4f8fad7c88bcc62.png


那如何解决呢?就是在Print函数后面加上个const关键字修饰。

920639fed309497aa76fdeb649cdf034.png


类中的成员函数的参数很多都需要用const修饰,也不会修改 this 指针指向的内容,所以很多成员函数都需要用const来修饰。那么什么成员函数要用const修饰呢?不会修改 this 指针指向的内容的成员函数就需要用const来修饰。大家可以给以上写的成员函数加上const。


给大家留几个问题:


  1. const对象可以调用非const成员函数吗?
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数内可以调用其它的非const成员函数吗?
  4. 非const成员函数内可以调用其它的const成员函数吗?


👉取地址及const取地址操作符重载👈


这两个默认成员函数一般不用重新定义 ,编译器默认会生成。不过,我们也把这两个函数实现一下。


//...
Date* operator&()
{
  return this;
}
const Date* operator&() const
{
  return this;
}
//...

4ad94bc779e74cabb46ec10493e688b1.png


如果这两个函数不写也没有什么问题,编译器生成也够用。如果你不想让别人拿到类对象的地址就可以像下面这样写。


Date* operator&()
{
  return nullptr;
}
const Date* operator&() const
{
  return nullptr;
}
//...


fea1fdb46b9d44c2aab2f3aa5c5500bc.png



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


👉总结👈


本篇博客主要讲解了构造函数、析构函数、拷贝构造函数、赋值运算符重载、运算符重载、const修饰成员函数以及取地址及const取地址操作符重载,这些内容是学习后面内容的基础,希望大家能够掌握。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️

相关文章
|
7天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
21 2
|
13天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
40 5
|
19天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
49 4
|
20天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
46 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1
|
2月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
17 0
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)