当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义。
1.基本概念
重载的运算符时具有特殊名字的函数,它们的名字由关键字operator和要定义的运算符号组成的。
重载运算符函数的参数数量与该运算符作用的运算对象数量一致。
当重载的运算符是成员函数时,this绑定到左侧运算对象。成员函数符函数的参数数量比运算对象的数量少一个。
当运算符作用域内置类型的对象时,无法改变该运算符的含义。
int operator+(int,int); //错误,至少含有一个类类型的参数
只能重载已有的运算符,而无权发明新的运算符号。
对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。
:: | .* | . | ? : |
我们能像调用普通函数一样直接调用运算符函数,需要传入数量正确、类型适当的实参。
operator+(data1,data2); // data1 + data2
也能像调用成员函数一样显式地调用成员运算符函数。
data1.operator+(data2); //data1 += data2
使用重载的运算符本质上是一次函数调用,因此关于运算对象求值顺序的规则无法应用到重载的运算符上。 (逻辑或、逻辑与、逗号)
一般不重载逗号运算符和取地址运算符,因为C++已经定义了它们用于类类型对象时的特殊含义。
重载运算符的返回类型通常情况下应该与其内置版本的返回类型一致或兼容。
当我们定义重载的运算符时,必须先决定是否将其声明为类的成员函数或是一个普通的非成员函数
一些能帮助你决定的准则:
1、赋值、下标、调用和成员访问箭头运算符必须是成员的。
2、改变对象状态的运算符或给定类型密切相关的运算符,如递增、递减和解引用,通常是成员的
3、具有对称性的运算符可能转换成任意一端的运算对象,如算术、相等性、关系、位运算等,通常是普通的非成员函数。
当我们把运算符定义成成员函数时,它的左侧对象必须时运算符所属类的一个对象。
string s = "world";
string t = s + "hi"; //正确,s是string对象
string u = ""hi" + s; //若+是string的成员,则产生错误
2.输入和输出运算符
2.1、重载输出运算符<<
通常情况下,输入运算符的第一个形参是一个非常量ostream对象的引用。
第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型。
operator<<一般要返回它的ostream形参。
输入运算符主要负责打印对象的内容而非控制格式,输出运算符不应打印换行符。
与iostream标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数。
2.2、重载输入运算符>>
通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用。
第二个形参是将要读入到的(非常量)对象的引用。
输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
当读取操作发生错误时,输入运算符应该负责从错误中恢复。
if(is) //检查输入是否成功
// 输入
else
//输入失败,赋予对象默认的状态
3.算术与关系运算符
通常情况下,把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。
若类同时定义了算术运算符和相关的复合赋值运算符,则应该使用复合赋值来实现算术运算符。
sales_data operator+(sales_data& lhs,sales_data& rhs)
{
sales_data sum = lhs;
sum += rhs;
return sum;
}
3.1、相等运算符
通常情况下,相等运算符应该具有传递性。(若a == b 和 b == c,则a == c)
若类定义了operator==,则这个类也应该定义operator !=。
相等运算符和不相等运算符中的一个应该把工作委托给另外一个函数。
bool operator != (const sales_data& lhs,const sales_data& rhs)
{ return !(lhs == rhs); }
3.2、关系运算符
如果存在唯一一种逻辑可靠的<定义,则应该考虑位这个类定义<运算符。
若类同时还包含==,则当且仅当<的定义和==产生的结果相同时才定义<运算符。
4.赋值运算符
类有拷贝赋值运算符、移动赋值运算符。
标准库vector类还定义了接受花括号内的元素列表作为参数的赋值运算符。
vector<string> vec;
v = {"hi","hello","world"};
在定义该类型的赋值运算符时,无须检查对象向自身赋值的情况。
strvec &strvec::operator=(initializer_list<string> il)
//形参initiaizer_list确保il与this所指的不是同一个对象
我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
复合赋值运算符通常情况下也应定义为类的成员,返回左侧对象的引用。
5.下标运算符
下标运算符必须是成员函数。
下标运算符通常以所访问元素的引用作为返回值,最好同时定义常量与非常量版本。
const std::string& operator[] (std::size_t n) const
{ return elements[n]; }
6.递增和递减运算符
递增/递减运算符会改变所操作对象的状态,建议将其设定为成员函数。
定义递增和递减运算符的类应同时定义前置版本和后置版本,这些运算符通常被定义成类的成员。
前置运算符应返回递增或递减后对象的引用,与内置版本保持一致。
operator++()
{
check(curr,"尾后位置"); //先检查curr是否指向尾后位置,若在尾后,则报错
++curr; //curr++
return *this; //返回本对象
}
operator--()
{
--curr; //递减curr,若curr本身在0处,递减后则处于无效位置
check(curr,"无效位置"); //检查是否在无效位置
return *this;
}
区分前置和后置运算符,后置版本接受一个额外的(不被使用)int类型的形参。
后置运算符应返回对象的原值(递增/递减前的的值),返回形式是一个值而非引用。
对于后置版本来说,在递增对象之前需要记录对象的状态。
operator++(int)
{
strblobstr ret = *this; //记录当前的值
++*this; //前置++能检查递增的有效性
return ret; //返回之前记录的状态
}
我们可以显式地调用一个重载的运算符,使用函数调用。
p.operator++(0); //后置版本
p.operator++(); //前置版本
7.成员访问运算符
箭头运算符必须是类的成员。解引用运算符通常也是类的成员。
成员访问运算符并不会改变所操作对象的状态,定义为const成员。
我们重载箭头时,可以改变箭头从哪个对象当中获取成员,而箭头获取成员的事实是永远不变的。
重载的箭头运算符必须返回类的指针或自定义了箭头运算符的某个类的对象。
8.函数调用运算符
若类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。
int u = absobj(i); //将i传递给absobj.operator()
函数调用运算符必须是成员函数;一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
函数对象类通常含有一些数据成员,这些成岩被用于定制调用运算符中操作。
函数对象常常作为泛型算法的实参。
8.1、lambda是函数对象
当编写一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。
在lambda表达式产生的类中含有一个重载的函数调用运算符。
默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员函数。
当lambda表达式通过引用捕获变量时,编译器可以直接使用而无需在类中将其存储为数据成员。
而通过值捕获的变量会被拷贝到类中的相对应的数据成员中。
lambda表达式产生的类不含有默认构造函数、赋值运算符及默认析构函数。
8.2、标准库定义的函数对象
标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。
这些类都定义在functional头文件中。
这些类都被定义为模板的形式,可以为其指定具体的应用类型(调用运算符的形参类型)。
plus<int> intadd; //可执行int加法的函数对象
negate<int> gate; //可对int值取反的函数对象
int sum = intadd(10,20); // 10+20
int ga = gate(10); // ga = -10
算术 | 关系 | 逻辑 |
Plus<type> | equal_to<type> | logical_and<type> |
minus<type> | not_equal_to<type> | logical_or<type> |
multiplies<type> | greater<type> | logical_not<type> |
divides<type> | greater_eqial<type> | |
modulus<type> | less<type> | |
negate<type> | less_equal<type> |
表示运算符的函数对象类常用来替换算法中的默认运算符。
标准库规定其函数对象对于指针类型同样适用。
less函数对象能帮助我们来比较两个指针的内存地址,这在以前是不被允许的。
8.3、可调用对象与function
C++中的可调用对象:函数、函数指针、lambda表达式、bind创建的对象和重载了函数调用运算符的类。
可调用对象也有属于自己的类型。
两个不同类型的可调用对象可以共享同一种调用形式。
调用形式指明了调用返回的类型以及传递给调用的实参类型。
int(int,int); //一种调用形式,接受两个int,返回int
对于几个可调用对象共享同一种调用形式,我们能把它们看成具有相同的类型。
int add(int i,int j) { return i+j;}
auto mod = [](int i,int j) {return i*j;};
//类型不同,但共享同一种调用形式
function是一个模板,创建一个具体的function类型时需提供表示对象的调用形式。
function定义在function头文件中。
function<t> f; | f是一个用来存储可调用对象的空function 这些可调用对象的调用形式应该与函数类型t相同 |
function<t> f(nullptr); | 显式地构造一个空的function |
function<t>f(obj); | 在f中存储可调用对象obj的副本 |
f | 将f作为条件:当f含有一个可调用对象时为真;否则为假 |
f(args) | 调用f中的对象,参数为args |
function<int(int,int)> f1 = add; //函数指针
function<int(int,int)> f2 = divide(); //函数对象类的对象
function<int(int,int)> f3 = [](int i,int j) { return i*j; }; //lambda
我们不能直接将重载函数的名字存入function类型的对象中,这会引起二义性问题。
可以通过存储函数指针或使用lambda来消除二义性。
int(*p)(int,int) = add; //函数指针
9.重载、类型转换与运算符
9.1、类型转换运算符
类型转换运算符是类的一种特殊成员函数,负责将一个类类型的值转换成其他类型。
类型转换运算符没有显式的返回类型,也没有形参,必须定义为类的成员函数。
类型转换运算符是隐式执行的,无法向函数传递实参。
operator int() const {} //向int型转换
在实践中,类很少提供类型转换运算符,但定义向bool的类型转换是比较普遍的现象。
当我们不希望类型转换运算符发生隐式类型转换,可以使用显式的类型转换运算符。
explicit operator int() const {} //编译器不会自动执行该类型转换
当类型转换运算符是显式时,我们可以通过显式的强制类型转换来执行类型转换。
static_cast<int>(si) + 3; //显式地将si转换成int
若表达式被用作条件,则编译器会将显式的类型转换隐式地执行。
无论我们在什么时候在条件中使用流对象,都会使用为IO类型定义的operator bool。
while(std::cin >> value)
向bool的类型转换通常用在条件部分,因此operator bool一般定义为explicit的。
9.2、避免有二义性的类型转换
若类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一的转换方式。
通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标是算术类型的转换。
struct b;
struct a{
a() = default;
a(const b&); //把b转换成a
};
struct b{
operator a() const; //把b转换成a
};
a a = f(b); //二义性错误,是f(b::operator a())还是f(a::a(const b&))
struct a{
a(int = 0); ///两个转换源都是算术类型的类型转换
a(double);
operator int() const; //两个转换对象都是算术类型的类型转换
operator double() const;
};
void f2(long double); //函数
a a;
f2(a); //二义性错误,是f(a::operator int())还是f(a::operator double())
long lg;
a a2(lg); //二义性错误,是a::a(int)还是a::a(double)
上述二义性问题发生的原因在于它们所需的标准类型转换级别一致。
不要令两个类型执行相同的类型转换:若a类有一个接受b类对象的构造函数,则不要在b类定义转换目标是a类的类型转换运算符。
当我们调用重载函数时,若两个或多个类型转换都提供了相同的可行匹配,则可能会引起二义性。
struct a{
a(int);
};
struct b{
b(int);
};
void m(const a&);
void m(const b&);
m(10); //二义性错误,是m(a(10))还是m(b(10))
调用者可以显式地构造正确的类型来消除二义性,但这意味着程序的设计存在不足。
m(a(10));
当调用重载函数时,若有多个类型转换都提供了可行匹配,在这个过程中,忽略标准类型转换的级别。
9.3、函数匹配与重载运算符
重载的运算符也是重载的函数,通用的函数匹配规则同样适用于判断在给定的表达式中该使用哪种运算符。
当我们使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。
如果我们对同一个类提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。
class small{
friend small operator+(const small&,const small&);
public:
samll(int = 0); //转换源为int的类型转换
operator int() const {} //转换目标为int的类型转换
private:
std::size_t val;
};
samll s1 = 3; //正确,3转换成small类型
int i = s1 + 0; //二义性错误,是s1转换成int,然后对两个int执行内置加法运算符
//还是将0转换成small类型,然后使用small的加法运算符