C++ note (6)
Eiaton

运算符重载 Ⅰ

概念

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名参数列表

重载的运算符可以理解为带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。

opand1 op opand2 -> op(opand1, opand2)

A + B -> operator + (A, B) -> add(A, B)

方法

运算符重载的两种方法:

  1. 类成员函数运算符重载
    • return_type class_name::operator op(operand2) {}
    • 重载二元运算符时,成员运算符函数只需显式传递一个参数(即二元运算符的右操作数),而左操作数则是该类对象本身,通过 this 指针隐式传递。
    • 重载一元运算符时,成员运算符函数没有参数,操作数是该类对象本身,通过 this 指针隐式传递
  2. 友元函数运算符重载
    • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Integer {

private:
int x;

public:
Integer(int x = 0) : x(x) {}
Integer operator + (const Integer& Int) {
return Integer(x + Int.x);
}
Integer operator - (const Integer& Int) {
return Integer(x - Int.x);
}
Integer operator - () {
return Integer(-x);
}
void print() {
cout << x << endl;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
int main() {
Integer a = 3, b = 4;
a.print();
b.print();
Integer c = a + b;
Integer d = a - b;
Integer e = -a;
c.print();
d.print();
e.print();
return (0);
}
1
2
3
4
5
3
4
7
-1
-3

注意:

  1. 左操作数:必须是 *this
  2. 右操作数:类型任意,注意区分 Tconst T&T&T&&T*const T* 的差别
  3. 返回值:可以是任意类型,一般是左操作数类型的临时对象 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
      20
      class 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
      11
      int 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
      7
      3
      prefix is invoked
      4
      4
      suffix is invoked
      5
      4

重载赋值运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Integer {

private:
int x;

public:
Integer(int x = 0) : x(x) {}
Integer& operator = (const Integer& Int) {
x = Int.x;
cout << "function is invoked" << endl;
return *this;
}
void print() {
cout << x << endl;
}
};
1
2
3
4
5
6
7
int main() {
Integer a = 3, b;
a.print();
b = a;
b.print();
return (0);
}
1
2
3
3
function is invoked
3


而不重载赋值运算符时会有一个缺省赋值运算符,每个成员变量直接拷贝值

1
2
3
4
5
6
7
8
9
10
11
class Integer {

private:
int x;

public:
Integer(int x = 0) : x(x) {}
void print() {
cout << x << endl;
}
};
1
2
3
4
5
6
7
int main() {
Integer a = 3, b;
a.print();
b = a; // default operator '='
b.print();
return (0);
}
1
2
3
3

所以当成员变量包含指针类型的时候,要注意浅拷贝和深拷贝的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class IntArray {

private:
int *a, n;

public:
IntArray(int n = 1) : n(n) {
a = new int[n];
}
~IntArray() {
delete[] a;
}
int& operator [] (const int& i) {
assert(0 <= i && i < n);
return a[i];
}
void print() {
for (int i = 0; i < n; ++i) {
cout << a[i] << " ";
}
cout << endl;
}
};
// 未重载赋值运算符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
IntArray a(4), b;
for (int i = 0; i < 4; ++i) {
a[i] = i;
}
a.print();
b = a; // 默认赋值符 '=' 不是不行,但不推荐,有可能内存泄漏
b.print();
for (int i = 0; i < 4; ++i) {
a[i]=-i;
}
a.print();
b.print(); // 与a一致
a.~IntArray(); // 直接调用析构,导致多次析构
b.print(); // b,a先后析构,造成segmentation fault
return 0;
}
1
2
3
4
5
6
0 1 2 3 
0 1 2 3
0 -1 -2 -3
0 -1 -2 -3
40316064 0 17039696 0

一种重载赋值运算符的正确写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class IntArray {

private:
int *a, n;

public:
IntArray(int n = 1) : n(n) {
a = new int[n];
}
~IntArray() {
delete[] a;
}
int& operator [] (const int& i) {
assert(0 <= i && i < n);
return a[i];
}
IntArray& operator = (const IntArray& A) {
delete[] a;
n = A.n;
a = new int[n];
memcpy(a, A.a, sizeof(int)*n);
return *this;
}
void print() {
for (int i = 0; i < n; ++i) {
cout << a[i] << " ";
}
cout << endl;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
IntArray a(4), b;
for (int i = 0; i < 4; ++i) {
a[i] = i;
}
a.print();
b = a; // deep copy is good
b.print();
for (int i = 0; i < 4; ++i) {
a[i] = -i;
}
a.print();
b.print(); // would be different from a
//a.~IntArray();
b.print(); // nothing happend
return 0;
}
1
2
3
4
5
6
0 1 2 3 
0 1 2 3
0 -1 -2 -3
0 1 2 3
0 1 2 3

如果是 string 对象, new 数组会带来巨大对象构造成本。

例如,作业中我们开辟 10000 个 account 数组,不仅每个对象构造成本巨大,而却 Account 通常是没有默认无参构造函数,因为没有账号ID的 account 不合理。

重载移位运算符(类外定义)

cin >>cout << 的用法重载了 >><<

同样,可以自行对定义的类做重载

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Integer {
int x;
};

istream& operator >> (istream& istrm, Integer& Int) {
istrm >> Int.x;
return istrm;
}

ostream& operator << (ostream& ostrm, const Integer& Int) {
ostrm << Int.x;
return ostrm;
}
1
2
3
4
5
6
int main() {
Integer x;
cin >> x;
cout << x << endl;
return (0);
}

要点:

  1. 使用 struct 为了说明位移运算可以作为普通函数重载,而 = 运算只能作为成员函数重载
  2. 第一操作数不是 *this 的,只能在类外定义
  3. 要返回对象自身的引用

展开讲 2

在C++中,istreamostream 是标准库中的类,用于读取和写入数据流。这两个类都是抽象类,不能直接实例化,需要使用其派生类进行实例化,例如iostreamifstreamofstream等。

由于 istreamostream标准库的类,我们不能在这些类的内部添加自定义的函数。因此,我们只能将重载的输入输出运算符定义为类的友元函数类的非成员函数,并在类的外部进行定义。

而,如果我们将这些函数定义为类的成员函数,则必须将其定义在类的内部,而不能在类的外部定义。

当我们将重载的输入输出运算符定义为类的友元函数时,我们可以在类的内部声明这些函数,然后在类的外部进行定义。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Integer {

public:
friend istream& operator >> (istream& istrm, Integer& Int);
friend ostream& operator << (ostream& ostrm, const Integer& Int);

private:
int x;
};

istream& operator >> (istream& istrm, Integer& Int) {
istrm >> Int.x;
return istrm;
}

ostream& operator << (ostream& ostrm, const Integer& Int) {
ostrm << Int.x;
return ostrm;
}

当我们将重载的输入输出运算符定义为类的非成员函数时,我们不需要将它们声明为类的友元函数,只需在类的外部进行定义。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Integer {

public:
int x;
};

istream& operator >> (istream& istrm, Integer& Int) {
istrm >> Int.x;
return istrm;
}

ostream& operator << (ostream& ostrm, const Integer& Int) {
ostrm << Int.x;
return ostrm;
}

无论采用哪种方式,都可以实现对Integer类的输入输出运算符的重载,并且将其定义在类的外部。

展开讲 3

istream& operator >> (istream& istrm, Integer& Int)ostream& operator << (ostream& ostrm, const Integer& Int) 函数都返回它们的第一个参数,即输入流和输出流的自身引用。这是因为这些函数通过引用参数修改了流对象的状态,因此返回自身引用可以实现连续的输入/输出操作

对于输入运算符 >> 来说,函数需要将输入流对象的状态修改为已读取输入的值,而对于输出运算符 << 来说,函数需要将输出流对象的状态修改为已写入输出的值。因此,为了实现链式输入输出操作,这些重载函数通常返回它们的第一个参数,即输入输出流对象的引用,以便连续调用。

如果我们不返回自身引用,那么我们就无法通过链式调用连续地对输入流进行操作,例如:

1
2
cin >> x; // 正常调用
cin >> x >> y; // 无法连续调用,因为第一个输入操作没有返回自身引用

因此,返回对象的自身引用可以实现链式的输入输出操作,使代码更加简洁、易读。