一文读懂 C++ 运算符重载
运算符重载是 C++ 语言的一个特性,利用运算符重载能够写出更加简洁的代码,对外封装类的实现细节。本文介绍运算符重载相关知识点。
谈到运算符重载,还需要从 “函数重载”
说起。函数重载是指函数名相同,但函数参数类型、参数数量不同(返回类型相同)的函数。比如实现某种数据类型向
string 类型运算的函数: 1
2
3
4
5
6// 函数重载示例
string to_string(int i);
string to_string(double d);
string to_string(char c);
string to_string(float d);
// 等等
运算符重载本质上也是函数重载,它们可以看做函数名为
operator+
、operator-
、operator*
、operator++
、operator<<
等等的函数。重载即体现在函数名相同,但函数参数类型不同。当然,一些运算符也允许参数数量不同的重载,典型的就是函数调用运算符operator()
的重载。
利用函数重载可以实现更好的封装,同时提高代码的简洁度。例如,我们要实现一个大整数类,用于支持高精度运算,如果没有运算符重载,我们只能按照如下方式实现:
1
2
3
4
5
6
7
8struct BigNum{
// 相关数据成员声明
};
BigNum add(const BigNum &,const BigNum &); // 加法
BigNum sub(const BigNum &,const BigNum &); // 减法
BigNum mul(const BigNum &,const BigNum &); // 乘法
BigNum div(const BigNum &,const BigNum &); // 除法
BigNum mod(const BigNum &,const BigNum &); // 取模 1
2BigNum n1, n2, n3;
n3 = add(n1, n2); // 显式函数调用1
2
3
4
5
6
7
8
9
10
11
12class BigNum{
private:
// 相关数据成员
public:
friend BigNum operator+ (const BigNum&, const BigNum &); // 加法
friend BigNum operator- (const BigNum&, const BigNum &); // 减法
friend BigNum operator* (const BigNum&, const BigNum &); // 乘法
// 略
};
// 进行加法运算
BigNum n1, n2, n3;
n3 = n1 + n2; // 直接使用 + 运算符即可,相当于调用 operator+ 函数
知道了上面的基础知识,我们正式进入 C++ 运算符重载的主题,重点介绍运算符重载的语法特点和一些需要注意的问题。(下文以 TypeName 代表自定义类型名,如上面的 BigNum )
1. 重载输出运算符 <<
输入输出运算符的重载是为了更加方便的输入/输出数据。其语法规则如下:
1
2
3
4
5
6
7
8
9
10
11
12
13// 类内声明友元
class TypeName{
private:
// 相关成员定义 略
public:
friend ostream& operator<< (ostream &, const TypeName &);
};
// 类外定义函数
ostream & operator<< (ostream &os, const TypeName &temp)
{
// 具体实现 略
return os;
}
注意点如下:
- 形参 1 :ostream &类型,非 const 引用
- 形参 2 :const 引用类型。const 是因为输出运算符一般不应该改变对象的的状态;使用引用类型是处于性能与效率的考虑,减少调用时对象的拷贝,从而提高程序运行效率
- 返回类型: ostream 引用,这是为了能够连续输出
- 一般定义为友元
2. 重载输入运算符 >>
输入运算符与输出运算符比较相似,其语法规则如下: 1
2
3
4
5
6
7// 类内声明友元与上面输出运算符类似 此处略
// 类外实现如下
istream & operator>> (istream &is, TypeName &temp)
{
// 具体实现 略
return is;
}
注意点如下:
- 形参 1 :istream &类型, 同样是非 const 引用类型
- 形参 2 :非 const 引用,之所以是非 const 是因为输入一般要改变对象内部状态;之所以是引用类型是因为要改变形参对象本身
- 返回类型:与输出运算符重载类型,返回引用,目的是能够连续输入
- 一般定义为友元
3. 重载算术和关系运算符
实际上,算术和关系运算符重载有两种实现方法,即重载是定义为成员函数还是定义为非成员函数(友元)。以加法为例说明如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 方法 1 :非成员函数
class TypeName{
public:
// 类内声明
friend TypeName operator+ (const TypeName &, const TypeName &);
};
// 类外定义
TypeName operator+ (const TypeName &lop, const TypeName &rop)
{
// 略
}
// 方法 2: 成员函数
class TypeName{
public:
TypeName operator+ (const TypeName &rop)
{
// 略
}
};
3.1 算术运算符
我们以加法为例介绍算术运算符重载。语法规则如下:(实际上这段代码和上面方法
1 代码相同) 1
2
3
4
5
6
7
8
9
10class TypeName{
public:
// 类内声明
friend TypeName operator+ (const TypeName &, const TypeName &);
};
// 类外定义
TypeName operator+ (const TypeName &lop, const TypeName &rop)
{
// 略
}
注意点如下:
- 参数类型:两个形参都为 const &类型 ,这是因为算术运算符不改变运算对象本身;同时引用减少拷贝提高效率
- 返回值类型:返回一个经算术运算后生成的临时对象
3.2 关系运算符
关系运算符很多,包括:==
、!=
、>
、>=
、<
、<=
共
6 种。它们的实现方式同样推荐方法 1
,即以非成员函数方式实现。它们的参数类型都是 const
引用类型 ,返回值都是 bool 类型。
这 6 种关系运算符实际上存在逻辑上的转换关系,因此在实际编程中,往往采用如下方式实现这些运算符重载:
- 首先实现
==
运算符 - 利用
==
实现!=
运算符,即a != b
相当于!(a == b)
- 实现
<
运算符 - 实现
>
运算符 - 利用
<
和==
实现<=
运算符,即a <= b
相当于(a < b || a == b)
(当然,也可以用!(a > b)
实现) - 利用
>
和==
实现>=
运算符,即a >= b
相当于(a > b || a == b)
(当然,也可以用!(a < b)
实现)
实际上,除了上述实现方式也有其他方法,如 a == b
相当于
!( (a>b) || (a<b) )
,其他运算符也有另外实现方式,只须符合逻辑规则即可,这里不赘述。
下面以 ==
为例给出语法规则: 1
2
3
4
5
6
7
8
9
10class TypeName{
public:
// 类内声明
friend bool operator== (const TypeName &, const TypeName &);
};
// 类外定义
bool operator== (const TypeName &lop, const TypeName &rop)
{
// 略
}
4. 重载赋值运算符
赋值运算符分为两种,一为普通的赋值运算符
=
;二为复合赋值运算符,如
+=
、-=
等。其中,普通赋值运算符又分为拷贝赋值和移动赋值,它们实际上更应该归类与类的拷贝控制。公众号此前写过文章介绍类的拷贝控制,其中包括拷贝赋值和移动赋值运算符介绍:《浅谈 C++
类的拷贝控制》
。普通赋值运算符的重载必须为类的成员函数。此外,赋值运算符的也可以重载其它参数类型,如
C++ 标准库提供的初始化列表类型
initializer_list<TypeName>
,不过其不是本文重点,相关资料请自行查阅。
下面重点介绍复合赋值运算符的重载。复合赋值运算符不是必须定义为类的成员,但是绝大多数情况下,最好定义为类的成员函数。其语法规则如下:
1
2
3
4
5TypeName & operator+= (const TypeName &rop)
{
// 具体实现 略
return *this;
}
注意点如下:
- 形参类型:const 引用类型
- 返回值类型:引用类型,这是为了与 C++
内置与普通数据成员的复合赋值运算符保持一致的特性,即返回引用类型,在代码中体现为
return *this
- 所有复合赋值运算符都应该定义在类内部,作为成员函数
5. 重载下标运算符 [ ]
下标运算符必须是成员函数。其语法规则如下: 1
2
3
4
5
6
7
8
9
10// 不完全正确版本
class TypeName{
private:
// 略
public:
ReturnType & operator[] (int i)
{
// 略
}
}ReturnType
代表返回类型,一般取决于类内部数据成员类型。以
C++ 提供的 vector
为例,vector<T>
类型
的下标运算符返回类型为
T &
。这样我们就可以通过下标运算符修改数据成员。
注意点如下:
- 参数类型:一般为整型,代表下标
- 返回值类型:一般返回引用类型
- 对于 const 类型数据,其下标运算符应该返回 const
引用类型。因此,在类中应该定义下标运算符的 const 和非 const
两个版本重载函数,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 正确版本
class TypeName{
private:
// 略
public:
ReturnType & operator[] (int i) // 非 const
{
// 略
}
// const 类型重载
const ReturnType & operator[] (int i) const
{
// 略
}
}
6. 重载递增和递减运算符 ++ --
递增递减运算符分前置和后置两个版本,它们一般应该定义为类的成员。前置与后置版本二者差别较大,下面以
++
运算符为例分别介绍。
6.1 前置版本递增/递减运算符
前置版本递增运算符重载语法规则如下: 1
2
3
4
5
6
7
8class TypeName{
public:
TypeName & operator++ ()
{
// 具体实现
return *this;
}
};
注意点如下:
- 无参函数
- 返回值类型:引用类型,代码中
return *this
体现
6.2 后置版本递增/递减运算符
后置版本递增运算符重载语法规则如下: 1
2
3
4
5
6
7
8class TypeName{
public:
TypeName operator++ (int)
{
// 具体实现
return 自增前的对象;
}
};
注意点如下:
- 参数类型:
int
,不过,这个int
放在函数形参列表只是为了区分前置与后置,int
形参在函数内部不需要用到,因此也不必给出标识符 - 返回值类型:普通类型,返回递增/递减前保存的临时对象
- 后置版本一般利用前置版本实现,示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class TypeName{
public:
TypeName & operator++ ()
{
// 具体实现
return *this;
}
TypeName operator++ (int)
{
TypeName temp = *this;
++*this; // 调用前置版本++
return temp; // 返回递增前保存的临时对象
}
};
7. 重载成员访问运算符 -> *
成员访问运算符包括两个:对于指针的 ->
以及对于对象的
*
。->
必须是类的成员;*
通常是类的成员,但并非必须如此。它们的重载函数通常定义为 const
的,语法规则如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14class TypeName{
public:
ReturnType1 operator* () const
{
// 具体实现
// 可返回任意类型
}
ReturnType2 operator-> () const
{
// 具体实现
// 必须返回类的指针或者自定义了箭头运算符的某个类的对象
}
};
8. 重载函数调用运算符 ( )
函数调用运算符的重载相对比较特殊。理论上,我们可以重载任意多个函数调用运算符,只要保证这些重载之间不发生冲突即可。函数调用运算符必须是成员函数。定义了函数调用运算符的类的对象称为函数对象。举一个例子如下:
1
2
3
4
5
6
7class TypeName{
public:
bool operator() (const int &a, const int &b)
{
return a > b;
}
};1
2TypeName obj;
bool b = obj(12, 15);
不过,函数对象更为常用的是作为函数参数提供给标准库函数,典型的如 sort
函数: 1
2
3
4
5
6int a[] = {1,2,3,5,7,8,9,22,343,0,-232,-23423};
int n = sizeof(a)/sizeof(int);
sort(a, a+n, TypeName());
for(int i = 0;i < n; ++i)
cout << a[i] << "\t";
cout << endl;TypeName()
临时对象作为函数参数传递给 sort
函数,就能够实现自定义规则排序。上面代码将数组降序排序,而
sort
函数默认为升序排序。
值得一提的是,C++ 中提供的 lambda 表达式,实际上都转换为类,这个类重载了函数调用运算符,参数类型即为 lambda 表达式的参数类型。这样,调用 lambda 表达式就转换为调用函数对象的函数调用运算符。
9. 重载类型转换运算符
类型转换运算符的重载更为特殊,是类的一种特殊成员函数,其语法规则如下:
1
2
3
4operator type() const
{
// 具体实现
}type
表示目标转换类型。
类型转换运算符一般很少使用,在实际编程中也应该谨慎使用。若定义不当,可能产生错误或者意料之外的结果。一般而言,类定义向
bool
类型的转换规则就足够了,而且该转换函数最好定义为
explicit
的。
以上就是本文全部内容。不过这些并不是对运算符重载的全面介绍,想了解更深入详细的知识,仍需阅读相关书籍。