本文章是作者根据史蒂芬·普拉达所著的《C++ Primer Plus》而整理出的读书笔记,如果您在浏览过程中发现了什么错误,烦请告知。另外,此书由浅入深,非常适合有C语言基础的人学习,感兴趣的朋友可以自行阅读此书籍。
对象和类
面向对象编程(OOP)是一种特殊的、设计程序的概念性方法。下面是最重要的OOP特性:- 抽象
- 封装和数据隐藏
- 多态
- 继承
本文先了解面向对象编程(OOP)、抽象、封装、类设计和使用。
过程性编程和面向对象编程的区别
一般来说,计算机语言要处理两个概念——数据和算法。数据是程序使用和处理的信息,而算法是程序使用的方法。过程性编程强调的是编程的算法方面,从概念上说,过程化编程首先要确定计算机应采取的操作,然后使用编程语言来实现这些操作。程序命令计算机按一系列流程生成特定的结果,就像菜谱指定了厨师做蛋糕时应遵循的一系列步骤一样。
过程性编程的设计思路是自顶向下。在C语言中,其理念是将大型程序分解成小型、便于管理的任务。如果其中一项任务仍然过大,则将它分解为更小的任务。这一过程将一直持续下去,直到将程序划分为小型的、易于编写的模块。
假设我们要实现一个小程序,这个程序从终端获取学生的基本信息,如学号、姓名、语文分数、数学分数、英语分数,然后计算学生的总分和平均分,并在终端打印出来。
#include <iostream>
using namespace std;
int main()
{
char id[20] = {0};
char name[20] = {0};
int yuwen = 0;
int shuxue = 0;
int yingyu = 0;
cout << "input id: ";
cin >> id;
cout << "input name: ";
cin >> name;
cout << "input yuwen: ";
cin >> yuwen;
cout << "input shuxue: ";
cin >> shuxue;
cout << "input yingyu: ";
cin >> yingyu;
cout << endl;
cout << "show info: " << endl;
int total = yuwen + shuxue + yingyu;
int avr = total / 3;
cout << id << " " << name << endl
<< "yuwen: " << yuwen << endl
<< "shuxue: " << shuxue << endl
<< "yingyu: " << yingyu<< endl
<< "total : " << total << endl
<< "avr : " << avr << endl;
return 0;
}
input id: > 0001
input name: > wangming
input yuwen: > 89
input shuxue: > 90
input yingyu: > 99show info:
0001 wangming
yuwen: 89
shuxue: 90
yingyu: 99
total : 278
avr : 92
采用过程性编程方法时,首先考虑要遵循的步骤,然后考虑如何表示这些数据。
与强调算法的过程性编程不同的是,OOP强调的是数据。OOP不像过程性编程那样,试图使问题满足语言的过程性方法,而是试图让语言来满足问题的要求。其理念是设计与问题的本质特性相对应的数据格式。在C++中,类是一种规范,它描述了这种新型的数据格式,对象是根据这种规范构造的特定数据结构。通常,类规定了可使用哪些数据来表示对象以及可以对这些数据执行哪些操作。
OOP程序设计方法首先设计类,它们准确地表示了程序要处理的东西。类定义描述了对每个类可执行的操作。然后就可以设计一个使用这些类的对象的程序。从低级组织(如类)到高级组织(如程序)的处理过程叫做自下向上的编程。
还是上面的程序,从OOP的角度来考虑,我们需要有这么一个类型,这个类型可以存储学生的序号、姓名、各科成绩,也可以通过各科成绩来计算总分和平均分,并支持打印这些信息。
#include <iostream>
using namespace std;
class Student{
public:
char id[20];
char name[20];
int yuwen;
int shuxue;
int yingyu;
int total;
int avr;
void set_total() { total = yuwen + shuxue + yingyu;}
void set_avr() { avr = total/3;}
void display() {
cout << endl;
cout << "show info: " << endl;
cout << id << " " << name << endl
<< "yuwen: " << yuwen << endl
<< "shuxue: " << shuxue << endl
<< "yingyu: " << yingyu<< endl
<< "total : " << total << endl
<< "avr : " << avr << endl; }
};
int main()
{
Student st;
cout << "input id: ";
cin >> st.id;
cout << "input name: ";
cin >> st.name;
cout << "input yuwen: ";
cin >> st.yuwen;
cout << "input shuxue: ";
cin >> st.shuxue;
cout << "input yingyu: ";
cin >> st.yingyu;
st.set_total();
st.set_avr();
st.display();
return 0;
}
input id: > 0001
input name: > wangdamao
input yuwen: > 89
input shuxue: > 90
input yingyu: > 99show info:
0001 wangdamao
yuwen: 89
shuxue: 90
yingyu: 99
total : 278
avr : 92
采用OOP方法时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。
抽象和类
抽象
生活中充满复杂性,处理复杂性的方法之一就是简化和抽象。为了解决某个问题,我们需要在这个问题所需的层次上进行抽象。也就是说,将问题的本质特征抽象出来,并根据特征来描述解决方案。
抽象是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽象接口的类设计。
类型
对于内置类型来说,有关操作的信息被内置到编译器中。但在C++中定义用户自定义的类型时,必须自己提供这些信息。付出的这些劳动换来了根据实际需要定制新数据类型的强大功能和灵活性。‘指定基本类型,完成了三项工作:
- 决定数据对象所需的内存数量;
- 决定如何解释内存中的位(如long和float在内存中占用的位数相同,但将它们转换为数值的方法不同);
- 决定可使用数据对象执行的操作或方法。
C++中的类
类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操作数据的方法组合成一个整洁的包。一般来说,类规范由两个部分组成:
- 类声明:以数据成员的方式描述数据部分,以成员函数的方式描述公有接口
- 类方法定义:描述如何实现类成员函数
什么是接口?
接口是一个共享框架,供两个系统交互时使用。对于类的公共接口来说,用户是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。
访问控制
上面的学生成绩显示程序,有一个隐藏问题,那就是允许在main函数中,修改学生的基本信息。一般来说,我们录入学生的基本信息后,就不应该允许别人随意修改,这里的别人,指的就是main函数或者其他模块。C++支持访问控制,需要使用关键字public、private、protect(protect在第13章类继承时再讨论)。将上面的Student类重新修改一下:
// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
class Student{
private:
char id[20];
char name[20];
int yuwen;
int shuxue;
int yingyu;
int total;
int avr;
void set_total() { total = yuwen + shuxue + yingyu;}
void set_avr() { avr = total/3;}
public:
void set_info();
void display();
};
#endif
这样一来,我们就不能在其它模块下,如main函数中,访问数据成员了。那是因为,我们使用了private关键字将数据成员和两个方法封装了起来。但是这种情况的话,就没法设置这些数据成员了,于是临时增加了个成员函数set_info()。
使用类对象程序都可以直接访问公有部分,但只能通过公有函数(或友元函数,第11章介绍)来访问对象的私有成员。例如,要修改Student的name,就需要使用Student的成员函数。
类设计尽可能将公有接口与实现细节分开。公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。
关于封装有三种形式:
- 数据隐藏,也就是将数据放在类的私有部分。
- 将实现的细节放在私有部分,比如set_total()或者set_avr()。
- 将类函数定义和类声明放在不同的文件中。**
数据隐藏不仅可以防止直接访问数据,还让开发者无需了解数据是如何被表示的。例如,display()成员可以显示总成绩,这个值可以存储在对象中(比如set_total),但也可以在需要时通过计算得到。从使用类的角度看,使用哪种方法没有什么区别。所需要知道的只是各种成员函数的功能。原则是将实现细节从接口设计中分离出来。如果以后知道了更好的、实现数据表示或成员函数细节的方法,可以对这些细节进行修改,而无需修改程序接口,这使程序维护起来更容易。
实现类的成员函数
上面的程序我们已经有了类声明,接下来,我们需要创建类描述的第二部分:为那些由类声明中的原型表示的成员函数提供代码。成员函数与常规函数的定义相似,包括函数头、函数体、返回类型和参数。但它还有两个特殊的特征:
- 定义成员函数时,使用作用域解析运算符(::)来表示函数所属的类;
- 类方法可以访问类的private组件。
第一个特征,也就是说,我们要实现 set_info()时,函数头应该写成这样:
void Student::set_info()
这种表示法意味着我们定义的set_info()函数时Student类的成员。这不仅将set_info()标识为为成员函数,还意味着我们可以将另一个类的成员函数也命名为set_info()。例如将Programmer类的set_info()函数头如下:
void Programmer::set_info()
作用域解析运算符确定了方法定义对应的类的身份。
同一个类中的方法可以不用加作用域解析运算符去使用其它的成员方法,比如set_info()中可以使用set_total()。因为在同一个类中,类作用域是相同的,因此成员函数之间彼此都是可见的。
第二个特征,方法可以访问类的私有成员。例如display()中可以使用这样的代码:
cout << "show info: " << endl;
cout << id << " " << name << endl
<< "yuwen: " << yuwen << endl
<< "shuxue: " << shuxue << endl
<< "yingyu: " << yingyu<< endl
<< "total : " << total << endl
<< "avr : " << avr << endl;
这些都是类的私有数据成员,如果试图使用非成员函数访问这些数据成员,编译器将会报错。(除了友元函数)
基于以上两个特征,我们就可以实现类方法了,如下所示:
//student.cpp
#include <iostream>
#include "student.hpp"
using namespace std;
void Student::set_info()
{
cout << "input id: ";
cin >> id;
cout << "input name: ";
cin >> name;
cout << "input yuwen: ";
cin >> yuwen;
cout << "input shuxue: ";
cin >> shuxue;
cout << "input yingyu: ";
cin >> yingyu;
set_total();
set_avr();
}
void Student::display()
{
cout << "show info: " << endl;
cout << id << " " << name << endl
<< "yuwen: " << yuwen << endl
<< "shuxue: " << shuxue << endl
<< "yingyu: " << yingyu<< endl
<< "total : " << total << endl
<< "avr : " << avr << endl;
}
有一个问题是,为什么我们不能像set_total()和set_avr()一样,直接把函数实现写到类声明中呢?
这不符合封装的特性,封装的第三种形式是“将类函数定义和类声明放在不同的文件中。”
但是为什么set_total()和set_avr()可以写到类声明中呢?
首先它们放在了private域中,这表明了只有类的成员函数才可以访问它;其次,在类中直接实现的函数,被称为内联函数。
C++引入内联函数的目的是为了替代C语言中的宏定义。简单理解,编译器在编译时知道这个函数是内联函数时,就会把使用了函数的地方,都替换成函数体。
比如set_info()中使用了set_total(),实际上编译器会改为:
void Student::set_info()
{
cout << "input id: ";
cin >> id;
cout << "input name: ";
cin >> name;
cout << "input yuwen: ";
cin >> yuwen;
cout << "input shuxue: ";
cin >> shuxue;
cout << "input yingyu: ";
cin >> yingyu;
total = yuwen + shuxue + yingyu; // 原为set_total();
avr = total/3; // 原为set_avr();
}
除了在类声明中直接实现,也可以在类声明之外定义成员函数时,使其称为内联函数,只需在类实现部分中定义函数时加上inline限定符。
比如类声明中,我们正常写:
// student.hpp
#ifndef _STUDENT_H_
#define _STUDENT_H_
class Student{
private:
...
void set_total();
...
};
#endif
在类实现部分,这么写:
//student.cpp
#include <iostream>'
#include "student.hpp"
using namespace std;
...
inline void Student::set_total()
{
total = yuwen + shuxue + yingyu;
}
...
这样set_total也就成了一个内联函数。使用内联函数的目的,是为了解决部分函数功能单一、代码较少(几行之内),但会被频繁调用而导致运行资源浪费。也就是说,当函数调用的成本大于函数执行的成本时,可以考虑使用内联函数。
使用类
我们已经创建了一个类的,现在我们来使用它。使用方式如下://student_main.cpp
#include <iostream>
#include "student.hpp"
using namespace std;
int main()
{
Student st1;
Student st2;
st1.set_info();
st1.display();
st2.set_info();
st2.display();
return 0;
}
input id: > 0001
input name: > xiaoming
input yuwen: > 78
input shuxue: > 87
input yingyu: > 98
show info:
0001 xiaoming
yuwen: 78
shuxue: 87
yingyu: 98
total : 263
avr : 87
input id: > 0002
input name: > xiaohong
input yuwen: > 89
input shuxue: > 98
input yingyu: > 86
show info:
0002 xiaohong
yuwen: 89
shuxue: 98
yingyu: 86
total : 273
avr : 91
我们创建了两个对象st1和st2,这两个对象都有自己的存储空间,用于存储其内部变量和成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。例如,st1.name和st2.name占用了不同的内存块,但是st1.set_info()和st2.set_info()执行的是同一个代码块,只是将这些代码用于不同的数据。
在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,但该方法被用于两个不同的对象。
小结
指定类设计的第一步是提供类声明。类声明类似结构声明,可以包括数据成员和函数成员。声明私有部分,在其中声明的成员只能通过成员函数进行访问;声明还具有公有部分,在其中声明的成员可被使用类对象的程序直接访问。通常,数据成员被放在私有部分中,成员函数被放在公有部分中,因此典型的类声明的格式如下:class className{
private:
data member declarations;
public:
member function prototyes;
};
公有部分的内容构成了设计的抽象部分——公有接口。将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。
指定类设计的第二步是实现类成员函数。可以在类声明中提供完整的函数定义,而不是函数原型,但是通常的做法是单独提供函数定义(除非函数很小)。在这种情况下,需要使用作用域解析运算符(::)来指出成员函数属于哪个类。