操作重载与类型转换
1 基本概念
重载运算符是具有特殊名字的函数:其名字由关键字operator
和其后要定义的运算符号共同组成。
可以被重载的运算符如下:
符号 | 符号 | 符号 | 符号 | 符号 | 符号 |
---|---|---|---|---|---|
+ |
- |
* |
/ |
% |
^ |
& |
| |
~ |
! |
, |
= |
< |
> |
<= |
>= |
++ |
-- |
<< |
>> |
== |
!= |
&& |
|| |
+= |
-= |
/= |
%= |
^= |
&= |
|= |
*= |
<<= |
>>= |
[] |
() |
-> |
->* |
new |
new[] |
delete |
delete[] |
不能被重载的运算符: ::
.*
.
? :
某些运算符不应该被重载
由于某些运算符指定了运算对象求值的顺序,因此关于运算对象求值顺序的规则无法应用到重载的运算符上,特别是,逻辑与运算符、逻辑或运算符、逗号运算符的运算对象求值顺序规则无法保留下来,除此之外,&&和||运算符的重载版本也无法保留内置运算符的短路求值属性。
即:通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符
赋值和复合赋值运算符
赋值运算符的行为和复合版本的类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回其左侧运算对象的一个引用。
2 输入和输出运算符
2.1 重载输出运算符<<
一般情况下,输出运算符的第一个形参是一个非常量ostream
对象的引用,之所以ostream
是非常量是因为向流写入内容会改变其状态;而第二个参数一般是常量引用,打印对象不会改变对象的内容。operator<<
一般返回其ostream
形参。
编写Sales_data
的输出运算符,示例如下:
ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
输出运算符主要负责打印对象内容而非控制格式,因此尽量减少格式化操作,如打印换行符等。
2.2 重载输入运算符>>
一般情况下,输入运算符的第一个形参是运算符将要读取的流的引用;而第二个参数将要读入到的(非常量)对象的引用。
编写Sales_data
的输入运算符,示例如下:
istream &operator>>(istream &is, Sales_data &item)
{
double price;
is >> item.bookNo >> itme.units_sold >> price;
if (is) //检查输入是否成功
item.revenue = item.units * price;
else
item = Sales_data(); //输入失败:对象被赋予默认的状态
return is;
}
3 算术和关系运算符
3.1 相等运算符
检查两个对象是否相等,编写两个Sales_data对象是否相等,示例如下:
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() &&
lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs);
}
设计准则:
- 若判断一个类含有判断两个对象是否相等的操作,则定义成
operator==
比定义为一个普通的函数更好,也更容易使用标准库容器和算法。 - 若类定义了
operator==
,则该运算符应能判断一组给定的对象中是否含有重复数据 - 若类定义了
operator==
,则该类也应该定义operator!=
4 下标运算符
表示容器的类通常可以通过元素在容器中的位置访问元素。下标运算符通常以访问元素的引用作为返回值,这样可以使下标出现在赋值运算符的任意一端。如果一个类包含下标运算符,则通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。示例如下:
class StrVec{
public:
std::string& operator[](std::size_t n)
{ return elements[n]; }
const std::string& operator[](std::size_t n) const
{ return elements[n];
private:
std::string *elements; //指向数组首元素的指针
}
5 递增和递减运算符
定义前置递增/递减运算符
在StrBlobPtr类中定义递增和递减运算符,示例如下:
//前置运算
class StrBlobPtr{
public:
//递增和递减运算符
StrBlobPtr& operator++(); //前置运算符
StrBlobPtr& operator--();
}
//前置版本:返回递增/递减对象的引用
StrBlobPtr& StrBlobPtr::operator++()
{
check(curr, "increment past end of StrBlobPtr");
++curr; //将curr在当前状态下向前移动一个元素
return *this;
}
StrBlobPtr& StrBlobPtr::operator--()
{
//如果curr是0,则继续递减它将产生一个无效下标
--curr; //将curr在当前状态下向前移动一个元素
check(curr, "increment past end of StrBlobPtr");
return *this;
}
后置版本接受额外的(不被使用)一个int类型的形参,其作用是区分前置版本和后置版本的函数,示例如下:
//后置运算
class StrBlobPtr{
public:
//递增和递减运算符
StrBlobPtr& operator++(int); //后置运算符
StrBlobPtr& operator--(int);
}
//后置版本:递增/递减对象的值但是返回原值
StrBlobPtr StrBlobPtr::operator++(int)
{
StrBlobPtr ret = *this; //记录当前的值
++*this; //先前移动一个元素
return ret; //返回之前记录的状态
}
StrBlobPtr& StrBlobPtr::operator++()
{
StrBlobPtr ret = *this; //记录当前的值
++*this; //向后移动一个元素
return ret; //返回之前记录的状态
}
6 成员访问运算符
在迭代器及智能指针中常常用到解引用运算符(*
)和箭头运算符(->
)。在StrBlobPtr类添加这两种运算符:
class StrBlobPtr
{
public:
std::string& operator*() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; //(*p)是对象所指的vector
}
std::string* operator->() const
{
//将实际工作委托给解引用运算符
return & this->operator*();
}
}
7 函数调用运算符
7.1 lambda是函数对象
表示lambda以及相应捕获行为的类
当一个lambda表达式通过引用捕获变量时,将由出席负责确保lambda执行时引用所引的对象确实存在。lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员,示例如下:
//lambda的作用是找到第一个长度不小于给定值的string对象
//获得第一个指向满足条件的迭代器,该元素满足size() is >=sz
auto wc = find_if(words.begin(), words.end(),
[sz](const string &a)
{return a.size() >= sz;});
//该lambda表达式产生的类将形如:
class SizeComp {
SizeComp(size_t n): sz(n) {} //该形参对应捕获的变量
//该调用运算符的返回类型、形参和函数体都与lambda一致
bool operator()(const string &s) const
{ return s.size() >= sz; }
private:
size_t sz; //该数据成员对应通过值捕获的变量
}
lambda表达式产生的类不包含默认构造函数、赋值运算符及默认析构函数
7.2 标准库定义的函数对象
标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类。例如,plus类定义了一个函数调用运算符用于对一对运算对象执行+的操作;modulus类定义了 一个调用运算符执行二元的%操作;equal to类执行==,等等。下列类型定义在functional
头文件中。
算术 | 关系 | 逻辑 |
---|---|---|
plus<type> |
equal_to<type> |
logical_and<type> |
minus<type> |
not_equal_to<type> |
logical_or<type> |
multiplies<type> |
greater<type> |
logical_not<type> |
devides<type> |
greater_equal<type> |
|
modulus<type> |
less<type> |
|
negate<type> |
less_equal<type> |
使用示例如下:
plus<int> intAdd; //可执行int加法的函数对象
negate<int> intNegate; //可对int值取反的函数对象
int sum = intAdd(10, 20); //等价于sum = 30
sum = inAdd(10, intNegate(10)); //sum = 0
在算法中使用标准库函数对象
==需要特别注意的是,标准库规定其函数对象对于指针同样适用。==通过比较指针的内存地址来sort指针的vector,使用标准库函数对象来实现,示例如下:
vector<string *> nameTable; //指针的vector
//错误:nameTable中的指针彼此之间没有关系,所以<将产生未定义的行为
sort(nameTable.begin(), nameTable.end(),
[](string *a, string *b) {return a < b; });
//正确:标准库规定指针的less是定义良好的
sort(nameTable.begin(), nameTable.end(), less<string*>());
关联容器使用less<key_type>对元素排序
,因此可以调用一个指针的set或者再map中使用指针作为关键值而无须直接声明less。
7.3 可调用对象与function
C++语言中几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。定义一个函数表用于储存指向指向这些可调用对象的“指针”,当程序需要执行某个特定的操作时,从表中查找该调用的函数即可。在C++中函数表可以通过map来实现。例如使用一个表示运算符符号的string 对象作为关键字;使用实现运算符的函数作为值,当需要求给定运算符的值时,先通过运算符索引map,然后调用找到的那个元素。示例如下:
//构建从运算符到函数指针的映射关系,其中函数接受两个int,返回一个int
map<string, int(*)(int, int)> binops;
//将add的指针添加到binops中:
binops.insert({"+", add}); //{"+", add}是一个pair
标准库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<T>
的成员的类型
类型 | 描述 |
---|---|
result_type |
该function类型的可调用对象返回的类型 |
argument_type |
当T有一个或两个实参时定义的类型。如果T只有一个实参,则argument_type是 |
first_argument_type` | 该类型的同义词,如果T有两个实参,则两个实参的类型分别为 |
second_argument_type |
first_argument_type和second_argument_type |
function使用示例如下:
function<int(int, int)> f1 = add; //函数指针
function<int(int, int)> f2 = devide(); //函数对象类的对象
function<int(int, int)> f3 = [](int i, int j) //lambda表达式
{ return i * j; };
//使用function类型操作定义map:
map<string, function<int(int, int)>> binops;
接下来就可以把所有课调用的对象,包括函数指针、lambda或者函数对象在内,都添加到这个map中:
map<string, function<int(int, int)>> binops = {
{"+", add}, //函数指针
{"-", std::minus<int>()}, //标准函数对象
{"/", devide()}, //用户定义的函数对象
{"*", [](int i, int j) { return i * j; }}, //未命名的lambda
{"%", mod}, //命名了的lambda对象
};
//使用示例:
binops["+"](10, 5); //调用add(5, 5)
binops["-"](10, 5); //调用minus<int>对象的调用运算符
重载的函数与function
==不能直接将重载函数的名字存入function中,==示例如下:
int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert({"+", add}); //错误:没有指明具体的add
解决上述问题的方法之一:存储函数指针而非函数的名字
int (*fp)(int, int) = add; //指针所指的add是接受两个int的版本
binops.insert({"+", fp}); //消除二义性问题
解决上述问题的方法之二:使用lambda来消除二义性
//使用lambda来指定使用的add版本
binops.insert({"+", [](int a, int b) {return add(a, b);}});