【C++】类和对象 (中篇)(3)

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

五、运算符重载

1、运算符重载的引入

对于C/C++编译器来说,它知道内置类型的运算规则,比如整形+整形、指针+整形、浮点型+整形;但是它不知道自定义类型的运算规则,比如日期+天数 、日期直接比较大小、日期-日期;我们要进行这些操作就只能去定义对于的函数,比如AddDay、SubDay;但是这些函数的可读性始终是没有 + - > < 这些符号的可读性高的,而且不同程序员给定的函数名称也不一样相同;

所以为了增强代码的可读性,C++为自定义类型引入了运算符重载,运算符重载是具有特殊函数名的函数 – 其函数名为关键字operator+需要重载的运算符符号,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似;换句话说,运算符重载函数只有函数名特殊,其他方面与普通函数一样;我们以日期+天数为例:

Date类:

class Date
{
public:
  Date(int year = 1970, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //获取每个月的天数
  int GetMonthDay(int year, int month)
  {
    static int day[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;
    return day[month];
  }
  //打印
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

函数方式实现:

void AddDay(Date& d, int day)
{
  d._day += day;
  while (d._day > GetMonthDay(d._year, d._month))
  {
    d._day -= GetMonthDay(d._year, d._month);
    d._month++;
    if (d._month > 12)
    {
      d._month -= 12;
      d._year++;
    }
  }
}

运算符重载方式实现:

void operator+=(Date& d, int day)
{
  d._day += day;
  while (d._day > GetMonthDay(d._year, d._month))
  {
    d._day -= GetMonthDay(d._year, d._month);
    d._month++;
    if (d._month > 12)
    {
      d._month -= 12;
      d._year++;
    }
  }
}

2020062310470442.png

2、运算符重载函数的位置

如果大家实际上手编写我们上面的 AddDay 和 operator+= 函数就会发现一个问题:类中的成员函数 _year、_month、_day 都是私有的,我们在类外并不能直接修改它们;

2020062310470442.png

但是我们又不能直接把成员变量设为共有,这样类的封装线得不到保证;那么如果我们把函数放到类里面呢?比如下面这样:

2020062310470442.png

上面这种情况是由我们在 类和对象上篇 中提到的 this 指针引起的 – 类的每个成员函数的第一个参数都是一个隐藏的 this 指针,它指向类的某一个具体对象,且 this 不能显示传递,也不能显示写出,但是可以在函数内部显示使用;


也就是说,本来 += 这个操作符只能有两个操作数,所以使用 operator 重载 += 得到的函数也只能有两个参数;但是由于我们为了使用类的成员变量将函数放在了类内部,所以编译器自动传递了对象的地址,并且在函数中使用一个 this 指针来接收,导致函数参数变成了三个;所以出现了 “operator += 的参数太多” 这个报错;


那么为了解决这个问题,我们在定义 operator+= 函数时,就只显式的传递一个参数 – 右操作数,而左操作数由编译器自动传递;当我们在函数内部需要操作左操作数时,也直接操作 this 指针即可;


还是以日期+天数为例;

//运算符重载+=
void operator+=(int day)  //只传递右操作数,通过this操作左操作数
{
    this->_day += day;  //这里的this->编译器会自动添加
    while (_day > GetMonthDay(_year, _month))
    {
        _day -= GetMonthDay(_year, _month);
        _month++;
        if (_month > 12)
        {
            _month -= 12;
            _year++;
        }
    }
}

2020062310470442.png

注意

1、当我们将函数放在类内部时,不管操作数有几个,this 默认指向第一个操作数;

2、对于在类外部无法访问类的私有成员变量的问题其实也可以使用友元解决,我们后面再学习;

3、运算符重载的特性

运算符重载函数有如下特性:

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

重载操作符必须有一个类类型参数 (因为运算符重载只能对自定义类型使用);

用于内置类型的运算符,其含义不能改变,即不能对内置类型使用运算符重载;

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

以下5个运算符不能重载: .* :: sizeof . ?: 注意这个经常在笔试选择题中出现,特别是 .* 操作符,希望大家记住;

4、常见的运算符重载

常见的运算符重载有:operator+ (+)、operator- (-)、operator* (*)、operator/(/)、operator+= (+=)、operator-= (-=)、operator== (==)、operator= (=)、operator> (>)、operator< (<)、operator>= (>=)、operator<= (<=)、operator!= (!=)、operator++ (++)、operator-- (–)等;


其中,对于 operator++ 和 operator-- 来说有一些不一样的地方 – 因为 ++ 和 – 分为前置和后置,二者虽然都能让变量自增1,但是它们的返回值不同;但是由于 ++ 和 – 只有一个操作数,且这个操作数还会由编译器自动传递;所以正常的 operator++ 和 operator-- 并不能对二者进行区分;最终,C++规定:后置++/–重载时多增加一个int类型的参数,此参数在调用函数时不传递,由编译器自动传递;


其次,上面重载函数中的 operator= 就是默认成员函数之一 – 赋值重载函数;

注:由于运算符重载函数很多,情况也比较复杂,所以我们将运算符重载的详细细节 (比如引用做返回值、引用做参数、函数的复用、对特殊情况的处理等知识) 放在 Date 类的实现中去介绍;

六、赋值重载

1、基础知识

赋值重载函数是C++的默认六个成员函数之一,它也是运算符重载的一种,它的作用是两个已存在的对象之间的赋值,其特性如下:

  1. 赋值重载的格式规范;
  2. 赋值运算符只能重载成类的成员函数不能重载成全局函数;
  3. 若未显式定义,编译器会生成默认的赋值重载函数;
  4. 默认的赋值重载函数对内置类型以字节为单位直接进行拷贝 – 浅拷贝,对自定义类型调用其自身的赋值重载函数;

2、特性分析 – 函数格式

赋值重载函数的格式一般有如下要求:

使用引用做参数,并以 const 修饰

我们知道,使用传值传参时函数形参是实参的一份临时拷贝,所以传值传参会调用拷贝构造函数;而使用引用做参数时,形参是实参的别名,从而减少了调用拷贝构造在时间和空间上的消耗;另外,赋值重载只会改变被赋值对象,而不会改变赋值对象,所以我们使用 const 来防止函数内部的误操作;

void operator=(const Date& d);

使用引用做返回值且返回值为*this

我们可以对内置类型进行连续赋值,比如 int i,j; i = j = 0; 那么对于自定义类型来说,我们也可以使用运算符重载来让其支持连续赋值,则重载函数就必须具有返回值;同时,由于我们是在函数外部调用重载函数,所以重载函数调用结束后该对象仍然存在,那么我们就可以使用引用作为函数的返回值,从而减少一次返回值的拷贝,提高程序效率;

另外,我们一般使用左操作数作为函数的返回值,也就是 this 指针指向的对象;

Date& operator=(const Date& d);

检测是否自己给自己赋值

用户在调用成员函数时有可能发生下面这种情况:Date d1; Date& d2 = d1; d1 = d2; 这种情况对于只需要浅拷贝的对象来说并没有什么大碍,但对于有资源申请,需要进行深拷贝的对象来说就会发生不可控的事情,具体案例我们在第四点特性中讲解;

在 《Effective C++》中对赋值重载函数自我赋值的解释是这样的:

2020062310470442.png

if(this == &d)  //比较两个对象的地址是否相同
  return *this;

Date 类的赋值重载函数如下:

//赋值重载
Date& operator=(const Date& d)
{
    //自我赋值
    if (this == &d)  
    {
        return *this;
    }
    _year = d._year;
    _month = d._month;
    _day = d._day;
    return *this;
}

2020062310470442.png

3、特性分析 – 重载为成员函数

赋值运算符只能重载成类的成员函数不能重载成全局函数,这是因为赋值重载函数作为六个默认成员函数之一,如果我们不显示实现,编译器会默认生成;此时用户如果再在类外自己实现一个全局的赋值运算符重载,就会和编译器在类中生成的默认赋值运算符重载冲突,从而造成链接错误;

2020062310470442.png

class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  int _year; 
  int _month;
  int _day;
};
Date& operator=(Date& left, const Date& right)
{
  if (&left != &right)
  {
    left._year = right._year;
    left._month = right._month;
    left._day = right._day;
  }
  return left;
}

2020062310470442.png


相关文章
|
3天前
|
设计模式 安全 编译器
【C++11】特殊类设计
【C++11】特殊类设计
22 10
|
8天前
|
C++
C++友元函数和友元类的使用
C++中的友元(friend)是一种机制,允许类或函数访问其他类的私有成员,以实现数据共享或特殊功能。友元分为两类:类友元和函数友元。类友元允许一个类访问另一个类的私有数据,而函数友元是非成员函数,可以直接访问类的私有成员。虽然提供了便利,但友元破坏了封装性,应谨慎使用。
39 9
|
3天前
|
存储 编译器 C语言
【C++基础 】类和对象(上)
【C++基础 】类和对象(上)
|
11天前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以&#39;\0&#39;结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加&#39;\0&#39;。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。
|
12天前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `&lt;`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
11天前
|
C++
【C++】string类的使用④(常量成员Member constants)
C++ `std::string` 的 `find_first_of`, `find_last_of`, `find_first_not_of`, `find_last_not_of` 函数分别用于从不同方向查找目标字符或子串。它们都返回匹配位置,未找到则返回 `npos`。`substr` 用于提取子字符串,`compare` 则提供更灵活的字符串比较。`npos` 是一个表示最大值的常量,用于标记未找到匹配的情况。示例代码展示了这些函数的实际应用,如替换元音、分割路径、查找非字母字符等。
|
12天前
|
C++
C++】string类的使用③(修改器Modifiers)
这篇博客探讨了C++ STL中`string`类的修改器和非成员函数重载。文章介绍了`operator+=`用于在字符串末尾追加内容,并展示了不同重载形式。`append`函数提供了更多追加选项,包括子串、字符数组、单个字符等。`push_back`和`pop_back`分别用于在末尾添加和移除一个字符。`assign`用于替换字符串内容,而`insert`允许在任意位置插入字符串或字符。最后,`erase`函数用于删除字符串中的部分内容。每个函数都配以代码示例和说明。
|
12天前
|
安全 编译器 C++
【C++】string类的使用②(元素获取Element access)
```markdown 探索C++ `string`方法:`clear()`保持容量不变使字符串变空;`empty()`检查长度是否为0;C++11的`shrink_to_fit()`尝试减少容量。`operator[]`和`at()`安全访问元素,越界时`at()`抛异常。`back()`和`front()`分别访问首尾元素。了解这些,轻松操作字符串!💡 ```
|
12天前
|
存储 编译器 Linux
【C++】string类的使用②(容量接口Capacity )
这篇博客探讨了C++ STL中string的容量接口和元素访问方法。`size()`和`length()`函数等价,返回字符串的长度;`capacity()`提供已分配的字节数,可能大于长度;`max_size()`给出理论最大长度;`reserve()`预分配空间,不改变内容;`resize()`改变字符串长度,可指定填充字符。这些接口用于优化内存管理和适应字符串操作需求。
|
12天前
|
C++ 容器
【C++】string类的使用①(迭代器接口begin,end,rbegin和rend)
迭代器接口是获取容器元素指针的成员函数。`begin()`返回首元素的正向迭代器,`end()`返回末元素之后的位置。`rbegin()`和`rend()`提供反向迭代器,分别指向尾元素和首元素之前。C++11增加了const版本以供只读访问。示例代码展示了如何使用这些迭代器遍历字符串。