第12章 类、对象与面向对象程序设计
第 12 章 面向对象的事物分类及其编程——类、对象和面向对象的程序设计初步
第3章讲解的是自动控制煮饭的例子,用的电炊具是电饭煲,电饭煲可以用于煮饭,那可不可以用于蒸、炖食物,煲汤和烧开水呢?一样是可以的,这些都是电饭煲本身所具有的功能。但如果蒸食物、炖食物、煲汤和烧开水等各编写一个程序,是比较麻烦费时的,这种方法就是面向处理事情的过程来编写程序。那么能否针对电饭煲编写一个程序,把电饭煲和它的现有功能都编写在一个程序中,到时要用什么功能就调该功能的相应部分来用呢?答案是肯定的,这就是面向对象(物体,此例是电饭煲)编写程序,这种设计程序的方法就是面向对象的程序设计方法,它会将数据和函数形成一个整体,可以保护数据(体现了封装性),使编写程序更省时、方便,编写出来的程序具有思路清晰(体现了抽象性)、可读性好、可维护性好、可移植性好等优点。
12.1 打印一个右上三角形图案的例题详解
【例 12-1】题目的功能同例 5-6,但此题要求用面向对象的方法来编程实现。
1. 用心分析人想通,机随人行写提纲
题目与第5章的例5-6相同,区别只是此题用面向对象的方法编程,而例5-6是用面向过程的方法编程。
将例 5-6 的饭菜分家变为饭菜打包就可完成此题的编程了,记住:饭还是原来的饭(数据),菜还是原来的菜(函数),只是将饭菜打包在一起而已。所以人的做法同图 5-4。
现在的问题是这个打包饭菜的盒子是怎样获得的。参考 12.3 节的知识,先声明一个类(某个盒子),写法与声明结构的方法相同(注意,类也是一种数据类型),即先写声明类的关键字 class(相当于写 struct),说明这是类(盒子),再起个类名 Fig1(盒子名)。接下来的“{}”内是盒子里的内容(饭和菜),饭就是数据成员(相当于面向过程的变量和常量),将这些饭(数据成员)归放到要保护的区域;菜也是原来的菜(函数操作部分),把它们放在此类合适的区域(公有成员)中,同以前的声明定义方式一样,这里采用声明和定义在一起的方式。
有了打包好的饭菜,就可以享用了。谁来享用,总要有个对象吧,假设这个对象是张三(此处是 dr),那么张三享用什么饭菜呢?享用盒子名为 Fig1 的饭菜。于是就定义对象张三(dr)。张三拿到饭盒后就会吃盒中的饭菜(调用类中的 dr2()函数成员),吃完饭菜(运行完 dr2()函数)后,返回主函数,程序就结束了。
2. 对号入座写文章,头脑运行改文章
程序代码如卜:
//ex12-1.cpp
#include<iostream.h>
class Fig1//声明类 Fig1
{
protected://保护成员
int i,j,k;//声明保护数据成员
public: //公有成员
void dr2()//定义公有函数成员
{
char c='';//声明局部变量 c 为一个空格,
//c 不是类的成员,只是 dr2 的局部变量
for(i=1;i<=5;i++)//画图案
{
for(j=1;j<=30-2*i;j++)
cout<<c;
for(k=1;k<=i;k++)
cout<<" #";
cout<<endl;
}
};
void main()
{
Fig1 dr;//定义类 Fig1 的对象 dr
dr.dr2();//通过对象 dr 调用类 Fig1 的函数成员 dr2
}
- C++帮助且裁判,人定胜机凯歌扬
用面向对象方法编写的程序运行的结果:
##
###
###
###
###
###
用面向过程方法编写的程序运行的结果:
#
##
##
###
###
###
###
结果一样,都正确。
12.2 勤奋实践,总结评分
12.2.1 习题
将第3章的习题第1~10题用面向对象的方法编写程序。
12.2.2 思考题
1.定义一个时间类,编写程序实现时间的增加、减少和输出时间值。
- 可以用类将整型数转换为字符串吗?
12.2.3 总结评分,继续提高
浏览、体验了例题、习题和思考题之后,说说自己的体会和收获,并认真总结,按表 12-1 给自己评判一下可以得多少分。
| 成绩 | A | B | C | D | 不足的原因 | 改进的方法 | 备注 |
| C++知识 | 完全理解和会用 | 基本理解和会用 | 肤浅理解和会用 | 基本不理解和不会用 | |||
| 程序原创(独立编程)的比例(%) | ≥90 | 80~89 | 60~79 | <60 |
不论得分多少,大家都要总结经验,扬长避短,勤奋努力,不断提高。
最后浏览一下本章的第4节,可以先考考自己,然后再浏览,以便使重点知识掌握得更牢固。
12.3 类、对象和面向对象的程序设计初步知识
是否采用面向对象的程序设计方法之后,以前学的面向过程的程序设计方法就没有用了呢?不是的,因为以前学过的 C++ 知识、编程思路和设计方法等均为使用面向对象的程序设计方法打下了牢固的基础,而且可以采用类似于从单文件结构转换到多文件结构的方法,把面向过程的程序设计方法转换成面向对象的程序设计方法。
12.3.1 在什么情况下用类、对象和面向对象的程序设计
在什么情况下都可以用面向对象的程序设计方法来设计编写程序。
即使是再小、再简单的程序也最好用面向对象的程序设计方法来设计编写。因为即使编写的程序今天用不上,或者当前不具备通用性,但可能日后编写程序时就需要移植它了,这就像家里的东西,机械生产车间的螺丝或螺母一样,即使今天用不上,日后也会用得上。
12.3.2 关于类、对象和面向对象的程序设计初步
1. 什么是类,怎样确定类
类与大自然的分门别类是同样一个概念,因为 C++ 是接近生活语言的一种计算机的高级算法语言,即大自然怎么分类,C++ 就怎么分类。
例如,人是一个大类,同时人又有许多子类,学生是个子类,教师也是其中的一个子类;数学是一个大类,高等数学是一个子类,初等数学也是一个子类,求最大公约数是初等数学里的一个子子类,等等。
同时,在 C++ 中,类也是一种数据类型。
2. 什么是对象
对象就是每个类中的一个个实体和事项,可以通过这些对象展示这些类的特征、性能、动作、处理事情的过程等,实现这些类所具有的功能。
例如,刚才所讲的学生是一个子类,大学生又是学生中的一个子子类,而王东同学是大学生中的一个实体,他不是类,他是属于这个类中的一个对象,通过他可以展示大学生类所具有的特征、性能、动作和功能等。例如,可以说王东的姓名、王东的性别、王东的高数成绩、王东今天上午考大学英语、王东下午4点打篮球,以及王东是个三好学生等,而不能说大学生(类)是个三好学生,大学生(类)是男的。即类的所有特征、性能和功能等均要通过对象才能引出、展示和实现。
3. 怎样从面向过程的程序设计转换为面向对象的程序设计(即面向对象的程序设计初步)
面向过程的程序设计就像是饭一盒、菜一盒,而面向对象的程序设计则是饭和菜共一盒,饭即是数据,菜即是函数,也就是说,面向过程的程序设计是数据归数据,函数归函数,而面向对象的程序设计是数据和函数在一个类里存放。例如,土豆牛肉饭、香菇鸡肉饭等就是饭和菜共一盒,若合适就整盒饭菜(整个类)拿为我用,而不用像面向过程的程序设计那么麻烦,有了菜(函数)还要另外配饭(数据),或反之。面向过程设计和面向对象设计的方法特点如表12-2所示。
| 设计方法 | 特 点 | ||||
| 数据(饭)和函数(菜)的放置 | 可读性 | 可维护性 | 可移植性 | 对数据的保护性 | |
| 面向过程设计 | 饭菜分家 | 不好 | 不好 | 不好 | 不好 |
| 面向对象设计 | 饭菜打包 | 好 | 好 | 好 | 好 |
那么二者之间的转换方法是饭菜打包:把原来的饭(数据)和菜(函数)打在一个包里,即声明一个类,再声明该类的对象,通过对象就可以吃这盒丰盛的饭菜(展示、操作和实现类的特性及功能等)了,这就从原来的面向过程的程序设计转换为面向对象的程序设计了。
另外,若不从转换的观点出发,那就把要操作的物体或事件声明为类,在这个类中,所需要的数据就声明为数据成员,所做的一切处理、操作就声明定义为函数成员,然后也要定义该类的对象,通过对象调用函数成员的操作,这样就完成面向对象的程序设计了。
4. 怎样声明类
类作为一种数据类型,该怎样声明呢?还是和以前声明数据类型的原则一样,先声明,然后使用,声明的具体方式如下。
方式一:声明类和定义函数成员合为一体,即
class ☐ ☐ ☐
{
访问控制说明符:
一系列数据成员的声明;
访问控制说明符:
一系列函数成员的定义;
}
例 12-1 中的类 Fig1 及类中的 dr2() 函数成员就是声明类与定义函数成员合为一体的例子。
方式二:声明类和定义函数成员分开,即
class ☐ ☐ ☐
{
访问控制说明符:
一系列数据成员的声明;
访问控制说明符:
一系列函数成员的声明;
}
类名::函数成员的定义
注:“类名::函数成员的定义”这部分可放在本文件内,也可放在其他文件中。例12-7中dr2()函数成员的声明与定义就是分开的。
方式三:方式一后连着对象的定义,即
class ☐ ☐ ☐
{
访问控制说明符:
一系列数据成员的声明;
访问控制说明符:
一系列函数成员的定义;
} 对象名;
例 12-1 中若把对象 dr 放在类 Fig1 的 “\}” 和 “;” 之间,即 “\}dr;\”,就是方式三的情况。
方式四:方式二后连着对象的定义,即
class ☐ 姓名
{
访问控制说明符:
一系列数据成员的声明;
访问控制说明符:
一系列函数成员的声明;
对象名:
类名::函数成员的定义
方式四的例子可参考例 12-7 中的 dr 对象。
上述4种声明类的方式中,有以下问题需要注意。
1)关键字 class 是法定的,表示类,相当于以前声明结构时的 struct,也是法定的,表示结构。
2)类的起名原则同变量,只是类名的第一个字母最好大写,以凸显该关键字是类名,这对于提高程序的可读性和可维护性均有好处。
3)在类中声明和定义的所有成员(数据和函数),全部用一对花括号“{}”括起来,并记住别丢了尾巴——“;”。
4)在类中要用到的数据成员,就相当于以前用到的变量或常量。注意:现在的数据成员的声明与原 $ \underset{\cdot}{来} $的变量或常量的定义方法基本相同,只是给数据成员赋初值时必须在构造函数里进行,其中给静态数据成员赋初值还有特殊的形式(见第15章)。
5)实现类的所有特征、性能、动作及功能的函数成员(或称为成员函数),在编写时仿照以前面向过程的程序设计中函数的编写方法即可。
6)访问控制说明符(或称访问属性)。
例如,声明了某个家庭是一个类,则这个家庭中的所有人和物品都是这个类的对象,这个家庭所有特征、操作及功能的展示和实现都是这个类中的成员,这些成员在这个类中是可以互相访问并使用的,是不用加以控制的。但这个家庭(类)中的某些成员是不能被这个家庭以外的人所随意使用的,那么就需要把这些成员限制(声明)为私有或保护成员。而有些成员,外人是可以来借用的,则这些成员就不用限制,即作为(声明为)公有成员。所以C++要用访问控制说明符来区分类中的成员,关于访问控制说明符的说明如表12-3所示。
| 访问控制说明符 | private | protected | public |
| 含义 | 私有成员 | 保护成员 | 公有成员 |
| 访问(即使用和被使用)范围 | 类的内部函数 | 类的内部函数 | 在任何地方,即类的内部或外部函数均可访问 |
| 何时应用 | 当数据和函数不让外部函数改变,即需要保护时 | 同左 | 当数据和函数不需要保护,即可以随意改变时 |
注意:如果不注明访问控制说明符,则默认是私有成员。外部函数包括其他类的函数成员和一般函数(如主函数)。
有关私有成员的实例见例 12-7。
7)当在类的声明之外定义函数成员时,因为要知道是哪个类的函数成员,所以要使用作用域区分符“::”来加以区分。例如:
本科生(类): 王东同学(数据成员)
研究生(类)∴计算考试平均分(函数成员)
在类的声明之外定义函数成员这种方式(即上述方式二),是比较好的声明定义方式,因为能使类中的成员更清晰明了,如例12-7。
5. 怎样定义对象
前面已经叙述过,声明了类以后,还要定义对象,才能实现函数成员的操作,下面就来看看如何定义对象。
和以前定义变量类似,原则还是:先定义,后使用,其定义格式如下:
数据类型 对象名:
即原来的变量名换成了现在的对象名,数据类型用已声明的类名,类似于以前的整型(int)等。
例如:
$$ \mathrm{Fig1~dr;//} 定义了类 \mathrm{Fig1} 的对象 \mathrm{dr} $$
从现在开始,以前的数据,现在说成数据成员;以前的函数,现在说成函数成员(或成员函数);以前的变量现在说成对象,但对象里不单单包含变量的数据,还包含了它所归属的类的所有特性及功能,即数据成员和函数成员,这些成员组成了类这个家族,对象是这个家族中的一员。
6. 特殊的成员函数——构造函数和析构函数
(1) 构造函数
1)什么是构造函数。
定义对象时,会在计算机的存储器中给这些对象开辟一个存放的房间并进行初始化,这些工作由特殊的函数成员——构造函数来完成,即在定义对象时,C++会自动调用构造函数来完成这些工作。
2)构造函数的写法与面向过程的程序设计中函数的写法基本相同,唯一的区别是构造函数没有返回值,即构造函数不写返回的数据类型。注意:构造函数名一定要与类名相同,并在公有成员中声明,定义则与其他函数成员一样,可在类内定义,也可在类外定义。
3 )构造函数的种类及用途。
构造函数有以下两种。
① 一种是 C++ 提供的构造函数(即不用自己编写),它只创建对象,即开辟一个存放的房间,但不做初始化工作。一般无须初始化时用这种方式,即不用声明构造函数。
注意:对数据成员初始化时,一定要在构造函数里赋初值,否则,编译时会出错,如在类中这样声明定义数据成员:
$$ \mathrm{p r o t e c t e d:i n t~i,j,k,n=5;} $$
则在编译时会出现如下错误信息:
error C2258: illegal pure syntax, must be '= 0'
E:\C++\hc++\C++BOOK\十二章\ex12-1.cpp(5): error C2252: 'n' : pure specifier can only be specified for functions
执行 cl.exe 时出错.
而例 12-2 中 “n=5;” 在构造函数 Fig1() 中初始化就是正确的。
但对函数成员中的局部变量的初始化就可以在函数定义段中进行,如例 12-1 中 dr2 的
局部变量 c 的定义如下:
void dr2()
{
char c='';
//c 是函数成员 dr2 的局部变量,c 的声明定义和使用只在 dr2 中才有效
}
② 另一种是自定义的构造函数,其中又分为两种。
其一是不带参数的构造函数,当不管创建什么对象,所做的初始化工作都相同时用这种方式。
【例 12-2】
Fig1::Fig1()
{
n=5;
}
注意:在构造函数中不要再给数据成员名称前加数据类型,否则,将作为局部变量处理了,即不是该类的数据成员了。
其二是带参数的构造函数,当创建不同的对象,希望初始化工作有区别时用这种方式。
【例12-3】
//12-1-4.cpp
Fig1::Fig1(int b)//创建对象 dr 时,实参 n1 的值传给了形参 b
{
n=b;
}
void main()
{
int n1;
cout<<"请输入定义对象所需的实际参数 n1"<<endl;
cin>>n1;
Fig1 dr(n1);//带参数的构造函数必须在定义对象时就带着实参 n1
dr.dr2();
}
(2) 析构函数
1 )析构函数是什么。
因为创建对象时开了存储房间,当程序运行完毕时,对象就消失了,那怎么退掉这个房间呢?由系统自动调用一种特殊的成员函数——析构函数来完成。
2)析构函数的写法基本与面向过程的程序设计中函数的写法相同,只是在析构函数声明句的最前面要加个符号“~”——按位求反运算符,并在公有成员中声明定义(或在公有成员中声明,在类外定义)。
3 )析构函数的用途及种类。
析构函数的用途只有一个:把开过的存储房间退掉,即释放这个存储空间。种类有两种:一种是默认的,系统肯定会自动调用;另一种是自定义的(可有可无),格式为:
~类名() {...}
这肯定是无参的,也没有返回类型,函数体可以写一些显示信息的语句,也可为空,即
~类名{}
注意:不能调用析构函数,系统肯定会自动调用的。如果没有特殊需要,一般不用显式地定义析构函数。
【例12-4】
//12-1-4.cpp
~Fig1()//析构函数
{
cout<<"释放对象 dr"<<endl;
}
7. 怎样通过对象进行操作
类外函数必须先定义该类的对象,然后才能通过对象对类中的成员进行操作。怎样通过对象进行操作呢?这个问题与对结构的分量进行操作很相像,所以将结构对分量的操作与对象对类中成员的操作进行如下对照说明:

由此可以得知:
1)通过对象对成员进行操作时的格式如下:
对象名.类的成员(数据成员和函数成员)
或
对象指针名->类的成员(数据成员和函数成员)
2)对象对类的成员的操作完全与结构对结构分量的操作相同。
【例 12-5】假设已有了一个类 Fig1,其下有公有的函数成员 dr2()和数据成员 x,则有程序如下。
#include<iostream.h>
class Fig1
{
private:
int N;
int i,j,k;
void in1()
{
cout<<"请输入行数 N"<endl;
cin>>N;
}
public:
void dr2()
{
in1();
char c='';
for(i=1;i<=N;i++)
{
for(j=1;j<=30-2*i;j++)
cout<<c;
}
for(k=1;k<=i;k++)
cout<<" #";
cout<<endl;
}
int x;
};
void main()
{
Fig1 dr;//定义了类 Fig1 的对象 dr
dr.dr2();//调用类 Fig1 中的函数成员 dr2()
dr.x=5;//给类 Fig1 中的数据成员 x 赋值
cout<<dr.x<<endl;
}
运行结果:
请输入行数 N
3
#
#
# #
5
结果正确。
【例 12-6】对例 12-5 用引用对象的方式来编程实现。
编写主函数如下:
void main()
{
Fig1 dr;//定义了类 Fig1 的对象 dr
Fig1 &y=dr;//声明了对象 dr 的别名 y
y.dr2();//用别名 y 进行操作和用真名 dr 进行操作的效果相同
y.x=5;
cout<<y.x<<endl;
}
用别名 y 进行操作和用真名 dr 进行操作的运行结果相同。
12.3.3 如何运用类、对象和面向对象的程序设计
采用了面向对象的程序设计方法之后,主函数 main 就变成简单的一两句或几句了,因为都尽量把程序段分门别类地归到类中了,所以主函数就显得简单明了了。
运用类、对象和面向对象的程序设计时,要注意类的声明、对象的定义及通过对象的操作这几个问题。
【例 12-7】请用面向对象的程序设计方法设计一个声明私有数据成员、私有函数成员和公有成员的程序。
程序代码如下:
//ex12-6.cpp
#include<iostream.h>
class Fig1//声明类 Fig1
{
private:
int N;//声明私有数据成员 N(画多少行)
int i,j,k;
void in1();//声明私有函数成员
public:
void dr2();//声明画图案的公有函数成员
~Fig1();//声明析构函数,注意:肯定要作为公有成员
} dr; //采用方式四声明类和对象
//以下至 main 之前是类 Fig1 的函数的定义
void Fig1::in1();//定义私有函数成员从键盘接收 N 值
{
cout<<"请输入行数 N"<<endl;
cin>>N;
}
void Fig1::dr2();//定义画图案的公有函数成员
{
in1();//调用私有函数成员
char c='';//定义局部变量 c 为一个空格
for(i=1;i<=N;i++)
{
for(j=1;j<=30-2*i;j++)
cout<<c;
for(k=1;k<=i;k++)
cout<<" #";
cout<<endl;
}
}
Fig1::~Fig1();定义析构函数
{
cout<<"释放对象 dr"<<endl;//此句要不要均可
}
void main()
{
//Fig1 dr:,因为采用了方式四声明类和对象,此句就不要了
dr.dr2();
}
运行结果:
请输入行数N
#
结果正确。
12.4 类、对象和面向对象的程序设计初步的亮点
- 在什么情况下用类、对象和面向对象的程序设计的亮点
在什么情况下都可以用面向对象的程序设计方法来设计编写程序。
- 类、对象和面向对象的程序设计初步具体知识的亮点
1)类与大自然的分门别类是同样一个概念。
2)对象就是每个类中的一个个实体和事项。
3)面向对象的程序设计是数据和函数在一个类里存放,即饭菜打包。把原来的数据和函数打在一个包里,即声明一个类,再声明该类的对象,通过对象就可以吃这盒丰盛的饭菜(展示、操作和实现类的特性及功能等)了,这就从原来的面向过程的程序设计转换为面向对象的程序设计了。
4)类和以前声明数据类型的原则一样,先声明,后使用。声明类有多种方式。
5)声明了类以后,还要定义对象,才能实现函数成员的操作。
6)对数据成员的初始化工作必须要在构造函数中进行。构造函数和析构函数均在公有成员中声明定义(或在公有成员中声明,在类外定义)。
7)对象对类的成员的操作完全与结构对结构分量的操作相同。
3. 运用类、对象和面向对象的程序设计的亮点
按照饭菜打包原则,设置并声明好类,定义好对象,然后正确地通过对象去调用类中的成员来进行操作,从而完成题目的要求。