运算符重载
运算符的重载形式
C++程序设计的重要基础是类和对象,允许用户自己定义新的类型
C++允许重载现有的运算符,使这些简单易用、众所周知的运算符能够直接作用于用户自定义的类对象,扩大了运算符的作用范围
在C++中,很多的运算符被当做是函数,这称为“运算符函数(operator function)”,设运算符为@,那么它对应的运算符函数的原型可以形式化地表示为:
返回值类型 operator @(参数列表);
返回值和参数的设定必须与重载的运算符的含义相匹配
首先需要补充操作数和运算符的概念:
运算符和操作数
• 运算符的符号化定义为@,它代表+、-等常规运算符;
• 在单目运算@lhs或lhs@中,lhs称为“操作数”;
• 在双目运算lhs @ rhs中,lhs称为“左操作数”,rhs称为“右操作数”
在绝大多数情况下,参与重载运算的操作数(至少其中一个)是一个类对象,而重载的@运算函数与这个类相关。
(注:比如我们不能只对两个char *类型的对象进行运算符重载,因为我们至少需要一个类对象!)
运算符的重载语法
以类的成员/友元函数进行重载
运算符作为类的成员函数进行重载
考虑为complex类重载+=运算符。这个运算符的特点如下:
• 是双目运算符,其形式为lhs += rhs;
• 运算符结果反映在了lhs上,而不是产生一个新结果;
• rhs始终不会被改变;
运算的结果可以作为左值使用。
complex& complex::operator+=(const complex& c)
{
real += c.real;
imag += c.imag;
return *this;
}
c1 += c2;
代码解释:
1.运算符函数+=是类的成员。(所以会有complex::)
2.函数的参数是右操作数;而左操作数是启动+=函数的对象本身。(此处用const以保证右操作数不被改变)
3.+=返回左操作数对象本身的引用,这样就能是返回结果当做左值使用。(此处之所以不用complex而选择用引用是为了避免对复制构造函数(思考:什么时候会调用复制构造函数?)的一次无谓调用从而减少了时间的消耗;同理参数传入引用亦是同样的道理)
运算符作为类的友元函数进行重载
complex operator+(const complex& c1, const complex& c2)
{
return complex(c1.real + c2.real, c1.imag + c2.imag); //调用构造函数
}
代码解释:
两个操作数作为常量参数,因此都不能改变。(加const)
返回一个临时对象(匿名、常量)(调用复制构造函数)。该对象不能作为左值。
故:表达式c1 + c2(隐式调用) 会被解释为如下形式:::operator+(c1, c2);(显示调用)(注:此处::运算符说明operator+是全局函数。)
运算符的重载规则
重载运算符规则
1)大多数系统预定义的运算符可以通过运算符重载函数定义它们对用户定义类型进行操作的新的含义。只有以下少数的C++运算符不能重载:
:: (作用域解析运算符)
?: (条件运算符)
. (成员选择运算符)
.* (成员选择运算符)
2)重载运算符时:
• 不能改变它们的优先级
• 不能改变它们的结合性
• 不能改变这些运算符所需操作数的数目
3)重载的运算符必须和用户自定义的类对象一起使用,其参数至少应有一个是类对象(或类对象的引用)
4)用于类对象的运算符一般必须重载,只有两个运算符:赋值运算符=和地址运算符&可以不必重载
5)应当使重载运算符的功能类似于该运算符作用于标准类型数据时所实现的功能
例如:2 + 3永远都被被编译器解释为两个整数相加,而不能有别的含义。
6)重载的运算符函数不能是类的静态成员。
重载运算符函数的参数和返回值设定建议
1.运算符重载为成员还是友元的建议
=,(),[],-> 必须为成员函数
单目运算符 建议为成员函数
复合赋值运算符 建议为成员函数
其他双目运算符 建议为友元函数
2)对于重载的单目运算符函数:
• 作为成员重载时,函数没有参数,运算作用在lhs上;函数返回lhs的左值引用;
• 作为友元重载时,函数有一个参数(并且是左值引用),运算作用在参数对象上;函数返回参数对象的左值引用。
• 例外
• 当@是后缀++/–时
3)对于作为成员重载的双目运算符(必须有充分的理由这么做):
• 函数有一个参数,这个参数就是rhs
4)对于作为友元重载的双目运算符:
• 有两个参数(即左右操作数),并且建议为常量,且这两个参数中至少有一个是将该运算符函数作为友元对待的类的对象;
• 函数产生一个新值,即返回值是一个对象(非引用非指针)。这会引起复制构造函数的调用
5)对于关系和逻辑运算符:
它们应该产生一个bool类型的结果。如果使用的编译器不支持bool类型,那么应该返回一个替代的整型值:非0表示真,0表示假
6)如果作为成员重载的运算只是读取对象的属性而不会改变它们,那么建议将该函数设为常成员(函数名后+const)
常用运算符的重载
赋值运算符的重载
两个同类对象之间的复制有两条途径可走:
一是可以在定义对象时利用复制构造函数完成,例如:array a1, a2(a1);
二则是在程序代码中更常见到的赋值操作。例如:array a1(10), a2; a2 = a1;
这两种赋值方式一样吗?
显然是不一样的,第二种采用了最直接的方式完成两个对象间的赋值:逐成员(静态数据成员除外)复制
赋值运算符是一个典型的双目运算符,它的左操作数是个左值,右操作数却是左右值不限。因此,赋值运算符函数最好(其实是只能)作为类的成员重载,其唯一的参数最好是右值对象的常量引用,而其返回值应该是左值对象的引用
complex& complex::operator=(const complex& c)
{
real = c.real;
imag = c.imag;
return *this;
}
注意,赋值运算符函数完成的功能与复制构造函数的几乎完全一样,唯一的区别似乎只是它们的返回值不同。
那么为什么还需要重载赋值运算符呢?
二者的相似性源于它们都属于复制控制的范畴。但二者的最大区别在于对象的存在性:当复制构造函数调用时,复制目标对象(lhs)正在被创建中;而在赋值时,lhs已经存在了。
复合赋值运算符要完成两项操作:
• 释放复制目标原有的资源
• 将复制源的资源全部复制到复制目标中
array& array::operator=(const array&& a)
{
delete []head;
capacity=a.capacity;
head=new int[capacity];
for(size_t i=0;i<a.len;++i)push_back(a.head[i]);
return *this;
}
其实这里我们可以发现,从某个意义上而言,因为复制构造函数的存在,我们可以完全不必要编写重载赋值运算符的代码,这也解释了前文提到的赋值运算符并不一定需要重载的缘由,但我们往往还是要编写的原因就在于,由于程序的可读性需求,我们需要让我们所编写的类更贴近于正常用户的使用习惯,故往往会重载赋值运算符。
考虑到如果复制源是个临时对象,意味着它即将失效,它的资源也将被释放。
我们引入转移赋值运算符
array& array::operator=(array&& a)
{
std::swap(capacity, a.capacity);
std::swap(len, a.len);
std::swap(head, a.head);
return *this;
}
代码解释:
重载复合赋值运算符
复合赋值运算符要完成两项操作:
1.完成指定的运算;
2.完成赋值
算术运算符的重载
1. 单目运算符
单目运算符只需要一个操作数,并且最好作为类成员被重载。对任意单目运算符@,其重载形式为:
class 类名
{
public:
返回类型 operator@();//此处重载函数没有参数
}
至于函数的返回值,应该视运算符具体的语义而定。总的原则是:
• 如果操作产生一个临时值结果,那么函数应该返回对象的值。例如,负号运算符
• 如果操作产生一个左值(其实就是操作数本身),那么函数应该返回操作数的引用。
例:
- 正号运算符
- 前缀++和–
2. 双目运算符
除了赋值(含复合赋值)运算符外,几乎所有的双目运算符都应该作为类的友元进行重载。具体的语法形式为:
class 类名
{
public:
friend 返回类型 operator@(const 类型1 &, const 类型2 &);
}
重载++和- -运算符
如果有int a = 0,那么表达式
• ++a使a自加为1,并且整个表达式的值等于a的值,也就是1。可以这么理解,++a的结果是a本身;
• a++使a自加为1,但整个表达式的值等于a自加前的值,也就是0。这就是说,a++的结果是个临时值。
• 前缀++应该以类的成员函数形式重载,其返回值是操作数对象本身的引用;
• 后缀++应该以类的成员函数形式重载,其返回值是操作数自加前的一个副本,是一个值结果。
但问题是:以类的成员函数形式重载的单目运算符没有参数,那么如何区分是前缀还是后缀呢?
complex& operator++()
{
++real; ++imag;
return *this;
}
complex operator++(int/*占位参数*/)
{
complex temp(this->real, this->imag);
++real; ++imag;
return temp;
}
答:利用占位符来区别
几种特殊运算符的重载
重载输入输出运算符>>和<<
class complex //简化版
{
friend ostream& operator<<(ostream& os, const complex &c);
friend istream& operator>>(istream& is, complex &c);
};
注:只能作为友元函数来重载!
(思考:why? ——一言以蔽之:为了复合人们的操作习惯,更多详情:https://blog.csdn.net/exaggeration08/article/details/78000065)
重载类型转换运算符
在运算过程中、在函数参数传递中、在函数返回值过程中,类型转换是经常发生的。这些转换大体上可以分为三类:
• 标量类型向类类型转换;
• 类类型向标量类型转换;
• 类类型向类类型转换。
1. 标量类型向类类型转换
在定义对象的时候,可能发生标量类型到类类型的转换。例如:
complex c = 3.0;
编译器会调用类的构造函数来完成这个转换:
complex c = complex(3.0); //用缺省参数调用complex(double,double)
即使在赋值语句当中,构造函数也能发挥作用:
c = 5.0;
这等价于
c = complex(5.0);
这种标量向类类型的转换过程称为“装箱(boxing)”。
注:如果构造函数被说明成是explicit,则这条语句就是错误的。
2. 类类型向标量类型转换
与装箱相反的过程称为“拆箱(unboxing)”。例如:
double d = c;
这类重载的类型转换运算符函数有如下特征:
• 不能为函数指定返回值类型;但函数体中的return语句必须返回一个与运算符同名类型的实例;
• 函数必须是类的成员,并且没有参数。
class complex //简化版
{
private:
double real, imag;
public:
operator double()
{ return sqrt(real * real + imag * imag); }
};
3. 类类型向类类型转换
类类型转化到其它类类型也是可能的。
例如,复数的表示有两种:
• 直角坐标表示法。用实部和虚部来表示,行为x+iy;
• 指数表示法,用指数reia形式来表示,其中r为模,a为幅角。
两种表示法之间可以相互转换。
重载[]运算符
运算符[]只能以非静态成员函数的方式进行重载,其语法如下:
class 类名
{
public:
返回值类型 operator[](类型);
};
[]运算符一般为顺序容器重载。我们可以把顺序容器想象成一个可以动态生长的数组。这样,对顺序容器使用重载的[]可以获得分量的读写能力。
重载[]运算符可以在一定程度上解决下标越界问题
当发生了越界时,可以抛出异常来阻止更大错误的发生
重载指针运算符(待详细补充)
引入原因
如何避免孤悬指针?
如何安全释放内存?
C++98标准中的auto_ptr
auto_ptr<class_need_resource>p1(new class_need_resource());
scoped_ptr
scoped_array
scoped_ptr
shared_ptr
weak_ptr
intrusive_ptr
重载()运算符
运算符 ()称为“函数调用运算符”,只能以非静态成员函数的方式进行重载,其语法如下:
class 类名
{
public:
返回值类型 operator()(参数列表);
};
对于该类的一个对象obj,表达式obj()被编译器解释为
obj.operator()(参数列表);
可以看到,()作用在类对象上时,形式非常类似于函数调用,但“函数名”不是真正代表了函数,而是一个对象的名字。这种含有重载()运算符的对象称为“函数对象(function object)”。
class X { public: int operator ()(int i) { return i; } };
X x;
std::cout << x(0); //编译器解释为:x.operator()(0);
可调用对象
函数
int g_Minus(int i, int j)
{
return i - j;
}
lambda表达式
auto g_Minus = [](int i, int j){ return i - j; };
重载了()运算符的类对象
重载了()运算符的类对象
class Minus
{
int operator() (int i, int j)
{
return i - j;
}
};
std::function
一个函数包装器模板,最早来自boost库,对应其boost::function函数包装器。在c++ 11中,将boost::function纳入标准库中。该函数包装器模板能包装任何类型的可调用元素(callable element),例如普通函数和函数对象
包装普通函数
#include <iostream>
#include <functional>
using namespace std;
int g_Minus(int i, int j)
{
return i - j;
}
int main()
{
function<int(int, int)> f = g_Minus;
cout << f(1, 2) << endl;
return 1;
}
包装lambda表达式
#include <iostream>
#include <functional>
using namespace std;
auto g_Minus = [](int i, int j){ return i - j; };
int main()
{
function<int(int, int)> f = g_Minus;
cout << f(1, 2) << endl;
return 1;
}
包装函数对象
#include <iostream>
#include <functional>
using namespace std;
struct Minus{
int operator() (int i, int j)
{
return i - j;
}
};
int main()
{
function<int(int, int)> f = Minus();
cout << f(1, 2) << endl;
return 1;
}
ps:对于[],指针,()三种运算符的重载还需理解。