重载运算与类型转换——基本概念,输入和输出运算符,算术和关系运算符,赋值运算符,下标运算符,递增和递减运算符,成员访问运算符

Posted acgame

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重载运算与类型转换——基本概念,输入和输出运算符,算术和关系运算符,赋值运算符,下标运算符,递增和递减运算符,成员访问运算符相关的知识,希望对你有一定的参考价值。

一、基本概念

  重载的运算符时具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。

  重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个。对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。

  当一个重载的运算符时成员函数时,this绑定到左侧运算对象。成员运算符函数的显式参数数量比运算对象的数量少一个。

  对一个运算符来说,它或者是类的成员,或者至少含有一个类类型的参数

  可以重载的运算符:

+ - * / % ^
& | ~ ! , =
< > <= >= ++ --
<< >> == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= [] ()
-> ->* new new [] delete delete []

  不能被重载的运算符:

:: .* . ?:

  我们只能重载已有的运算符,而无权发明新的运算符号。对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。

 1)直接调用一个重载的运算符

  通用情况下,我们将运算符作用于类型正确的实参,从而以这种间接方式“调用”重载的运算符函数。然而,我们也能像调用普通函数一样直接调用运算符函数,先指定函数名字,然后传入数量正确、类型适当的实参:

  // 一个非成员运算符函数的等价调用
  data1+data2 // 普通的表达式
  operator+(data1,data2) // 等价的函数调用

  我们像调用其他成员函数一样显式地调用成员运算符函数。首先指定运算符函数的对象(或指针)的名字,然后使用点运算符(或箭头运算符)访问希望调用的函数:

  data1+=data2 // 基于“调用”的表达式
  data1.operator+=(data2) // 对成员运算符函数的等价调用

2)某些运算符不应该被重载

  某些运算符指定了运算对象的求值的顺序。因为使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上。特别是,逻辑与运算符、逻辑或运算符和逗号运算符的运算对象求值顺序规则无法保留下来。除此之外,&&和||运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。

  还有一个原因使得我们一般不重载逗号运算符和取地址符:C++语言已经定义了这两种运算符用于类类型对象时的特殊含义,这一点与大多数运算符都不相同。因为这两种运算符已经有了内置的含义,所以一般来说它们不应该被重载,否则它们的行为将异于常态,从而导致类的用户无法适应。

  通常情况下,不应该重载逗号、取地址、逻辑与、逻辑或运算符

3)使用与内置类型一致的含义

  如果某些类操作在逻辑上与运算符相关,则它们适合定义成重载的运算符:

  • 如果类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致。
  • 如果类的某个操作是检查相等性,则定义operator==;如果类有了operator==,意味着它通常也应该有operator!=。
  • 如果类包含一个内在的单序比较操作,则定义operator<;如果类有了operator<,则它也应该含有其他关系操作。
  • 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容:逻辑运算符和关系运算符应该返回bool,算术运算符应该返回一个类类型的值,赋值运算符和复合赋值运算符则应该返回左侧运算对象的一个引用。

4)赋值和复合赋值运算符

  赋值运算符的行为与复合版本的类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。重载的赋值运算符应该继承而非违背其内置版本的含义。

  如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符。

5)选择作为成员或者非成员

  下面的准则有助于我们在将运算符定义为成员函数还是普通的非成员函数做出抉择:

  • 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须还是成员。
  • 复合赋值运算符一般来说应该是成员,但并非必须。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员。
  • 具有对称性的运算符可能转换任一端的运算对象,例如算术、相等性、关系和位运算符等,通常应该是普通的非成员函数。

  当把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。

  当把运算符定义成普通的非成员函数时,唯一的要求是是至少有一个运算对象是类类型,并且两个运算对象都能精确无误地转换。

 

二、输入和输出运算符

1、重载输出运算符<<

  通常情况下,输出运算符的第一个形参是非常量ostream对象的引用。之所以ostream是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个ostream对象。

  第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型。第二个形参是引用的原因是我们希望避免复制实参;而之所以该形参是常量是因为通常情况打印对象不会改变对象的内容。

  为了与其他输出运算符保持一致,operator<<一般要返回它的ostream形参。

技术分享图片
1 std::ostream &operator<<(std::ostream& os, const SalesData &item)
2 {
3     os << item.m_bookno << ", " << item.m_unitssold << ", "
4         << item.m_revenue << ", " << item.avgPrice();
5     return os;
6 }
View Code

1)输入输出运算符必须是非成员函数

  与iostream标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数,因为我们无法给标准的类添加任何成员。

  IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元。

 

2、重载输入运算符>>

  通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的非常量对象的引用。该运算符通常会返回某个给定流的引用。第二个形参之所以必须是个非常量是因为输入运算符本身的目的就是将数据读入到这个对象中。

  输入运算符必须处理输入可能失败的情况,而输出运算符不需要。

技术分享图片
 1 std::istream &operator>>(std::istream &is, SalesData &item)
 2 {
 3     double price;
 4     is >> item.m_bookno >> item.m_unitssold >> price;
 5     if (is)
 6         item.m_revenue = item.m_unitssold*price;
 7     else
 8         item = SalesData(); // 输入失败,对象被赋予默认的状态
 9     return is;
10 }
View Code

1)输入时的错误

  在执行输入运算符时可能发生以下错误:

  • 当流含有错误类型的数据时读取操作可能失败。
  • 当读取到达文件末尾或者遇到输入流的其他错误时也会失败。

 

三、算术和关系运算符

   通常情况下,我们把算术运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用。

  算术运算通常会计算它的两个运算对象并得到一个新值,这个值有别于任意一个运算对象,常常位于一个局部变量之内,操作完成后返回该变量的副本作为其结果。如果类定义了算术运算符,则它一般也会定义一个对应的复合赋值运算符。此时,最有效的方法是使用复合赋值运算符来定义算术运算符。

技术分享图片
 1 SalesData &SalesData::operator+=(const SalesData &rhs)
 2 {
 3     m_unitssold += rhs.m_unitssold;
 4     m_revenue += rhs.m_revenue;
 5     return *this;
 6 }
 7 
 8 SalesData operator+(const SalesData &lhs, const SalesData &rhs)
 9 {
10     SalesData sum = lhs;
11     sum += rhs;
12     return sum;
13 }
View Code

1、相等运算符

  通常情况下,C++中的类通过相等运算符来检验两个对象是否相等。也就是说,它们会比较对象的每一个数据成员,只有当所有对应的成员都相等时才认为两个对象相等。

技术分享图片
 1 bool operator==(const SalesData &lhs, const SalesData &rhs)
 2 {
 3     return lhs.m_bookno == rhs.m_bookno&&
 4         lhs.m_unitssold == rhs.m_unitssold&&
 5         lhs.m_revenue == rhs.m_revenue;
 6 }
 7 
 8 bool operator!=(const SalesData &lhs, const SalesData &rhs)
 9 {
10     return !(lhs == rhs);
11 }
View Code

2、关系运算符

  定义了相等运算符的类也常常(但不总是)包含关系运算符。特别是,因为关联容器和一些算法要用到小于运算符,所以定义operator<会比较有用。

  通常情况下关系运算符应该:定义顺序关系,令其与关联容器中对关键字的要求一致。如果类同时也含有==运算符的话,则定义一种关系令其与==保持一致。特别是,如果两个对象是!=的话,那么一个对象应该<另一个。

 

四、赋值运算符

  我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。赋值运算符返回其左侧运算对象的引用。

1)复合赋值运算符

  复合赋值运算符不非得是类的成员,不过我们还是倾向于把包括赋值在内的所有赋值运算都定义在类的内部。为了与内置类型的复合赋值保持一致,类中的复合赋值运算符也要返回其左侧运算对象的引用。

 

五、下标运算符

  表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符operator[]。下标运算符必须是成员函数。

  为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值,这样做的好处是下标可以出现在赋值运算符的任意一端。进一步,我们最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值。

技术分享图片
 1 class StrVec
 2 {
 3 public:
 4     StrVec():vec(){}
 5     void push_back(const std::string &s) { vec.push_back(s); }
 6     std::string &operator[](std::size_t n) { return vec[n]; }
 7     const std::string &operator[](std::size_t n)const { return vec[n]; }
 8 private:
 9     std::vector<std::string> vec;
10 };
View Code

 

六、递增和递减运算符

  定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符通常应该被定义成类的成员。

1)定义前置递增/递减运算符

  为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。

技术分享图片
 1 // 前置版本:返回递增/递减对象的引用
 2 StrBlob &StrBlob::operator++()
 3 {
 4     ++curr;
 5     check(curr, "increment past end");
 6     return *this;
 7 }
 8 
 9 StrBlob &StrBlob::operator--()
10 {
11     --curr;
12     check(curr, "decrement past end");
13     return *this;
14 }
View Code

2)区分前置和后置运算符

  要想同时定义前置和后置运算符,必须首先解决一个问题,即普通的重载形式无法区分这两种情况。前置和后置版本使用的是同一个符号,意味着其重载版本所用的名字将是相同的,并且运算对象的数量和类型也相同。

  为了解决这个问题,后置版本接受一个额外的(不被使用)int类型的形参。当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。尽管从语法上来说后置函数可以使用这个额外的形参,但是在实际过程中通常不会这么做。这个形参的唯一作用就是区分前置和后置版本的函数,而不是真的要在实现后置版本时参与运算。

技术分享图片
 1 // 后置版本
 2 StrBlob StrBlob::operator++(int)
 3 {
 4     StrBlob ret = *this;
 5     ++*this;
 6     return ret;
 7 }
 8 
 9 StrBlob StrBlob::operator--(int)
10 {
11     StrBlob ret = *this;
12     --*this;
13     return ret;
14 }
View Code

  因为我们不会用到int形参,所以无须为其命名。

完整代码:

技术分享图片
 1 #include <iostream>
 2 #include <string>
 3 #include <vector>
 4 #include <algorithm>
 5 #include <memory>
 6 
 7 class StrBlob
 8 {
 9 public:
10     // 前置版本
11     StrBlob &operator++();
12     StrBlob &operator--();
13     // 后置版本
14     StrBlob operator++(int);
15     StrBlob operator--(int);
16 public:
17     typedef std::vector<std::string>::size_type size_type;
18     StrBlob(std::initializer_list<std::string> items)
19         :data(std::make_shared<std::vector<std::string>>(items)), curr(0) {}
20     size_type size()const { return data->size(); }
21     std::string now()const { return (*data)[curr]; }
22 
23 private:
24     void check(size_type i, const std::string &msg) const
25     {
26         if (i >= data->size())
27             throw std::out_of_range(msg);
28     }
29 private:
30     std::shared_ptr<std::vector<std::string>> data; // 多个对象间共享底层元素
31     std::size_t curr; // 在数组中的当前位置
32 };
33 
34 // 前置版本:返回递增/递减对象的引用
35 StrBlob &StrBlob::operator++()
36 {
37     ++curr;
38     check(curr, "increment past end");
39     return *this;
40 }
41 
42 StrBlob &StrBlob::operator--()
43 {
44     --curr;
45     check(curr, "decrement past end");
46     return *this;
47 }
48 // 后置版本
49 StrBlob StrBlob::operator++(int)
50 {
51     StrBlob ret = *this;
52     ++*this;
53     return ret;
54 }
55 
56 StrBlob StrBlob::operator--(int)
57 {
58     StrBlob ret = *this;
59     --*this;
60     return ret;
61 }
62 
63 int main()
64 {
65     StrBlob blob({ "a", "b","c","d","e" });
66     std::cout << (blob++).now() << std::endl;
67     return 0;
68 }
View Code

 

七、成员访问运算符

  在迭代器及智能指针类中常常用到解引用运算符(*)和箭头运算符(->)。箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。

技术分享图片
 1 class StrBlob
 2 {
 3 public:
 4     // 前置版本
 5     StrBlob &operator++();
 6     StrBlob &operator--();
 7     // 后置版本
 8     StrBlob operator++(int);
 9     StrBlob operator--(int);
10 
11     std::string &operator*()const {
12         return (*data)[curr];
13     }
14     std::string *operator->()const {
15         return &this->operator*(); // 将实际工作委托给解引用运算符
16     }
17 public:
18     typedef std::vector<std::string>::size_type size_type;
19     StrBlob(std::initializer_list<std::string> items)
20         :data(std::make_shared<std::vector<std::string>>(items)), curr(0) {}
21     size_type size()const { return data->size(); }
22     std::string now()const { return (*data)[curr]; }
23 
24 private:
25     void check(size_type i, const std::string &msg) const
26     {
27         if (i >= data->size())
28             throw std::out_of_range(msg);
29     }
30 private:
31     std::shared_ptr<std::vector<std::string>> data; // 多个对象间共享底层元素
32     std::size_t curr; // 在数组中的当前位置
33 };
View Code

1)对箭头运算符返回值的限定

  和大多数其他运算符一样(尽管这么做不好),我们能令operator*完成任何我们指定的操作。箭头运算符则不是这样,它永远不能丢掉成员访问这个最基本的含义。当我们重载箭头时,可以改变的是箭头从哪个对象中获取成员,而箭头获取成员这一事实则永远不变。

  对于形如point->mem的表达式来说,point必须是指向类对象的指针或者是一个重载了operator->的类的对象。根据point类型不同,point->mem分别等价于

  (*point).mem;  // point是一个内置的指针类型

  point.operator()->mem; // point是类的一个对象

除此之外,代码都将发生错误。point->mem的执行过程如下所示:

  1. 如果point是指针,则我们应用内置的箭头运算符,表达式等价于(*point).mem。首先解引用该指针,然后从所得的对象中获取指定的成员。如果point所指的类型没有名为mem的成员,程序发生错误。
  2. 如果point是定义了operator->的类的一个对象,则我们使用point.operator->()的结果来获取mem。其中,如果该结果是一个指针,则执行第1步;如果该结果本身含有重载的operator->(),则重复调用当前步骤。最终,当这一过程结束时程序或者返回了所需的内容,或者返回一些表示程序错误的信息。

  重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。

 

一些完整的例子:

技术分享图片
 1 #ifndef SALES_DATA
 2 #define SALES_DATA
 3 
 4 #include <iostream>
 5 #include <string>
 6 class SalesData
 7 {
 8     // 友元声明
 9     friend std::ostream &operator<<(std::ostream &os, const SalesData &item);
10     friend std::istream &operator>>(std::istream &, SalesData &);
11     friend SalesData operator+(const SalesData &, const SalesData &);
12     friend bool operator==(const SalesData &, const SalesData &);
13     friend bool operator!=(const SalesData &, const SalesData &);
14 public:
15     SalesData &operator+=(const SalesData &);
16 public:
17     SalesData(const std::string &s="") :m_bookno(s), m_unitssold(0), m_revenue(0) {}
18     SalesData(const std::string &s, unsigned n, double p) :
19         m_bookno(s), m_unitssold(n), m_revenue(p * n) {}
20 private:
21     double avgPrice() const;
22 
23     std::string m_bookno; // 书名
24     unsigned m_unitssold; // 数量
25     double m_revenue; // 总价
26 };
27 
28 std::ostream &operator<<(std::ostream& os, const SalesData &item);
29 std::istream &operator>>(std::istream &, SalesData &);
30 SalesData operator+(const SalesData &, const SalesData &);
31 bool operator==(const SalesData &, const SalesData &);
32 bool operator!=(const SalesData &, const SalesData &);
33 #endif
SalesData.h
技术分享图片
 1 #include "SalesData.h"
 2 #include <string>
 3 
 4 std::ostream &operator<<(std::ostream& os, const SalesData &item)
 5 {
 6     os << item.m_bookno << ", " << item.m_unitssold << ", "
 7         << item.m_revenue << ", " << item.avgPrice();
 8     return os;
 9 }
10 
11 std::istream &operator>>(std::istream &is, SalesData &item)
12 {
13     double price;
14     is >> item.m_bookno >> item.m_unitssold >> price;
15     if (is)
16         item.m_revenue = item.m_unitssold*price;
17     else
18         item = SalesData(); // 输入失败,对象被赋予默认的状态
19     return is;
20 }
21 
22 SalesData &SalesData::operator+=(const SalesData &rhs)
23 {
24     m_unitssold += rhs.m_unitssold;
25     m_revenue += rhs.m_revenue;
26     return *this;
27 }
28 
29 SalesData operator+(const SalesData &lhs, const SalesData &rhs)
30 {
31     SalesData sum = lhs;
32     sum += rhs;
33     return sum;
34 }
35 
36 bool operator==(const SalesData &lhs, const SalesData &rhs)
37 {
38     return lhs.m_bookno == rhs.m_bookno&&
39         lhs.m_unitssold == rhs.m_unitssold&&
40         lhs.m_revenue == rhs.m_revenue;
41 }
42 
43 bool operator!=(const SalesData &lhs, const SalesData &rhs)
44 {
45     return !(lhs == rhs);
46 }
47 
48 double SalesData::avgPrice() const
49 {
50     if (m_unitssold)
51         return m_revenue / m_unitssold;
52     else
53         return 0;
54 }
SalesData.cpp
技术分享图片
 1 #include <iostream>
 2 #include <string>
 3 #include <vector>
 4 #include <algorithm>
 5 #include <memory>
 6 
 7 class StrBlob
 8 {
 9 public:
10     // 前置版本
11     StrBlob &operator++();
12     StrBlob &operator--();
13     // 后置版本
14     StrBlob operator++(int);
15     StrBlob operator--(int);
16 
17     std::string &operator*()const {
18         return (*data)[curr];
19     }
20     std::string *operator->()const {
21         return &this->operator*(); // 将实际工作委托给解引用运算符
22     }
23 public:
24     typedef std::vector<std::string>::size_type size_type;
25     StrBlob(std::initializer_list<std::string> items)
26         :data(std::make_shared<std::vector<std::string>>(items)), curr(0) {}
27     size_type size()const { return data->size(); }
28     std::string now()const { return (*data)[curr]; }
29 
30 private:
31     void check(size_type i, const std::string &msg) const
32     {
33         if (i >= data->size())
34             throw std::out_of_range(msg);
35     }
36 private:
37     std::shared_ptr<std::vector<std::string>> data; // 多个对象间共享底层元素
38     std::size_t curr; // 在数组中的当前位置
39 };
40 
41 // 前置版本:返回递增/递减对象的引用
42 StrBlob &StrBlob::operator++()
43 {
44     ++curr;
45     check(curr, "increment past end");
46     return *this;
47 }
48 
49 StrBlob &StrBlob::operator--()
50 {
51     --curr;
52     check(curr, "decrement past end");
53     return *this;
54 }
55 // 后置版本
56 StrBlob StrBlob::operator++(int)
57 {
58     StrBlob ret = *this;
59     ++*this;
60     return ret;
61 }
62 
63 StrBlob StrBlob::operator--(int)
64 {
65     StrBlob ret = *this;
66     --*this;
67     return ret;
68 }
69 
70 int main()
71 {
72     StrBlob blob({ "a", "b","c","d","e" });
73     std::cout << blob->size() << std::endl;
74     return 0;
75 }
main.cpp

 




以上是关于重载运算与类型转换——基本概念,输入和输出运算符,算术和关系运算符,赋值运算符,下标运算符,递增和递减运算符,成员访问运算符的主要内容,如果未能解决你的问题,请参考以下文章

操作重载与类型转换C++

操作重载与类型转换C++

操作重载与类型转换C++

使用 C++ 概念重载不同模板类型的运算符

java(

C++之重载运算与类型转换