【C++基础】第七十五课:[重载运算与类型转换]基本概念

operator,逗号运算符

Posted by x-jeff on June 26, 2023

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.前言

当运算符被用于类类型的对象时,C++语言允许我们为其指定新的含义;同时,我们也能自定义类类型之间的转换规则。和内置类型的转换一样,类类型转换隐式地将一种类型的对象转换成另一种我们所需类型的对象。

当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义。

2.基本概念

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

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

如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。

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

1
2
//错误:不能为int重定义内置的运算符
int operator+(int, int);

这一约定意味着当运算符作用于内置类型的运算对象时,我们无法改变该运算符的含义。

我们可以重载大多数(但不是全部)运算符。表14.1指明了哪些运算符可以被重载,哪些不行。后续博客还会介绍重载new和delete的方法。

我们只能重载已有的运算符,而无权发明新的运算符号。例如,我们不能提供operator**来执行幂操作。

有四个符号(+、-、*、&)既是一元运算符也是二元运算符,所有这些运算符都能被重载,从参数的数量我们可以推断到底定义的是哪种运算符。

+,-在表示正数和负数时为一元运算符。

&在用作按位与运算时为二元运算符。

对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。不考虑运算对象类型的话,

1
x == y + z;

永远等价于x==(y+z)

2.1.直接调用一个重载的运算符函数

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

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

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

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

2.2.某些运算符不应该被重载

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

逗号运算符(comma operator)含有两个运算对象,按照从左向右的顺序依次求值。和逻辑与、逻辑或以及条件运算符一样,逗号运算符也规定了运算对象求值的顺序。

对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。

逗号运算符经常被用在for循环当中:

1
2
3
4
vector<int>::size_type cnt = ivec.size();
//将把从size到1的值赋给ivec的元素
for(vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt)
	ivec[ix] = cnt;

因为上述运算符的重载版本无法保留求值顺序和/或短路求值属性,因此不建议重载它们。当代码使用了这些运算符的重载版本时,用户可能会突然发现他们一直习惯的求值规则不再适用了。

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

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

2.3.选择作为成员或者非成员

当我们定义重载的运算符时,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数。在某些时候我们别无选择,因为有的运算符必须作为成员;另一些情况下,运算符作为普通函数比作为成员更好。

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

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

程序员希望能在含有混合类型的表达式中使用对称性运算符。例如,我们能求一个int和一个double的和,因为它们中的任意一个都可以是左侧运算对象或右侧运算对象,所以加法是对称的。如果我们想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。

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

1
2
3
string s = "world";
string t = s + "!"; //正确:我们能把一个const char*加到一个string对象中
string u = "hi" + s; //如果+是string的成员,则产生错误

如果operator+是string类的成员,则上面的第一个加法等价于s.operator+("!")。同样的,"hi"+s等价于"hi".operator+(s)。显然”hi”的类型是const char*,这是一种内置类型,根本就没有成员函数。

因为string将+定义成了普通的非成员函数,所以"hi"+s等价于operator+("hi",s)。和任何其他函数调用一样,每个实参都能被转换成形参类型。唯一的要求是至少有一个运算对象是类类型,并且两个运算对象都能准确无误地转换成string。