【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++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
3月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
89 0
|
3月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
169 0
|
5月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
176 12
|
6月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
128 16
|
6月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
6月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
6月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
345 6
|
6月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
7月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)