第14章 重载运算与类型转换
1. 基本概念
选择作为成员或者非成员
- =、[]、()、->运算符必须是成员。
- 改变对象状态或者与给定类型密切相关的运算符,如递增、递减和解引用等运算符通常应该是成员。
- 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,通常应该是非成员函数。
string s = "World";
string t = s + "!"; //正确
string u = "hi" + s; //如果+是string的成员,则产生错误
2. 输入和输出运算符
- 输入输出运算符必须是非成员函数。
- IO类型不可拷贝,因此传参必须由by-reference传参。
2.1 重载输出运算符
- 输出运算符应该尽量减少格式化操作,即不应该打印换行符。
- 通常IO运算符一般被声明为友元。
ostream &operator<<(ostream& os, const Sales_data &item)
{
os<<item.isbn()<<" "<<item.units_sold<<" "<<item.revenue<<" "<<item.avg_price;
return os;
}
2.2 重载输入运算符
- 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
- 当读取操作发生错误时,输入运算符应该负责从错误中恢复。
istream &operator>>(istream &is, Sales_data &item)
{
double price;
is>>item.bookNo>>item.units_sold>>price;
if(is) //检查是否输入成功
item.revenue = item.units_sold*price;
else
item = Sales_data(); //输入不成功,对象被赋予默认状态
}
3. 算术和关系运算符
- 通常情况下,把算术和关系运算符定义为非成员函数以允许对左侧或右侧的运算对象进行转换。
- 此类运算符通常不需要改变运算对象的状态,所以形参通常为常量引用。
3.1 算术运算符
- 通常算术运算符会计算它的两个运算对象并得到一个新值,而这个值常常位于一个局部变量之内,操作完成后返回其副本作为结果。
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum+=rhs;//如果定义了+=运算符,则调用+=运算符效果更好
return sum;
}
3.2 相等运算符
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;
}
4. 赋值运算符
- 重载赋值运算符必须定义为成员函数。
- 新的赋值运算符返回左侧运算对象的引用。
Sales_data &operator=(const Sales_data &rhs)
{
bookNo = rhs.bookNo;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return *this;
}
4.1 复合赋值运算符
- 倾向于将+=、-=等复合赋值运算符定义为类成员。
- 返回左侧对象的引用。
Sales_data &operator+=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
5. 下标运算符
- 表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义operator[]。
- 下标运算符必须是成员函数。
- 下标运算符通常以所访问的元素的引用作为返回值,这样可以出现在赋值运算符的任意一端。
- 通常定义两个版本的下标运算符:一个返回普通引用,另一个是类的常量成员并返回常量引用。
class StrVec
{
public:
StrVec();
~StrVec();
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; //指向数组首元素的指针
};
6. 递增和递减运算符
- 因为递增和递减运算符改变的是所操作对象的状态,因此建议将其设定为成员函数。
- 递增递减运算符分为前置运算符和后置运算符,两者应有区分。
- 前置运算符应该返回递增或者递减后对象的引用;后置运算符返回对象的原值,而非引用。
- 前置运算符需要检查索引有效性,后置运算符则不需要。
- 区分前置和后置运算符:前置递增/递减运算符在形式上和后置运算符一致,为了区分,后置版本接受一个额外的(不被使用)int类型的实参。
class StrBlobPtr
{
public:
...
//前置递增递减运算符
StrBlobPtr& operator++();
StrBlobPtr& operator--();
//后置递增递减运算符
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
private:
...
std::size_t curr; //在数组中的当前位置
};
StrBlobPtr& StrBlobPtr::operator++()
{
check(); //检查有效性
++curr;
return *this;
}
StrBlobPtr StrBlobPtr::operator++(int)
{
//check(); //无需检查有效性
StrBlobPtrf ret = *this;
++*this;
return ret;
}
StrBlobPtr p(al);
p.operator++(0); //显式调用前置版本
p.operator++(); //显式调用后置版本
7. 成员访问运算符
- 在迭代器及智能指针类中常常用到解引用运算符(*)和箭头运算符(->)。
- 箭头运算符必须是类的成员,解引用运算符通常也是类的成员。
- 箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
(*point).mem; //point是一个内置的指针类型
point.operator()->mem; //point是类的一个对象
class StrBlobPtr
{
public:
...
std::string& operator*() const
{
auto p = check(); //检查有效性
return (*p)[curr]; //返回curr所指元素的一个引用
}
std::string* operator->() const
{
return &this->operator*(); //返回解引用结果元素的地址
}
private:
...
;
8. 函数调用运算符
- 函数调用运算符必须是成员函数,。一个类可以有多个不同版本的调用运算符,相互之间应该在参数数量或者参数类型上有所区分。如果类定义了调用运算符
- ,则该类的对象称作函数对象,其行为“像函数一样”。
- 函数对象除了operetor()之外也可以包含其他成员,这些成员被用于定制调用运算符中的操作。函数对象常常作为泛型算法的实参。(具体参阅原书P507)
struct absInt
{
int operator()(int val) const
{
return val > 0 ? val : -val;
}
};
//调用
int i = -42;
absInt absObj;
int ui = absObj(i); //即使absObj是一个对象,我们也能像函数一样使用该对象
可调用对象与function
- C++中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。和其他对象一样,可调用对象也有类型。如每个lambda有它自己唯一的(未命名)类类型。
- 两个不同类型的可调用对象可能共用一种调用形式例如:int(int, int)。
int add(int i, int j) //普通函数
{
return i + j;
}
auto mod = [](int i, int j) //lambda表达式,其产生一个未命名的函数对象类
{
return i % j ;
}
struct divide
{
int operator()(int i, int j) //函数对象类
{
return i/j;
}
};
我们可能使用这些可调用对象构建一个简单的桌面计算器,这可以通过函数表来实现,用于存储指向这些可调用对象的“指针”。在C++中,函数表容易通过map来实现。
map<std::string, int(*)(int, int)> binops; //map为从运算符到函数指针的映射
binops.insert({"+", add}); //正确,add是一个函数指针
binops.insert({"%", mod}); //错误,mod不是一个函数指针
可以通过标准库function类型解决上述问题。
#include<functional>
function<int(int, int)> f1 = add;
function<int(int, int)> f2 = divide();
function<int(int, int)> f3 = [](int i, int j){return i%j};
//重新定义map
map<std::string, function<int(int, int)>> binops = {
{"+", add},
{"-", std::minus<int>()},
{"/", divide()},
{"*", [](int i, int j){return i*j}} //未命名的lambda
{"%", mod} //命名的lambda
};
//调用
binops["+"](10, 5);
binops["%"](10, 5);
binops["*"](10, 5);
注意:我们不能直接将重载函数的名字存入function类型的对象中,可通过存储函数指针而非函数名字来解决。
int add(int i, int j){return i + j;}
Sales_data add(const Sales_data&, const Sales_data&);
binops.insert("+", add); //错误,哪个add?
//解决方法
int (*f1)(int, int) = add;
binops.insert("+", f1);
9. 重载、类型转换与运算符
- 之前我们知道,由一个实参调用的非显式构造函数定义了一种隐式的类型转换,这种构造函数将实参类型的对象转换成类类型。我们通过转换构造函数和类型转换运算符共同定义类类型转换。
- 类类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转化成其他类型。一般形式如下:
operator type() const;
其中type表示某种类型。 - 类型转换运算符可以面向任意类型,不允许转化为数组或者函数,但可以转化为指针(包括数组指针和函数指针)或者引用类型。
- 一个类类型转换函数必须是类的成员函数,它不声明返回类型,形参列表也必须为空。类型转换函数通常应该是const。
class SmallInt
{
public:
SmallInt(int i = 0) :val(i)
{
if(i<0||i>255)
throw std::out_of_range("Bad SmallInt value");
}
operator int() const {return val}; //把SmallInt对象转化为int
private:
std::size_t val;
}
//调用
SmallInt si;
//编译器一次只执行一个用户定义的类型转换,但可以与内置类型转换一起使用
si = 3.14;
si + 3.14; //si转换为int,同时3.14转换为int
显式类型转换
隐式类型转换使用不当可能产生异常结果,为了防止这样的异常发生,C++引入了显式的类型转换运算符。
class SmallInt
{
public:
...
explicit operator int() const {return val}; //把SmallInt对象转化为int
private:
std::size_t val;
}
//调用
SmallInt si = 3;
si+3; //错误,不存在隐士类型转换
static_cast<int>(si)+3; //正确,显式类型转换
但上面的情况有一个例外,当表达式被用作条件,则编译器会自动调用显示的类型转换。也就是显式类型转换被隐式执行了。
- if、while、do语句的条件部分
- for的条件部分
- !、||、&&的运算对象
- ?:的表达式
WARNING:
- 不要令两个类执行相同的类型转换,否则容易出现二义性。如果Foo类有一个接受Bar类对象的构造函数,则不要在Bar中再定义转换目标是Foo类的类型转换运算符。
- 避免转换目标是内置算术类型的类型转换。
- 如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了冲在的运算符,则将会遇到重载运算符与内置运算符的二义性问题。
class SmallInt
{
friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int = 0); //转换int为类类型的构造函数
operator int() const {return val}; //把SmallInt对象转化为int
private:
std::size_t val;
}
//调用
SmallInt s1, s2;
SmallInt s3 = s1 + s2; //使用重载的operator+
int i = s3 + 0; //可以把0转化为SmallInt,也可以把s3转化为int,二义性错误