多态
C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数;
形成多态必须具备三个条件:
- 必须存在继承关系;
- 继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字 virtual 声明的函数,在派
生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数); - 存在基类类型的指针或者引用,通过该指针或引用调用虚函数。
这里我们还需要理解两个概念:
虚函数:
是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,
会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。虚函数声明如下:virtual
ReturnType FunctionName(Parameter) 虚函数必须实现,如果不实现,编译器将报错
纯虚函数:
若在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基
类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。纯虚函数声明如下:
virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。
包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
上面那些概念大家可以捋一捋,毕竟 C++概念还是挺多的。为什么说到多态要与虚函数和
纯虚函数扯上关系?光说概念没有实例确实难理解。下面我们还是以我们熟悉的狗类和动物类,另外加一个猫类进行多态的讲解。
新建一个目录 09_polymorphism_example,编辑一个 09_polymorphism_example.cpp 内容如下。(PS: polymorphism 翻译多态的意思,笔者就以这种方式命名例程了)。
1 #include <iostream>
2 #include <string>
3 using namespace std;
4
5 /* 定义一个动物类 */
6 class Animal
7 {
8 public:
9 virtual void run() {
10 cout<<"Animal 的 run()方法"<<endl;
11 }
12 };
13
14 /* 定义一个狗类,并继承动物类 */
15 class Dog : public Animal
16 {
17 public:
18 void run() {
19 cout<<"Dog 的 run()方法"<<endl;
20 }
21
22 };
23
24 /* 定义一个猫类,并继承动物类 */
25 class Cat : public Animal
26 {
27 public:
28 void run() {
29 cout<<"Cat 的 run()方法"<<endl;
30 }
31
32 };
33
34 int main()
35 {
36 /* 声明一个 Animal 的指针对象,注:并没有实例化 */
37 Animal *animal;
38 /* 实例化 dog 对象 */
39 Dog dog;
40 /* 实例化 cat 对象 */
41 Cat cat;
42
43 /* 存储 dog 对象的地址 */
44 animal = &dog;
45 /* 调用 run()方法 */
46 animal->run();
47
48 /* 存储 cat 对象的地址 */
49 animal = &cat;
50 /* 调用 run()方法 */
51 animal->run();
52 return 0;
53 }
第 9 行、第 18 行和第 28 行,都有一个 run()方法。其中我们可以看到基类 Animal 类的 run()方法前面加了关键字 virtual。这样让基类 Animal 类的 run()方法变成了虚函数。在这个例子里我们可以知道虚函数是 C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。简单的来说,上面的实例是基类 Animal 声明了一个指针 animal。
然后通过基类的指针来访问 Dog 类对象与 Cat 类的对象的 run()方法,前提是基类的 run()方法必须声明为虚函数,如果不声明为虚函数,基类的指针将访问到基类自己的 run()方法。我们可以尝试把 virtual 关键字去掉再重新编译测试,如果不加关键字 virtual 会是什么情况。
第 44 行和第 49 行,可以理解是 animal 指针实例化的过程。当基类的 run()方法定义成虚函
数,编译器不静态链接到该函数,它将链接到派生类的 run()方法,进行实例化。
执行下面的指令编译。
g++ 09_polymorphism_example.cpp -o 09_polymorphism_example
编译完成执行的结果如下。
数据封装
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受
到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴
露接口而把具体的实现细节隐藏起来的机制,C++ 通过创建类来支持封装和数据隐藏(public、protected、private)。
其实我们在第 2.2 小节开始就已经接触了数据封装。在 C++程序中,任何带有公有和私有
成员的类都可以作为数据封装和数据抽象的实例。通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。这通常应用于数据成员,但它同样适用于所有成员,包括虚函数。
下面我们还是以狗类为例,增加一个食物的方法 addFood(int number)。将获得食物的方法
设定在 public 下,这样 addFood(int number)方法就暴露出来了,也就是对外的接口。然后我们设置狗类的私有成员(private)食物的份数 total。我们在这个教程里第一次使用 private,在这章节里我们也可以学到什么时候该使用 private 什么时候使用 public。total 为获得的食物总数,然后我们还写一个公开的方法 getFood()在 public 下,通过 getFood()来打印出小狗总共获得了几份食物。
新建一个目录 10_encapsulation_example,编辑一个 10_encapsulation_example.cpp 内容如下。
(PS: encapsulation 翻译封装的意思,笔者就以这种方式命名例程了)。
1 #include <iostream>
2 #include <string>
3 using namespace std;
4
5 class Dog
6 {
7 public:
8 string name;
9
10 Dog(int i = 0)
11 {
12 total = i;
13 }
14
15 void addFood(int number) {
16 total = total + number;
17 }
18
19 int getFood() {
20 return total;
21 }
22 private:
23 int total;
24 };
25
26
27 int main()
28 {
29 Dog dog;
30
31 dog.name = "旺财";
32
33 dog.addFood(3);
34 dog.addFood(2);
35
36 cout<<dog.name<<"总共获得了"<<dog.getFood()<<"份食物"<<endl;
37
38 return 0;
39 }
第 10 至第 13 行,在构造函数里初始化 total 的数量,不初始化 total 的数量默认是随 int 类
型的数。所以我们需要在构造函数里初始化,也体现了构造函数的功能,一般是在构造函数里初始化。不要在类内直接赋值初始化,有可能有些编译器不支持。
第 15 至 17 行,addFood(int number),在这个方法里,将获得的食物份数赋值给 total。
第 19 至 21,getFood(),在这个方法里,将返回食物的总份数。通过调用这个方法,即可
访问私有成员的 total 总数。
第 33 和 34 行,添加食物的份数。
第 36 行,打印食物的总份数。
执行下面的指令编译。
g++ 10_encapsulation_example.cpp -o 10_encapsulation_example
编译完成执行的结果如下。
数据抽象
数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息
而不呈现细节。数据抽象是一种依赖于接口和实现分离的编程(设计)技术。
数据抽象的好处:
- 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
- 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要
求不改变用户级代码的错误报告。
举个简单的例子,比如我们生活中的手机。手机可以拍照、听音乐、收音等等。这些都是
手机上的功能,用户可以直接使用。但是拍照的功能是如何实现的,是怎么通过摄像头取像然后怎么在屏幕上显示的过程,作为用户是不需要知道的。也就是暴露的不用太彻底,用户也不必须知道这种功能是如何实现的,只需要知道如何拍照即可。
就 C++ 编程而言,C++ 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象
数据的公共方法,也就是说,外界实际上并不清楚类的内部实现。
其实像 cout 这个对象就是一个公共的接口,我们不必要知道 cout 是如何在屏幕上显示内容
的。cout 已经在底层实现好了。
在上一节我们已经学习过数据封装,数据封装是一种把数据和操作数据的函数捆绑在一起
的机制,而数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 程序中,任何带有公有和私有成员的类都可以作为数据抽象的实例。例子略,例子可
参考上 2.2.5 小节的例子。