STL 番外:运算符重载与友元
前言
正常来说,我们一般使用的运算符是对基本的数据类型进行操作,但是在C++中有了对象,导致对象无法通过运算符进行运算,故引入了运算符重载即需要重新的定义这些运算符,赋予已有运算符新的功能,使它能够用于特定类型执行特定的操作。运算符重载的实质是函数重载,它提供了C++的可扩展性。
运算符重载有成员函数与全局函数两种方式,本文将简要分析二者的关系。
作为成员函数进行重载
定义一个测试用的类并在类内重载 +
运算符。
1 | class addFloat |
进行如下测试:
1 | void test1() { |
运算符正常工作,看上去十分完美,但当我们把 2.2
放在运算符前方,就会发现程序无法正常运行。
1 | addFloat b = 2.2 + a; // 报错 |
可以做如下分析:
- 对于
b = a + 2.2;
,被转换为b = a.operator+(2.2);
,其中2.2
被隐式类型转换为了addFloat
类型。 - 对于
b = 2.2 + a;
,被转换为b = (2.2).operator+(a);
,这很显然是不正确的,进而编译报错。
这就是作为成员函数重载运算符所带来的 “不对称” 问题,想要解决这个问题,就必须在 float
内部也重载这一运算符,这显然是不合理的。运算符重载的初衷是给类添加新的功能,方便类的运算,它作为类的成员函数是理所应当的,是首选的;不过,类的成员函数不能对称地处理数据,程序员必须在(参与运算的)所有类型的内部都重载当前的运算符。所以 C++ 进行了折中,允许以全局函数(友元函数)的形式重载运算符。
作为全局函数进行重载
将上述类中的 operator+
改写为如下函数,即全局函数。其中,声明为友元是为了访问 private
变量。
1 | friend addFloat operator+(const addFloat& a, const addFloat& b) |
这里也可以看出友元函数与成员函数的区别,友元函数没有this指针,而成员函数有。因此,在两个操作数的重载中友元函数有两个参数,而成员函数只有一个。
再次进行测试:
1 | void test2() { |
-
先来看
c = a + 2.2;
,实际上会被转换为c = operator+(a, 2.2);
这样的形式进行调用;这里同样会发生隐式类型转换。 -
对于
d = 2.2 + a;
,也是一样的道理,被转换为c = operator+(2.2, a);
这样的形式,同样发生了隐式类型转换。
可以看出,使用全局函数定义的运算符可以避免对称性问题。
总结
发现还是 Primer 里面总结的好,这里直接进行一个复制粘贴。
下面的准则有助于我们在将运算符定义为成员函数还是普通的非成员函数做出抉择:
- 赋值 (
=
)、下标 ([]
)、调用 (()
) 和成员访问箭头 (->
) 运算符必须是成员。 - 复合赋值运算符 ( 如
+=
、-=
等 ) 一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。 - 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员。
- 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。
程序员希望能在含有混合类型的表达式中使用对称性运算符。例如,我们能求一个 int
和一个 double
的和,因为它们中的任意一个都可以是左侧运算对象或右侧运算对象,所以加法是对称的。如果我们想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。
当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。