多态
“多态”(Polymorphism
)是”poly
“ + “morphs
“的组合,其意味着多种形式。
C++中有两种类型的多态:
- 编译时多态性:通过函数重载和操作符重载来实现,这也称为静态绑定或早期绑定。
- 运行时多态性:它通过方法覆盖来实现,也称为动态绑定或后期绑定。
1. 编译期多态(静态多态)
1 // 例1:函数模板体现出编译期多态
2 #include <iostream>
3
4 template <typename T>
5 T add(T a, T b)
6 {
7 T c = a + b;
8 return c;
9 }
10
11 int main()
12 {
13 int i1 = 1;
14 int i2 = 2;
15 int iResult = 0;
16
17 iResult = add(i1, i2);
18 std::cout << "The result of integer is " << iResult << std::endl;
19
20 double d1 = 1.1;
21 double d2 = 2.2;
22 double dResult = 0;
23
24 dResult = add(d1, d2);
25 std::cout << "The result of double is " << dResult << std::endl;
26
27 return 0;
28 }
可以看到,我们定义了一个函数模板add,用来求两个数的和,这两个数的数据类型在使用时才知道。main函数中使用了两个int值的求和以及两个double值的求和,这里就体现了多态性,即在编译期,编译器根据一定的最佳匹配算法确定函数模板的参数类型到底是什么,这就体现了编译期的多种状态。
2. 运行期多态(动态多态)
在C++中,运行期多态主要通过虚函数来实现,并且一定要有继承关系,
1 // 例2:虚函数和继承关系体现运行期多态
2 #include <iostream>
3
4 class parent
5 {
6 public:
7 parent() {}
8
9 // 父类的虚函数
10 virtual void eat()
11 {
12 std::cout << "Parent eat." << std::endl;
13 }
14
15 // 注意这个并不是虚函数!!!
16 void drink()
17 {
18 std::cout << "Parent drink." << std::endl;
19 }
20 };
21
22 class child : public parent
23 {
24 public:
25 child () {}
26
27 // 子类重写了父类的虚函数
28 void eat()
29 {
30 std::cout << "Child eat." << std::endl;
31 }
32
33 // 子类覆盖了父类的函数,注意由于父类的这个函数
34 // 并不是虚函数,所以不存在继承后重写的说法
35 void drink()
36 {
37 std::cout << "Parent drink." << std::endl;
38 }
39
40 // 子类特有的函数
41 void childLove()
42 {
43 std::cout << "Child love playing." << std::endl;
44 }
45 };
46
47 int main()
48 {
49 parent* pa = new child();
50 pa->eat(); // 运行期多态的体现!!!
51 pa->drink(); // 这里调用的还是父类的drink,所以并不是多态!!!
52 // pa->childLove(); // 编译出错,父类的指针不能调用父类没有的函数
53
54 return 0;
55 }
56
57
58 运行结果:
59
60 Child eat.
61 Parent drink.
注意,在C++中只能用指针或引用来实现多态,不能通过普通的对象来实现多态。
第一,在我们的父类即parent中定义了一个虚函数(virtual关键字修饰的函数)---eat(),既然是虚函数,那么我们的子类就可以重写这个函数(注意这里强调是重写,而不是重载,也不是覆盖,重要的事情说三遍,是“重写!重写!重写!”)。我们的子类child中重写了eat()函数,至于父类和子类中的其他函数暂且先忽略,后面再讲解,这里只关注多态性相关的点。然后,在main函数中,我们定义了一个父类的指针parent,但是注意,我们虽然定义的是父类的指针,但是我们指向的是子类对象,即new的是child对象,这里涉及到向上转型,即将子类对象向上转型到了父类的指针所指。总之,一句话来说就是定义一个父类指针,指向子类对象,然后我们用父类的这个指针去调用eat()函数,这里就是多态发生的地方。从运行结果可以看到,实际上调用的是子类的eat()函数,并不是父类的eat()函数,这是因为虚函数,父类定义的是虚函数,而子类重写了这个函数,虽然我们定义的是父类指针,但是实际上指向的是子类对象,那么在运行期间,就会找到动态绑定到父类指针上的对象就是子类对象,然后实际上运行期间就是找到了子类对象中eat()函数的入口地址,然后调用了子类的eat()函数,这就是运行期多态。总结成一句话就是:“定义父类指针并指向子类对象,此时用父类指针去调用一个特殊的函数,即父类中该函数是虚函数,而子类重写了这个虚函数,此时调用的这个函数就在运行期间动态地绑定到了指针实际所指的对象,即子类对象,从而去调用子类中的这个函数”。
第二,说完了多态,我们来看看例2中其他需要注意的地方。我们发现在main函数中pa指针还调用了drink()函数,最终的运行结果显然调用的是父类的drink()函数,那么这里为什么没有多态呢?原因很简单,因为drink()函数不是虚函数,所以根本不存在多态这一特性,虽然父类和子类中都有drink()这个函数,但是子类仅仅是覆盖或者说隐藏了父类的drink()函数,并不是重写。而我们的指针是父类指针,所以必定要去调用父类的drink()函数。
第三,在子类child中,有一个只有它有而父类没有的函数,即childLove()函数,该函数是子类特有的,和父类没有任何关系,所以在main函数中用pa指针去调用这个函数会出错,因为父类指针根本访问不到子类的这个函数。这也是多态性的一个缺陷,即父类的指针只能访问子类中重写了父类中的那些虚函数,而不能访问子类新增的特有的函数。
纯虚函数实现多态性的例子。
// 例3:纯虚函数和继承关系体现运行期多态
#include <iostream>
// 父类因为包含纯虚函数,所以该类是抽象类,即不能定义对象
class parent
{
public:
parent() {}
// 父类的纯虚函数
virtual void eat() = 0;
};
class child : public parent
{
public:
child () {}
// 子类重写了父类的纯虚函数
void eat()
{
std::cout << "Child eat." << std::endl;
}
};
int main()
{
// parent pa0; // 编译期会出错,因为抽象类不能定义对象
// 注意抽象类虽然不能定义对象,但是可以定义指针用来指向具体类
parent* pa = new child();
pa->eat(); // 运行期多态的体现!!!
return 0;
}
运行结果:
Child eat.
从例3中就可以看到,纯虚函数就是一个函数后面加上"=0",而没有任何实现,那么包含纯虚函数的类就自然成为了抽象类,而抽象类是不能定义对象的,这也就是为什么main函数中的第一行会出错。注意,继承抽象类的具体类必须实现抽象类中的纯虚函数,否则也会出错。这里想强调一个误区,很多人觉得既然抽象类不能定义对象,那么main函数中的parent *pa为什么没有出错,这是因为抽象类虽然不能定义对象,但是可以定义指针,用来指向具体类,从而实现多态,一定要分清楚C++中的指针、引用、对象三者之间的关系,不要一概而论。
C++重载
如果创建两个或多个成员(函数)具有相同的名称,但参数的数量或类型不同,则称为C++重载。 在C++中,我们可以重载:
- 方法
- 构造函数
- 索引属性
这是因为这些成员只有参数。
C++中的重载类型有:
- 函数重载
- 运算符重载
C++函数重载
在C++中,具有两个或更多个具有相同名称但参数不同的函数称为函数重载。
函数重载的优点是它增加了程序的可读性,不需要为同一个函数操作功能使用不同的名称。
C++操作符重载
操作符重载用于重载或重新定义C++中可用的大多数操作符。 它用于对用户定义数据类型执行操作。
运算符重载的优点是对同一操作数执行不同的操作。
重写
C++虚函数
C++虚函数是基类中的一个成员函数,您可以在派生类中重新定义它。 它声明使用virtual
关键字。
它用于告诉编译器对函数执行动态链接或后期绑定。
后期绑定或动态链接
在后期绑定函数调用在运行时被解决。 因此,编译器在运行时确定对象的类型,然后绑定函数调用。
C++接口
抽象类是在C++中实现抽象的方式。 C++中的抽象是隐藏内部细节和仅显示功能的过程。 抽象可以通过两种方式实现:
- 抽象类
- 接口
抽象类和接口都可以有抽象所需的抽象方法。
C++抽象类
在C++类中,通过将其函数中的至少一个声明为纯虚函数,使其变得抽象。 通过在其声明中放置“= 0
”来指定纯虚函数。 它的实现必须由派生类提供。