多态性(C++) - Part 1

看完封装和继承了,该看多态了。

  • 运算符重载
  • 类型转换构造函数

运算符重载

类外定义的运算符重载函数

以如下代码为例

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
#include <bits/stdc++.h>
using namespace std;
class A {
public:
A() = default;
A(int x, int y) : a(x), b(y) {}
int a;
int b;
};

A operator+(A op1, A op2) {
A tmp;
tmp.a = op1.a + op2.a;
tmp.b = op1.b + op2.b;
return tmp;
}

int main() {
A a(1, 2);
A b(2, 3);
A c = a + b;
A d = operator+(a, b);
cout << c.a << " " << c.b << endl;
cout << d.a << " " << d.b << endl;
return 0;
}

a + boperator+(a, b) 是等价的

绝大多数的运算符都允许重载,只有以下几个不允许重载:

  1. .
  2. .*
  3. ::
  4. sizeof
  5. ?:

需要注意的是,重载运算符函数的参数至少有一个是类对象(或其引用),出于防止用户修改标准类型数据的运算符性质。

友元运算符重载函数

上文中在类外定义的运算符重载函数只能访问类中的公有成员。要使运算符重载函数能够访问私有成员,有两种方式:将其定义为成员函数(成员运算符重载函数),或定义为类的友元函数(友元运算符重载函数)。

在类的内部,定义友元运算符函数:

1
2
3
friend 函数类型 operator 运算符 (形参表){
...
}

也可以在类内声明友元函数原型,在外部定义

1
2
3
4
5
6
7
8
class A{
...
friend 函数类型 operator 运算符 (形参表);
...
};
函数类型 operator 运算符 (形参表){
...
}
双目运算符重载(二元运算符)

见前文

单目运算符重载

以下面的代码为例,单目运算符重载函数只有一个形参,也因此这个形参必须是类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <bits/stdc++.h>
using namespace std;
class A {
public:
A() = default;
A(int x) : a(x) {}
int a;
};

A operator+(A op1) { return A(op1.a); }

A operator-(A op1) { return A(-op1.a); }

int main() {
A a(1);
A b = -a; // 与 operator-(a) 等价
cout << a.a << endl;
cout << b.a << endl;
return 0;
}

成员运算符重载函数

在类的内部直接定义成员运算符重载函数

1
2
3
4
5
class A{
函数类型 operator 运算符 (形参表){
...
}
};

在类的内部声明成员运算符重载函数

1
2
3
4
5
6
7
8
class A{
...
函数类型 operator 运算符 (形参表);
...
};
函数类型 A::operator 运算符 (形参表){
...
}
双目运算符重载

对于双目运算符而言,成员运算符重载函数的形参表中仅有一个参数,因为另一个操作数(左操作数)是对象本身。

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
#include <bits/stdc++.h>
using namespace std;
class A {
public:
A() = default;
A(int x, int y) : a(x), b(y) {}
A operator+(A op2) {
A tmp;
tmp.a = a + op2.a;
tmp.b = b + op2.b;
return tmp;
}
int a;
int b;
};

int main() {
A a(1, 2);
A b(2, 3);
A c = a + b;
A d = a.operator+(b);
cout << c.a << " " << c.b << endl;
cout << d.a << " " << d.b << endl;
return 0;
}
单目运算符重载

对于单目运算符而言, 操作数就是对象本身,因此没有形参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <bits/stdc++.h>
using namespace std;
class A {
public:
A() = default;
A(int x) : a(x) {}
A operator-(){
return A(-a);
}
int a;
};

int main() {
A a(1);
A b = -a;
cout << a.a << endl;
cout << b.a << endl;
return 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
#include <bits/stdc++.h>
using namespace std;
class A {
public:
A() = default;
A(int x) : a(x) {}
A operator++(){
return A(a+1);
}
A operator++(int x){
cout << x << endl;
return A(a+2);
}
int a;
};

int main() {
A a(1);
A b = ++a;
A c = a++;
cout << b.a << endl;
cout << c.a << endl;
return 0;
}

执行这段代码有以下输出

1
2
3
0
2
3

其中,第一行的 0 ,是后缀调用的的传入参数,可以看出,后缀式,处理时是会隐式的在后面加上 0 的。

相应的,如果使用友元运算符重载函数,也是加入第二个 int 类型形参。

赋值运算符的重载

对于任何一个类,当没有显式的赋值运算符重载时,编译器会自动合成一个默认的赋值运算函数,这是一种浅拷贝,可能会造成问题,因此需要引入对赋值运算符的重载。

下面给出一个深拷贝的赋值的例子

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
#include <bits/stdc++.h>
using namespace std;
class A {
public:
A(string ss) { s = new string(ss); }
~A() { delete s; }
A &operator=(const A &);
void pt() { cout << s << " " << *s << endl; }

private:
string *s;
};
A &A::operator=(const A &op) {
if (this == &op)
return *this; // prevent A=A
delete s;
s = new string(*op.s);
return *this;
}
int main() {
A a("aaa");
A b("bbb");
a.pt();
b.pt();
a = b;
a.pt();
return 0;
}

输出类似下面的

1
2
3
0xb400007c58aae370 aaa
0xb400007c58aae010 bbb
0xb400007c58aae370 bbb

这里有些微妙的东西,就是 as 地址不变,代码中是有 delete 但是随后 newp 仍指向这个地址,因此看似没有修改。

需要注意的是,双目赋值只能是成员函数,否则左操作数可能不是类对象。

下标运算符[]的重载

对于 X[Y],可以认为这是一个双目表达式

下标运算符重载函数只能定义为成员函数

1
2
3
返回类型 类名::operator[] (形参){
...
}

调用方式既可以是X[Y]又可以是 X.operator[](Y)

类型转换

类类型与系统预定义类型的转换

  1. 通过转换构造函数进行类型转换
  2. 通过类型转换函数进行类型转换
转换构造函数
1
类名(系统预定义类型 形参名);

转换构造函数只有一个系统预定义类型的形参,用于将系统预定义类型转换为类类型

类型转换函数
1
2
3
operator 目标类型(){
...
}

也算一种运算符重载,毕竟 C++类型转换有 int(2.0) 这样的写法。

  1. 类型转换函数只能是成员函数而不是友元函数
  2. 类型转换函数既没有参数也不能指定类型
  3. 但类型函数中必须 return 出目标类型数据
  4. 一个类可以有多个类型转换函数