第十四章 重载运算符和类型转换
14.1 基本概念
一元运算符有一个参数.
二元运算符: 左侧运算对象传递给第一个参数, 右侧为第二个参数.
除了重载的函数调用运算符operator() 之外 都不能含有默认实参.
如果运算符函数是成员函数, 则第一个运算对象隐式的为this指针.
当运算符作用于内置类型的运算对象时, 不能改变该运算符的含义: 即:不能重定义int类型的+ 运算符
只能重载已有的运算符, 不能发明 创造.
可以被重载的运算符:
不能被重载的运算符:
如果类含有算术运算符或者位运算符, 则最好也提供对应的复合赋值运算符, 因为可能会使用+=运算符.
是否将运算符定义为成员/非成员函数的判断方法:
- 赋值=, 下标[], 调用 (), 成员访问箭头-> 必须是成员函数
- 符合运算符一般是成员, 但不是必须
- 改变对象状态的运算符或者与给定类型密切相关的, 如++, --, 解引用* 通常为成员函数
- 对称性的运算符可能转换任意一端的运算对象 如 算术, 相等性, 关系和位运算 通常是非成员函数, 即:可以任意交换两者运算位置的不能是成员函数, 成员函数默认第一个参数是this
14.2 输入和输出运算符
输出运算符应该打印换行符, 通常只负责打印内容, 不输出格式.
输出函数必须是非成员函数.
输入运算符必须处理输入可能失败的情况, 输出运算符不需要.
输入运算符需要处理输入错误的情况.
14.3 算数和关系运算符
14.3.1 相等运算符
如果定义了==, 也应该定义!=, 反之依然, 基本上是基于使用喜欢来的.
14.3.2 关系运算符
< > 的定义要符合实际需求, 需要通盘考虑类中个成员变量的实际情况.
14.4 赋值运算符
赋值运算符必须是成员函数, 返回一个左值.
注意 类似于 vector中的 ={} 的赋值情况
StrVec &StrVec::operator=(initializer_list<string> il){
auto data = alloc_n_copy(il.begin(),il.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
14.5 下标运算符
下标运算符必须是成员函数, 通常有两种版本的返回值: 普通引用和常量引用
14.6 递增和递减运算符
作为成员函数
前置版本的定义: 注意递增和递减定义中检查指针的顺序.
StrBlobPtr& operator++();
StrBlobPtr& operator--();
StrBlobPtr& StrBlobPtr::operator++() {
check(xxxx); // 检查curr是否已经指向了尾后位置, 否则将抛出异常
++curr;
return *this;
}
StrBlobPtr& StrBlobPtr::operator--() {
--curr; // 如果curr是0, 继续递减将产生一个无效下标
check(xxxx);
return *this;
}
后置版本的定义. 后置版本需要提供一个无用的int形参, 用于区分前置版本. 返回值是一个值而非引用.
// 此处提供的形参int 没有任何意义, 仅仅用于区别该版本函数是后置版本
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
// 此处的int 没有名字, 因为用不到 所以连名都不需要
StrBlobPtr& StrBlobPtr::operator++(int) {
StrBlobPtr ret=*this;
++*this;
return ret;
}
StrBlobPtr& StrBlobPtr::operator--(int) {
StrBlobPtr ret = *this;
--*this;
return ret;
}
前置,后置版本区别
- 前置返回引用, 后置返回值
- 前置需要验证有效性, 后置不需要
- 前置没有参数, 后置有一个无用形参用于区分前置还是后置
14.7 成员访问运算符
解引用 和 箭头 函数 都是成员函数.
string& operator*() const;
string* operator->() const;
箭头函数 等价于 (*p).mem 和 p.operator()->mem;
重载的箭头运算符 必须 返回类的指针 或者 自定义了箭头运算符的某个类的对象.
14.8 函数调用运算符
struct absInt {
int operator()(int val) const { return val < 0 ? -val : val; }
};
int i = -32;
absInt absObj;
int ui = absObj(i);
感觉像 对象 增加了一个默认函数.
必须是成员函数, 可以定义多个, 根据参数区别.
14.8.1 lambda 是函数对象
14.8.2 保准库定义的函数对象
一些运算符的类方法表示<functional>:
例如:
plus<int> intAdd;
int sum = intAdd(10,20); // sum = 30
其中一个用途:
sort默认使用<比较, 然后排序, 可以使用greater<T>() 更改sort 的默认排序比较运算符为>, 从而更改了sort排序的方法.
14.8.3 可调用对象与function
使用map 存储具有相同调用形式的函数. 所谓具有相同调用形式如:
int add(int, int)
int mod(int, int)
int divide(int, int)
等, 无论函数内部是如何执行的, 但是其共同特点就是 定义的时候 具有相同参数类型和个数, 具有相同的返回值类型. 这类函数可以使用统一的调用方式 int x(int, int). 这些函数可以放进一个map 中.
map<string, int(*)<int,int>> binmap;
binmap.insert({"+", add}); // add 是指向add函数的指针
但是 如何能够 把一个lambda也放进去呢? 由此出现了function类型, 可以理解为function 重新包装函数.
function<int(int, int)> fAdd = add;
function<int<int, int>> fDivide = divide();
function<int<int, int>> fLambda = [](int i, int j) {return i*j;};
此时 更改map的定义为
map<string, function<int(int, int)>> exeMaps;
exeMaps.insert({"+", add});
exeMaps.insert({"*", fLambda});
// 调用
exeMaps["+"](10, 10);
exeMaps["*"](2, 33);
即: 只要具有相同调用方式的函数, 包括lambda表达式, 都可以添加进map. 在调用是则可充分 展示此种方法的便利性.
重载的函数如何使用此种方法呢? 重载时 函数名 相同
int add(int a, int b) {return a+b;}
ClassA add(const ClassA&, const Class&);
map<string, function<int<int, int)>> exeMaps;
exeMaps.insert({"*", add}); //此处 无法确定 add 究竟指向的是哪一个
可以使用函数指针:
int (*fAdd)(int, int) = add; // 因定义是指明了函数的参数类型和返回值,
// 由此可以确定add是指的第一个函数
exeMaps.insert({"+", fAdd});
也可以包装一层lambda 以确定使用哪个 add:
exeMaps.insert({"+", [](int a, int b){return add(a, b);}});
14.9 重载 类型转换与运算符
14.9.1 类型转换运算符
负责将一个类类型的值转换成其他类型. 格式
operator type() const;
- 必须是类的成员函数
- 不能声明返回类型 没有必要, 目标类 已经说明了返回值的类型
- 形参列表必须为空 因为类型转换符是隐式执行的, 所以无法传递参数
- 通常应该是const
一个小例子:
class SamllInt {
public:
SamllInt(int i = 0) : val(i) {
if (i < 0 || i> 255)
throw std::out_of_range("Invalid value");
}
operator int() const { return val; } // 此处就是类型转换运算符
private:
std::size_t val;
};
// 使用
SmallInt s =3;
s+3; // 此处发生隐式转换, SamllInt -> int, 调用了int()
隐式类型转换运算符 需要谨慎, 否则 运算结果 将与预期结果相去万里.
class SmallInt{
public:
SmallInt(int i = 0) : val(i) {
if (i < 0 || i> 255)
throw std::out_of_range("Invalid value");
}
explicit operator int() const { return val; } // 此处就是类型转换运算符
private:
std::size_t val;
};
// 调用
SmallInt s = 3;
s+3; // 抛出类型不匹配的错误, 不会隐式调用int() 进行类型转换
static_cast<int>(s) + 3; // 显示调用int() 执行类型转换
显式调用被自动调用的地方: 被用作条件时:
- if while do的条件部分
- for 条件表达式中
- 逻辑非运算符!, 逻辑或运算符||, 逻辑与运算符&&
- 条件运算符 ? :
向bool的类型转换通常用在条件部分, 因此operator bool 一般定义成 explicit.
14.9.2 避免有二义性的类型转换
两种情况可能会产生二义性转换:
- A的构造函数可以把B转换为A, B中又定义转换为A的转换构造函数.
- 定义多个转换规则, 而设计的类型本身可以通过其他类型转换联系在一起.
对于算术类型 需要尤其小心, 尽量不要构建两个或以上 源/目标 是 算术类型 的转换.
除了显式的向bool类型的转换之外, 应当尽量避免定义类型转换函数, 并尽可能地限制那些非显式构造函数.
// 14.51
转换匹配顺序:
- 精确匹配
- 常量版本
- 类型提升
- 算术转换或指针转换 14.51 使用的算术转换
- 类类型转换
void calc(string a) { cout << "int a" << endl; };
void calc(LongDouble l) { cout << "LongDouble l" << endl; }
int main(int argc, char** argv) {
//14.51
double dval = 1.0;
calc(dval); // 将使用类类型转换, 结果为LongDouble l
}
// 如果 void calc(int a) calc(dval) // 将使用算术类型转换 结果为int a
// 如果 void calc(double d) { cout << "double d" << endl; } 结果 姜维 double d
14.9.3 函数匹配与重载运算符
表达式中运算符的后院函数集既包括成员函数, 也应该包括非成员函数.
class SmallInt {
friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int i = 0) : val(i) {
if (i < 0 || i> 255)
throw std::out_of_range("Invalid value");
}
explicit operator int() const { return val; } // 此处就是类型转换运算符
private:
std::size_t val;
};
int main(int argc, char** argv) {
SmallInt s1, s2;
SmallInt s3 = s1 + s2; // 此处可以, 调用SmallInt的+
int a = s3 + 0; // 此处具有二义性, 既可以 把0 转成SmallInt, 也可以把S3 转成int
}
如果一个类既定义了转换目标是算术类型的类型转换, 也提供了重载的运算符, 则 将会遇到重载运算符与内置运算符的二义性问题