Overloaded Operator
人生得意需尽欢——在有电的地方赶紧把电子设备都充满。—— Weng Kai
重载的实现
C++ 实现了运算符重载,允许用户对自定义的类型使用和 C 一样的运算符。
经典重载: std::cin >> x
重载了用于位移的运算符。
可以重载的运算符
+ - * / % & | ^ ~ << >>
= += -= *= /= %= &= |= <<= >>=
< > == != <= >=
! && || ++ --
, ->* ->() []
new delete new[] delete[]
不能被重载的运算符
. .* :: ?:
sizeof typeid
static_cast dynamic_cast const_cast
reinterpret_cast
重载条件
-
不在 C 语言的运算符之内的符号不能用于重载,例如
**
。 -
必须在类或枚举类(enumeration)上定义
-
算子(operand)的数量保持一致,例如
a / b
必须两个参数。- 在 IntegerI 领域中,算子指在矩阵中的一个操作(operator),需要区分开来。
-
重载后运算符的优先级不变。
语法
用关键字 operator
来声明运算符重载。既可以是类的成员函数,也可以是自由函数。
类内重载:
// inside class
Integer operator+(const Integer& b){
return Integer(this->val + b.val);
}
自由函数:
// globally
Integer operator+(const Integer& a, const Integer& b){
return Integer(a.val + b.val);
}
class Integer{
public:
Integer(int ii): i(ii){}
void print(){ std::cout << ii << std::endl;}
Integer operator+(const Integer& b){
std::cout << "operator + " << std::endl;
return Integer(this->val + b.val);
}
Integer operator-(){
return Integer(-i);
}
friend Integer operator*(const Integer& a, const B& b);
private:
int i;
};
Integer operator*(const Integer& a, const B& b){
return Integer(a.i + b.i);
}
signed main(){
Integer a1(1), a2(2);
Integer a3 = a1 + a2;// a1.operator+(a2);
Integer a4 = a1 + 3;
Integer a5 = -a1;// a5.operator=(a1.operator-());
}
注意事项:
- 在接收端不会进行类型转换
a1 + 3
将3
转换为Integer(3)
,再相加3 + a1
不能通过,因为a1
不能转换为int
。- 所以操作符左端的类型决定了使用的操作符的类型。
- 赋值运算符有默认函数
- 声明为自由函数时,操作数全部都要显式声明
- 在类中提供接口
.geti()
来使用值 - 或者声明为
friend
来使用 - 此时没有接收端,两侧都可能进行类型转换(
3 + a1
可以通过编译)
- 在类中提供接口
成员函数 VS 自由函数
- 单目运算符应该声明为成员
=, (), [], ->, ->*
必须是成员- 其他的赋值运算符
+= -= <<=
应该为成员 - 二目运算符建议为自由函数
参数和返回类型
参数类型:考虑有没有赋值,没有赋值的操作,参数 const _Tp&
,声明为 const
函数。
返回类型:考虑返回类型能不能作左值,能就返回一个可写的类型,否则返回不可写的类型。
const Integer operator+(const Integer& a, const Integer& b);
重载实现
函数原型
+ - * / % ^ & | ~
const _Tp operator X(const _Tp& l, const _Tp& r);
== != < > <= >=
bool operator X(const _Tp& l, const _Tp& r);
[]
_Tp& operator X(int index);
= += -= *= /= <<= >>=
_Tp& operator X(_Tp& l, const _Tp& r);
特殊问题:++
和 --
class Integer{
public:
const Integer& operator++();// prefix++
const Integer operator++(int);// postfix++
const Integer& operator--();// prefix--
const Integer operator--(int);// postfix--
...
private:
...
};
const Integer& Integer::operator++(){
*this += 1;
return *this;
}
const Integer Integer::operator++(int){// just leave the parameter unnamed
Integer old(*this);
++(*this);
return old;
}
重载比较运算符
bool Integer::operator==(const Integer& rhs)const{
return this->i == rhs.i;
}
bool Integer::operator!=(const Integer& rhs)const{
return !(*this == rhs);
}
bool Integer::operator<(const Integer& rhs)const{
return this->i < rhs.i;
}
bool Integer::operator>(const Integer& rhs)const{
return rhs < *this;
}
bool Integer::operator<=(const Integer& rhs)const{
return !(rhs < *this);
}
bool Integer::operator>=(const Integer& rhs)const{
return !(*this < rhs);
}
对于满足比较运算符性质的运算(对称性、自反性),可以只修改 ==
和 <
的内容,剩下的转化成 ==
、<
、!
的组合。
寻址运算符
// inside class Vector
// suppose int* buf, int size
int& operator[](int index){
return buf[index];
}
流输入输出
istream& operator>>(istream& is, _Tp& obj){
// read obj from is
return is;
}
ostream& operator<<(ostream& os, const _Tp& obj){
// output obj in os
return os;
}
ostream& tab(ostream& os){// manipulator
return os << '\t';
}
这样实现才能允许连续输入(is >> a >> b >> c
等同于 ((is >> a) >> b) >> c
)。
赋值运算符
默认的赋值运算符 =
和默认拷贝构造函数一样,member-wise。
需要自定义的情况也和拷贝构造函数一样,有非完全包含的资源等。
_Tp& _Tp::operator=(const _Tp& rhs){
if(this != &rhs){// explain[1]
size = rhs.size;
delete[] p;
p = new int[size];
for(int i = 0 ; i < size; ++i)
p[i] = rhs.p[i];
}
return *this;
}
[1]:如果出现了 a = a;
,在分配内存的时候很可能先释放了 a
的外部资源的内存(*p
),重新申请再构造,这个时候就会把有效数据释放。
和拷贝构造类似,如果你的类不需要赋值运算,可以将 =
声明成 private
,同时也不需要实现。这样在调用 operator=
的时候就会报错。
private:
_Tp& _Tp::operator=(const _Tp& rhs);
使用建议
谨慎使用重载运算符。仅在运算符能切合语义、简化代码和提升可读性的情况下使用。
不要重载 &&
、||
和 ,
。
类型转换
隐式类型转换
编译器会发生隐式的类型转换,例如前文:
signed main(){
Integer a1(1);
Integer a2 = a1 + 3;
}
隐式地将 3
转换为 Integer(3)
,再相加。
这种转换,在自定义类有对应的单参数构造函数时就可以触发。
class PathName{
public:
PathName(const string& pn): pth(pn){}
~PathName(){}
private:
string pth;
}
int main(){
string str("xyz");
PathName path("abc");
path = str;// Ok, path = PathName(str);
}
在构造函数前,可以使用 explicit
关键字:
explicit PathName(const string& pn): pth(pn){}
这样 PathName(const string&)
就不能用于隐式地转换了。
自定义类型转换
在类中,类型转换是特殊的成员函数,声明方式如下:
operator _Tp() const;
_Tp
可以是基本类型,或者是另一个类。
class Rational{
public:
Rational(long long _p, long long _q): numerator(_p), denominator(_q){}
~Rational(){}
operator double()const;
private:
long long numerator, denominator;
}
Rational::operator double()const{
return numerator / double(denominator);
}
int main(){
Rational r(1, 3);
double c = 3.14 * r; // Ok, 3.14 * 0.333
}
内置类型转换
对于基本类型的转换:
char => short => int => float => double
char => short => int => long
对于自定义类型的转换:
T => T& T& => T T => (const T)
T[] => T* T* => T[] T* => void*
使用建议
尽量不要使用类型转换,因为在意想不到的地方可能会被调用。
如果确实需要这样的转换,可以用单独的 toTp()
函数,而不是自定义类型转换。
double Rational::toDouble()const{
return numerator / double(denominator);
}
转换的匹配
如果 A
的对象需要被当作 B
的对象看待(例如传参、运算)时,C++ 会去寻找最优匹配来构造参数。
- 精确的类型匹配(
A
就是B
),这是最简单、最好的 - 内置类型转换
- 自定义的类型转换
- 如果有
B(A)
的构造函数,那么优先使用它 - 如果没有
B(A)
的构造函数,或者它被声明为explicit
,同时有A
到B
的自定义类型转换,那么优先使用 - C++11 以后的关键字
static_cast
:如果使用static_cast<B>
,那么明确使用构造函数。
- 如果有
- 对于模板函数的参数,如果上述条件均不满足,则编译器会考虑使用其他版本的函数。
#include <algorithm>
#include <iostream>
using namespace std;
class A;
class B;
class A{
public:
A();
operator B()const;
};
class B{
public:
B();
explicit B(A);
};
A::A(){cout << "constructor of A\n";}
A::operator B()const{
cout << "transformer from A to B\n";
return B();
}
B::B(){cout << "constructor of B\n";}
B::B(A v){cout << "constructor of B by A\n";}
void functionTakingB(B thing){
cout << "OK" << std::endl;
}
signed main(int argc, char **argv){
A a;
B b;
cout << "Round 1" << std::endl;
functionTakingB(b);
cout << "Round 2" << std::endl;
functionTakingB(a);
cout << "Round 3" << std::endl;
functionTakingB(static_cast<B>(a));
return 0;
}
可以本地编译一下看看输出,顺便观察编译器有没有优化拷贝构造。
如果不声明 explicit
,则编译器会警告。
转换运算符
C++ 中有四个转换运算符 Cast Operator:
static_cast
:基本类型dynamic_cast
:down-cast,安全const_cast
:修改const
属性reinterpret_cast
:低级别
static_cast
用于在相关类型之间转换。在编译时刻完成。
- 基本类型的转换
- 子类指针/引用向父类指针/引用的转换
void
指针和其他类型指针的转换
dynamic_cast
用于多态类型的转换,在运行时刻检查类型安全。(down-cast)
- 父类指针/引用向子类指针/引用的转换:如果这个指针实际上不是子类及其派生类的指针,会在转换时抛出异常
const_cast
用于修改类型的 const
或 volatile
属性。
- 去除
const
属性,使变量可以修改(传给另一个指针来修改) volatile
属性指的是变量不能被优化在寄存器中,每次修改必须访问内存
reinterpret_cast
低级别的、无类型检查的转换。可以在几乎任何类型间转换,但是非常危险。