【C++初阶】类和对象(二)(下)

简介: 【C++初阶】类和对象(二)(下)

5.运算符重载

5.1 规则

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

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2023, 2, 3);
  Date d2(2023, 3, 4);
  return 0;
}

创建 d1 和 d2 两个对象,时间分别为 2023.2.3 和 2023.3.4 ,如何比较它们的天数的先后,或者是算它们中间相差的天数?


对于平常情况下,我们可以手算,但是对于计算机呢?如何直接用运算符来比较?可不可以直接比较它们的先后?可以不可以让 d2 的天数直接减去 d1 的天数?例如 d1 > d2 ,d2 - d1 ?


在 C++ 中,只支持对内置类型的运算符计算,并不支持直接对自定义类型进行运算符计算,但是通过运算符重载可以达到这个效果 。


运算符重载:


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

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


返回值与参数:


对于返回值:不同的运算符重载函数,返回值是不同的,例如 > 就是 bool 类型;- 就是 int 类型

对于参数操作数有几个操作符,就有几个操作数


对于一个比较大小运算符重载的参数需要注意几点:

bool operator>(const Date& d1, const Date& d2);

重载的是 > ,1.对于这个符号有两个操作数;2.参数写成引用的形式,减少空间的消耗;3.加上 const 修饰,也防止这两个对象由于引用的原因被修改。

接下来,我们写出这个运算符重载:image.png但是这迎来一个问题 :运算符重载在类外部访问不到成员变量,这样就使得运算符重载不起作用了。那么我们就要相除解决方案。

方法 1(可以但不推荐):

去掉 Date 类中成员变量的 private 的私有,我们先用了再说:image.png这时,调试,然后到 d1 < d2 ,f11 进入运算符重载,对于这边的调用有两种方式:image.png编译器看到自定义类型 d1 < d2 去找是否重载了运算符,如果找到了,就转换为 operator< (d1, d2) 调用函数,找不到就 报错


方法 2:


方法 1破坏了封装性,所以不太可行。所以可以写在类的里面 ,但是这里会有一个问题:


写成成员函数时,成员函数默认会有一个 this 指针,但是对于运算符重载的参数个数等于操作数个数,<有两个,现在就有三个了,所以错了。


例如如果直接调用 operaror< 函数时:

Date d1;
d1.operator<(d2);

d1 作为 this 指针被传过去,d2 被作为另一个参数,但是这里对于我们当前的函数来说,有三个参数:this, d1, d2 ,参数不匹配了!

所以需要修改:

bool operator>(const Date& d) 
{
  if (_year > d._year)
  {
    return true;
  }
  else if (_year == d._year)
  {
    if (_month > d._month)
    {
      return true;
    }
    else if (_month == d._month)
    {
      if (_day > d._day)
      {
        return true;
      }
    }
  }
  return false;
}

这时 d1 > d2 ,会先去到类的成员里面找,找到了转换为 d1.operator>(d2),若类中没找到,再到全局找,找到转换为 operator>(d1, d2) ,进行调用。


对于上面两种 operator> 全局的和类中的可以同时存在(参数不同构成重载),若同时存在,则调用类中成员的,和就近原则没什么关系,取决于编译器的实现机制;但是这种情况一般不存在,因为成员变量一般都是 private 私有的,全局的并没有用。


对于运算符重载的规则:


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

重载操作符必须有一个类类型参数

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

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

.*(没用过,类似于访问解引用,重点记,选择题容易考) ::(作用域访问符) sizeof ?:(三目运算符) .(访问自定义类型) 注意以上5个运算符不能重载。经常在笔试选择题中出现。

返回值类型不一定是内置类型,例如 Date operate++() 返回值是日期

运算符重载的返回值必须写


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

5.2 赋值运算符重载

赋值运算符重载是对于两个已经存在对象之间的赋值拷贝,本质是一个运算符重载函数。

231794a5b84d4450bc59fcdb3520465c.png简单写一下:

// d1 = d3
void operator=(const Date& d)
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

可以完成功能,但是不符合连续赋值的含义:例如 i = j = k = 10 ,是将 10 赋给 k ,k 赋给 j ,j 赋给 i ,最后赋值后的返回值没有变量接收,就把值丢弃,通过这种方式 完成连续赋值 。

但是下图呢?55eb81712a424cd6815c870ad31413e0.png由于 operator= 没有返回值,为void,这样就不可行了,所以需要修改 :

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

根据上方的连续赋值规则,返回的应该是 左操作数 ,即 d1 ,也就是 this 指针指向的对象,即 *this ;但是对于 *this 的返回这里由于是传值返回,而 *this 指向的是一个对象,所以会调用拷贝构造函数

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  Date(const Date& d)
  {
    cout << "进行拷贝构造" << endl;
  }
  // d1 = d3
  Date operator=(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
    return *this; // 调用拷贝构造
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2023, 2, 3);
  Date d2(2023, 3, 4);
  Date d3(2022, 2, 3);
  d3 = d2 = d1; // 两次赋值,共计两次拷贝构造
  return 0;
}

因此返回时,最好使用 引用返回 来提高返回效率,因为出作用域 *this 仍然存在,所以引用返回完全没问题。

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

此刻不进行拷贝构造

但是还不是最终版本,因为可能会写错为 d1 = d1 ,自己给自己赋值 ,虽然代码并没有问题,但是浪费了赋值的过程,所以可以再加一个检查,形成最终版本 :

Date& operator=(const Date& d)
{
    if (this != &d) // this 和 d 的地址相等 
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    return *this;
}

总结一下赋值运算符重载格式 :


参数类型:const T&,传递引用可以提高传参效率

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

检测是否自己给自己赋值

返回*this :要符合连续赋值的含义

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


编译器默认生成的赋值重载,跟拷贝构造做的事情完全类似 :

内置类型成员,会完成字节序1值拷贝(浅拷贝)

自定义类型成员,会调用它的 operator= 赋值重载image.png补充:

Date d2 = d1; // 拷贝构造 or 赋值重载?
• 1

两个已经存在的对象才是赋值重载,d2 并不存在,所以是拷贝构造

总结 :

以上默认生成的四个默认成员函数,不能在类外面直接定义为全局,否则会与自动生成的默认成员函数冲突,但可以类内申明,类外定义。析构和构造处理机制基本类似;拷贝构造和复制重载处理机制基本类似。


构造和析构:


内置类型不做处理

自定义类型会调用对应的构造/析构

拷贝构造和赋值重载:


内置类型完成浅拷贝,按字节序拷贝

自定义类型去调用它的拷贝构造和赋值重载

6.总结:

今天我们认识并具体学习了类和对象的默认成员函数,分别为:构造函数、析构函数、拷贝构造函数、赋值操作符重载的知识。接下来,我们将继续学习类和对象的相关知识。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

c3ad96b16d2e46119dd2b9357f295e3f.jpg

相关文章
|
25天前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
4天前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
34 16
|
7天前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
50 6
|
1月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
25天前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
25天前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
2月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
85 19
|
2月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
79 13
|
2月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
68 5
|
2月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
52 5