第十四章 重载运算与类型转换
基本概念
- 重载运算符是具有特殊名字的函数:由关键字
operator
和其后要定义的运算符号共同组成。 - 当一个重载的运算符是成员函数时,
this
绑定到左侧运算对象。动态运算符符函数的参数数量比运算对象的数量少一个。 - 只能重载大多数的运算符,而不能发明新的运算符号。
- 重载运算符的优先级和结合律跟对应的内置运算符保持一致。
- 调用方式:
data1 + data2;
operator+(data1, data2);
- 是否是成员函数:
- 赋值(
=
)、下标([]
)、调用(()
)和成员访问箭头(->
)运算符必须是成员。 - 复合赋值运算符一般来说是成员。
- 改变对象状态的运算符或者和给定类型密切相关的运算符通常是成员,如递增、解引用。
- 具有对称性的运算符如算术、相等性、关系和位运算符等,通常是非成员函数。
- 赋值(
运算符:
可以被重载 | 不可以被重载 |
---|---|
+ , - , * , / , % , ^ | :: , .* , . , ? : , |
& , | , ~ , ! , , , = | |
< , > , <= , >= , ++ , -- | |
<< , >> , == , != , && , || | |
+= , -= , /= , %= , ^= , &= | |
|=, *= , <<= , >>= , [] , () | |
-> , ->* , new , new[] , delete , delete[] |
输入和输出运算符
重载输出运算符«
- 第一个形参通常是一个非常量的
ostream
对象的引用。非常量是因为向流中写入会改变其状态;而引用是因为我们无法复制一个ostream
对象。 - 输入输出运算符必须是非成员函数。
1
2
3
4
ostream &operator<<(ostream &os,const User &user){
os << user.name << " -- " << user.age ;
return os;
}
重载输入运算符»
- 第一个形参通常是运算符将要读取的流的因不用,第二个形参是将要读取到的(非常量)对象的引用。
- 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
1
2
3
4
5
istream &operator>>(istream &is,User &user){
is >> user.name >> user.age ;
if (!is) // 判断 输入可能会发生错误 错误即返回空User
user = User();
}
算数和关系运算符(+、-、*、/)
- 如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算数运算符。
相等运算符==
- 如果定义了
operator==
,则这个类也应该定义operator!=
。 - 相等运算符和不等运算符的一个应该把工作委托给另一个。
- 相等运算符应该具有传递性。
- 如果某个类在逻辑上有相等性的含义,则该类应该定义
operator==
,这样做可以使用户更容易使用标准库算法来处理这个类。
关系运算符
- 如果存在唯一一种逻辑可靠的
<
定义,则应该考虑为这个类定义<
运算符。如果同时还包含==
,则当且晋档<
的定义和++
产生的结果一直时才定义<
运算符。
赋值运算符=
- 我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
- 赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这么做。这两类运算符都应该返回左侧运算对象的引用。
下标运算符[]
- 下标运算符必须是成员函数。
- 一般会定义两个版本:
- 1.返回普通引用。
- 2.类的常量成员,并返回常量引用。
递增和递减运算符(++、–)
- 定义递增和递减运算符的类应该同时定义前置版本和后置版本。
- 通常应该被定义成类的成员。
- 为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
- 同样为了和内置版本保持一致,后置运算符应该返回递增或递减前对象的值,而不是引用。
- 后置版本接受一个额外的,不被使用的
int
类型的形参。因为不会用到,所以无需命名。
1
2
3
4
5
6
7
8
9
10
// 后置
User &operator++(int){
++age;
cout << " ++ " << endl;
}
// 前置
User &operator++(){
++age;
cout << " ++ " << endl;
}
成员访问运算符(*、->)
- 箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
- 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
- 解引用和乘法的区别是一个是一元运算符,一个是二元运算符。
函数调用运算符
- 可以像使用函数一样,调用该类的对象。因为这样对待类同时也能存储状态,所以与普通函数相比更加灵活。
- 函数调用运算符必须是成员函数。
- 一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
- 如果累定义了调用运算符,则该类的对象称作函数对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User2{
public:
int age;
User2() {}
User2(int age) : age(age) {}
void operator()(int i){
cout << i << endl;
}
};
void test08(){
User2 u;
u(2);
}
一个很好的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PrintString{
public:
PrintString(ostream &os = cout, char sep = ' ') : os(os), sep(sep) {}
void operator()(const string& s) const{
os << s << sep;
}
private:
ostream &os;
char sep;
};
void test09(){
PrintString p;
p("hello jay");
PrintString p2(cerr,'\n');
p2("hey");
p2("hey");
}
lambda
是函数对象
lambda
捕获变量:lambda
产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。
标准库定义的函数对象
标准库函数对象:
算术 | 关系 | 逻辑 |
---|---|---|
plus<Type> | equal_to<Type> | logical_and<Type> |
minus<Type> | not_equal_to<Type> | logical_or<Type> |
multiplies<Type> | greater<Type> | logical_not<Type> |
divides<Type> | greater_equal<Type> | |
modulus<Type> | less<Type> | |
negate<Type> | less_equal<Type> |
- 可以在算法中使用标准库函数对象。
可调用对象与function
标准库function类型:
操作 | 解释 |
---|---|
function<T> f; | f 是一个用来存储可调用对象的空function ,这些可调用对象的调用形式应该与类型T 相同。 |
function<T> f(nullptr); | 显式地构造一个空function |
function<T> f(obj) | 在f 中存储可调用对象obj 的副本 |
f | 将f 作为条件:当f 含有一个可调用对象时为真;否则为假。 |
定义为function<T> 的成员的类型 | |
result_type | 该function 类型的可调用对象返回的类型 |
argument_type | 当T 有一个或两个实参时定义的类型。如果T 只有一个实参,则argument_type |
first_argument_type | 第一个实参的类型 |
second_argument_type | 第二个实参的类型 |
- 例如:声明一个
function
类型,它可以表示接受两个int
,返回一个int
的可调用对象。function<int(int, int)>
重载、类型转换、运算符
类型转换运算符
- 类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:
operator type() const;
一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是
const
。1 2 3 4 5 6 7 8 9 10
class Demo2{ public: string str; operator int() const{ int n = atoi(str.c_str()); return n; } };
- 避免过度使用类型转换函数。
C++11引入了显式的类型转换运算符。 ` int i = static_cast
(demo2);` - 向
bool
的类型转换通常用在条件部分,因此operator bool
一般定义成explicit
的。
避免有二义性的类型转换
- 通常,不要为类第几个亿相同的类型转换,也不要在类中定义两个及以上转换源或转换目标是算术类型的转换。
- 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A{
public:
A() {}
A(B &b){
}
};
class B{
public:
operator A() const{
}
};
// so
f(A a);
f(B) // ??? how to chose?
函数匹配与重载运算符
- 如果
a
是一种类型,则表达式a sym b
可能是:a.operatorsym(b);
operatorsym(a,b);
- 如果我们队同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。
歧义1,
1
2
3
4
5
6
7
8
9
10
11
class Demo3{
Demo3& operator+(Demo3 &demo3){
cout << "内置" << endl;
return *this;
}
};
Demo3& operator+(Demo3 &d1, Demo3 &d2){
cout << "外置" << endl;
return d1;
}
歧义2,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Demo4{
public:
int s ;
Demo4(int s) : s(s) {}
operator int() const{
int i = 3;
return i;
}
};
Demo4& operator+(Demo4 &d1,Demo4 &d2){
}
Demo4 d1,d2;
Demo4 d3 = d1 + d2; // 正确
int i = d2 + 0; // 错误 有歧义