运算符重载 Ⅰ
概念
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名和参数列表。
重载的运算符可以理解为带有特殊名称的函数,函数名是由关键字 operator
和其后要重载的运算符符号构成的。
opand1 op opand2 -> op(opand1, opand2)
A + B -> operator + (A, B) -> add(A, B)
方法
运算符重载的两种方法:
- 类成员函数运算符重载
return_type class_name::operator op(operand2) {}
- 重载二元运算符时,成员运算符函数只需显式传递一个参数(即二元运算符的右操作数),而左操作数则是该类对象本身,通过 this 指针隐式传递。
- 重载一元运算符时,成员运算符函数没有参数,操作数是该类对象本身,通过 this 指针隐式传递
- 友元函数运算符重载
return_type operator op(operand1, operand2) {}
A + B -> A.operator + (B)
-A -> A.operator - ()
可重载运算符
运算符 | 操作符 | |
---|---|---|
双目 | 算术运算 关系运算符 逻辑运算 位运算 |
+ (add), - (minus), * (times), / (divide), % (mod)== , != , < (less), > (more), <= , >= || , && , ! | , & , ~ , ^ , << , >> |
单目运算 | + (pos), - (neg), * (ptr), & (addr) |
|
自增自减运算 | ++ , -- |
|
赋值运算 | = , += , -= , *= , /= , &= , |= , ^= , <<= , >>= |
|
其他运算 | () , -> , , , [] |
|
空间申请和释放 | new , delete , new[] , delete[] |
不可重载运算符
::
, .
, .*
(通过成员指针的成员访问), ?:
, sizeof
(alignof
, typeid
等), #
(预处理符号)
其他限制
- 不能创建新运算符,例如 **、<> 或 &|。
- 运算符
&&
与||
的重载失去短路求值 - 重载的运算符
->
必须要么返回裸指针,要么(按引用或值)返回同样重载了运算符->
的对象。 - 不可能更改运算符的优先级、结合方向或操作数的数量。
- 二元运算符中,左操作数为非对象的运算,必须用友元函数
- (5) 运算符重载不能改变该运算符用于内部类型对象的含义。它只能和用户自定义类型的对象一起使用,或者用于用户自定义类型的对象和内部类型的对象混合使用时
=
,()
,[]
,->
不能以友元方式重载,只能成员- 除了类属关系运算符
.
、成员指针运算符.*
、作用域运算符::
、sizeof
运算符和三目运算符?:
以外,C++中的所有运算符都可以重载
双目/单目运算符、成员函数重载例子
1 | class Integer { |
1 | int main() { |
1 | 3 |
注意:
- 左操作数:必须是 *this
- 右操作数:类型任意,注意区分
T
,const T&
,T&
,T&&
,T*
,const T*
的差别 - 返回值:可以是任意类型,一般是左操作数类型的临时对象
T
,或左操作数自身的引用T&
注意事项
重载自增运算符
重载自增运算符时需注意:
- 若为前缀自增运算符,直接重载(以
++
举例):return_type class_name::operator ++() {}
- 若为后缀自增运算符,该函数有一个
int
类型的虚拟形参,这个形参在函数的主体中是不会被使用的,这只是一个约定,它告诉编译器递增运算符正在后缀模式下被重载:return_type class_name::operator ++(int) {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Integer {
private:
int x;
public:
Integer(int x = 0) : x(x) {}
Integer& operator ++ () {
cerr << "prefix is invoked" << endl;
++x;
return *this;
}
Integer operator ++ (int) {
cerr << "suffix is invoked" << endl;
return Integer(x++);
}
void print() {
cout << x << endl;
}
};1
2
3
4
5
6
7
8
9
10
11int main() {
Integer a = 3;
a.print();
Integer b = ++a;
a.print();
b.print();
Integer c = a++;
a.print();
c.print();
return 0;
}1
2
3
4
5
6
73
prefix is invoked
4
4
suffix is invoked
5
4
重载赋值运算符
1 | class Integer { |
1 | int main() { |
1 | 3 |
而不重载赋值运算符时会有一个缺省赋值运算符,每个成员变量直接拷贝值
1 | class Integer { |
1 | int main() { |
1 | 3 |
所以当成员变量包含指针类型的时候,要注意浅拷贝和深拷贝的区别
1 | class IntArray { |
1 | int main() { |
1 | 0 1 2 3 |
一种重载赋值运算符的正确写法
1 | class IntArray { |
1 | int main() { |
1 | 0 1 2 3 |
如果是
string
对象,new
数组会带来巨大对象构造成本。例如,作业中我们开辟 10000 个 account 数组,不仅每个对象构造成本巨大,而却 Account 通常是没有默认无参构造函数,因为没有账号ID的 account 不合理。
重载移位运算符(类外定义)
cin >>
和 cout <<
的用法重载了 >>
和 <<
同样,可以自行对定义的类做重载
1 | struct Integer { |
1 | int main() { |
要点:
- 使用
struct
为了说明位移运算可以作为普通函数重载,而=
运算只能作为成员函数重载 - 第一操作数不是
*this
的,只能在类外定义 - 要返回对象自身的引用
展开讲 2
在C++中,istream
和 ostream
是标准库中的类,用于读取和写入数据流。这两个类都是抽象类,不能直接实例化,需要使用其派生类进行实例化,例如iostream
、ifstream
、ofstream
等。
由于 istream
和 ostream
是标准库的类,我们不能在这些类的内部添加自定义的函数。因此,我们只能将重载的输入输出运算符定义为类的友元函数或类的非成员函数,并在类的外部进行定义。
而,如果我们将这些函数定义为类的成员函数,则必须将其定义在类的内部,而不能在类的外部定义。
当我们将重载的输入输出运算符定义为类的友元函数时,我们可以在类的内部声明这些函数,然后在类的外部进行定义。例如:
1 | class Integer { |
当我们将重载的输入输出运算符定义为类的非成员函数时,我们不需要将它们声明为类的友元函数,只需在类的外部进行定义。例如:
1 | class Integer { |
无论采用哪种方式,都可以实现对Integer类的输入输出运算符的重载,并且将其定义在类的外部。
展开讲 3
istream& operator >> (istream& istrm, Integer& Int)
和 ostream& operator << (ostream& ostrm, const Integer& Int)
函数都返回它们的第一个参数,即输入流和输出流的自身引用。这是因为这些函数通过引用参数修改了流对象的状态,因此返回自身引用可以实现连续的输入/输出操作。
对于输入运算符 >>
来说,函数需要将输入流对象的状态修改为已读取输入的值,而对于输出运算符 <<
来说,函数需要将输出流对象的状态修改为已写入输出的值。因此,为了实现链式输入输出操作,这些重载函数通常返回它们的第一个参数,即输入输出流对象的引用,以便连续调用。
如果我们不返回自身引用,那么我们就无法通过链式调用连续地对输入流进行操作,例如:
1 | cin >> x; // 正常调用 |
因此,返回对象的自身引用可以实现链式的输入输出操作,使代码更加简洁、易读。