14. 重载运算与类型转换
14.1基本概念
重载的运算符是具有特殊名字的函数,它由关键字 operator与后面定义的符号组成,重载函数包含返回类型,参数列表以及函数体。重载运算符的参数数量与该运算符的作用的运算对象数量一样多。
如果一个运算符函数是成员函数,则其第一个运算对象绑定到this指针上。
注意:
- 对于一个运算符函数来说,它或者是类的成员或者至少含有一个类类型的参数。
- 同时只能重载已有的运算符,而不能发明新的符号。
- 对于一个重载的运算符来说,其优先级与结合律与对应的内置运算符保持一致
直接调用一个重载运算符
通常情况下,我们将运算符作用于类型正确的实参,从而以这种间接方式调用重载的运算符函数。然而,我们也能像普通函数一样调用运算符函数,然后传入数量正确类型恰当的实参
data1+data2; //普通的表达式
operator+(data1,data2);
这两次调用是等价的,他们都调用了非成员函数operator+,传入data1为第一个实参,传入 data2为第二个实参。
接下来像调用其他成员一样显示的调用成员运算符,具体做法是,首先制定运行函数对象和指针的名字,然后使用运算符访问希望调用的函数。
data1 += data2; //基于调用的表达式
data1.operator+=(data2); //对成员运算符函数的等价调用
某些运算符不应该被重载
通常情况下不应该重载逗号,取地址,逻辑与和逻辑或运算符。
赋值和复合运算符
赋值运算符的行为与复合版本的类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应返回左侧对象的一个引用。重载的赋值应该继承非违背其内置版本的含义。
选择作为成员还是非成员
准则:
- 赋值(=),下标([ ]),调用(())和成员访问箭头(->)运算符必须是成员。
- 复合赋值运算符一般来说是成员,但并非必须。
- 改变运算状态的运算符或者给定类型密切相关的运算符,如递增,递减和解引用运算符,通常应该是成员、
- 具有对称性的运算符可以转换为任意运算对象,如算术,相等性,关系,位运算符等,因此它应该包含普通的非成员函数
14.2 输入和输出运算符
14.2.1重载输出运算符<<
Tips:通常输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应当打印换行运算符。
输入输出运算符必须是非成员函数。
14.2.2重载输入运算符
输入时执行错误,和标识错误
标识错误:输入运算符可能检测bookNo是否符合格式,
14.3算术和关系运算符
14.3.1相等运算符
注意事项:相等运算符和不等运算符应该同时定义。
14.4赋值运算符
一个赋值运算符的例子
StrVec &StrVec::oprerator=(initializer_list<string> il){
//alloc_n_copy分配内存空间
auto data = alloc_n_copy(il.begin(), il.end());
free();
element = data.first;
first_free = cap = data.second;
return *this
14.5 下标运算符
一般定义为operator[]
一般需要定义下标运算符的常量和非常量版本,同时,已所访问元素的引用作为返回值。
例如
class StrVec{
public:
string& operator[](size_t n){
return elements[n];}
const string& operator[](size_t n) const {
return elements[n];}
private:
string *elements;
14.6递增和递减运算符
区分前置和后置运算符,后置版本接受一个额外的(int)形参
对于后置版本来说,在递增对象之前需要记录对象的状态。
14.7 成员访问运算符
14.8 函数调用运算符
如果重载了函数调用运算符,也可以像使用函数一样使用该类的对象。因为这样的类同时也能储存状态。
struct absInt{
int operator() (int val) const{
return val<0? -val: +val;
};
这个类只定义了一种操作,函数调用运算符,它接受一个Int类型的实参,然后返回实参的绝对值。
令一个absInt对象作用于一个实参列表
int i = - 42;
absInt absObj;
int ui = absObj(i);
如果定义了调用运算符,则该类的对象称为函数对象。
14.8.1 lamba为函数对象
14.8.2 标准库定义的函数对象
14.8.3可调用对象与function
由于存在几个调用对象共享同一种调用形式的现象。因此,需要定义一个函数表用于储存这些可调用对象的指针。这时候需要调用function的标准库来解决问题。
function是一个模板,在创建具体类型时必须提供额外的信息
function<int(int,int)>
对于不同类型有
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
14.9 重载,类型转换与运算符
14.9.1类类型转换
类型转换运算符是类的一种特殊成员函数,它负责将一个类型的值转换为其他类型,一般如下
operator type() const
其中type表示某种类型。类型运算符可以面向任意类型进行定义。(除了void)因此,不允许转换为数组或函数类型。但允许转换为指针类型或引用类型。
定义含有类型转换的运算符
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; }
private:
std::size_t val;};
Small int 定义了向类类型的转换和从类类型像其它类型的转换。其中,构造函数将算术运算符的值转换为SmallInt对象。
SmallInt si;
si = 4; //首先将4隐式的转换为smallInt,然后调用operator=函数
si + 3; //将si转换为隐式的int再调用加法。
由于类型转换运算符是隐式执行的。因此无法传递实参,也不能使用任何形参。同时,每个类型转换函数返回一个对应类型的值。
class SmallInt;
operator int(SmallInt&) //错误:不是成员函数
class SmallInt{
public:
int operator int() const; //错误,指定返回类型
operator int(int = 0) const; //错误,参数列表不为空
operator int*() const{ return 42; }
由于类型转换运算符可能产生意外,为了防止这种意外发生,往往采用显示运算符。
class SmallInt{
public:
//编译器不会自动执行这一类型的转换
explict operator int() const{ return val}
};
此时只能显示的进行类型转换
static_cast<int>si + 3;
当然,这些规定存在着一个例外。当表达式出现在下列位置时转换将隐式的执行。
- if,while与do语序的条件部分
- for语句的条件表达式
- 逻辑非,与,或运算符的运算对象
- 条件运算符的条件表达式
转换为bool
无论在什么时候在条件中使用流对象,都会使用I/O类型的operator bool
while(cin>>value);
while语句输入条件执行运算符,它负责将输入读到value并返回cin,为条件求值,cin被istream operator bool进行了隐式转换,真返回true;
14.9.2 避免二义性类型转换
如果一个类中包含一个或多个类型转换。则必须保证类类型和目标类型之间只有一种转换方式。
在两种情况下会产生二义性。第一种情况是两个类提供相同的类型转换。第二种情况是定义了多个转换规则,而这些规则可以通过其他规则联系在一起。
要正确的设计类的重载运算符,转换构造函数及类型转换函数。
(1)不要令两个类执行相同的类型转换,如果Foo类有一个接受Bar类对象的构造函数,则不要在Bar类中定义Foo的转换构造符。
(2)避免转换的目标是内置算术类型的类型转换,特别是定义了一个转换成算术类型的类型转换时。不要再定义接受算术类型的重载运算符。如果用户需要使用这样的运算符,则类型转换操作将转换你类型的对象,然后使用内置的运算符。不要定义转换到多种类型的算术转换。
重载函数与用户定义的类型转换。