面向对象与C++语言
2002年5月
摘 要:
本文介绍研究生“C++语言”课的教学设计和笔者的教学理解。除了交流的目的外,也想做一点计算机知识的宣传普及工作。本文的主要内容包括:面向过程和面向对象、从C到C++、C++中的主要概念及技术实现。
关键字:
面向对象、C++、研究生教育
从2000级开始,我校为研究生新生开设了计划学时为60(含上机10学时)的C++语言课,至今已操作两届。本文拟就该课程的教学设计和教学理解做一小结,期望与同行们交流;同时也想简要介绍面向对象(Object Oriented)的思想和C++语言。
一、教学背景和教学设计
目前我校共有工科专业硕士研究生点5个(食品科学、粮油与植物蛋白工程、农产品加工及储藏工程、结构工程、机械设计及理论)。考入我校的研究生分别来自不同的学校、单位和不同的专业,经调查,其中多数学过一门计算机高级语言(多为BASIC语言),也有少数学生学过C语言,还有少数学生(多为专科毕业)没有学过任何计算机语言,所以我们将设课目标最后确定为面向对象的C++语言,并根据学生的总体情况,对该课程做了如下的设计:
1、考虑多数学生已具有一门语言课的基础,对典型算法已基本掌握,所以算法不再作为主要授课内容。
2、以C++内含的C子集作为教学起点,详细讲授数据和过程,注意补充、深化、强化一些内容,如:机器数、位运算、递归算法、二维数组的按行和按列遍历、内存中的代码和数据、堆栈(stack)和堆(heap)、动态内存、链表操作、各种指针等。
3、授课重点是面向对象的编程思想及其在C++中的实现,内容包括:面向对象的主要技术特征、类和对象、类的聚集和派生、类成员的访问属性和类的派生方式、编译多态和运行多态等。因学时紧张,流类库和异常处理不再展开。
4、以微软的Visual C++为实验环境和工具,用户程序为控制台程序(console),并在模拟DOS环境下运行,不涉及Windows编程和基于MFC(Microsoft Foundation Classes——微软基础类库)的可视化编程。
5、以郑莉等编写的“C++语言程序设计”(清华版)为基本教材,但内容有所取舍,即:讲授第一章(概述)到第八章(多态性);学生自学第九章(群体类)到第十三章(MFC库与Windows程序开发概述)。
二、面向过程和面向对象
从世上第一个计算机高级语言Fortran诞生(1954~1956)到现在,计算机高级语言经历了从“非结构化”到“结构化”(七十年代)、从“面向过程”到“面向对象”(八十年代开始)的发展变化。前一变化着眼于改进程序的微观结构,提升程序的可阅读性和可维护性,对此,大家已比较熟悉,这里不再赘述。后一变化属编程思想的变化,它着眼于构造新的程序模块,提升程序的抽象层次和可复用层次,估计熟悉的人还不太多。
在计算机处理领域,“过程(procedure)”是指一段可重复利用的代码序列,用来实现某项独立的程序功能。在不同的语言工具中,“过程”有时也被称作“子程序(subrutine)”或“函数(function)”。所谓“面向过程”,它反映了早期的编程特点,其基本特征是数据和代码分离:编程前先根据问题设计数据结构,然后将大部分精力用于代码过程的组织和实现。由于数据和代码分离,造成了“面向过程”的如下缺点:① 一旦数据结构改变,有关过程乃至整个程序需要重新组织和设计;② 每次开发新软件时,由于数据结构的不同,除了最底层的过程(如库函数)可重复利用外,其它工作需要从头做起。
八十年代以来,随着计算机性能指标的提高和普及程度的提高,计算机应用水平也得到了迅速提高。多任务、图形界面、消息/事件驱动、网络、数据库、多媒体等新的应用给软件设计提出了新的要求,使得软件产品的功能越来越强,同时其规模也越做越大,软件的设计、开发、调试、维护面临着巨大的挑战。如果说面向过程的传统做法还能应付几万、几十万行程序规模的话,那么面对几百万行以及更大的程序规模,它已到了不能胜任的地步。人们希望软件能学一学硬件集成电路的做法,那就是,重复利用业已“成熟”的“模块(module)”资源,并在此基础上不断进行新的扩展。于是,诞生于六十年代末的“面向对象”的编程思想成为人们摆脱困境、实现理想的重要工具。
那么,究竟什么是“对象(object)”?简单地说,①“对象”是人们对所关注的事物在计算机处理领域进行“抽象(abstraction)”的产物,这种抽象既包括数据抽象,也包括功能抽象;②“对象”在形态上是一种将数据和代码捆绑在一起的新型数据。说得再具体一点,“对象”就是“提取”事物中被人们所关心的若干静态属性和动态属性,然后形成的一种可以被计算机处理的数据和功能的集合。
在一个典型对象(假定它描述学生张三)内,数据和代码一般是并存的(在特殊情况下,对象内可以没有数据,或者没有代码,或者两者都没有),其中:
① 数据用于描述对象的静态属性、即状态属性(比如学号和各科成绩)。在一般情况下,这些数据被“封装(encapsulation)”在对象内部,外界不能直接访问,只有通过特定的渠道才能访问到它们,从而将它们保护起来(数据隐藏),我们称这些数据其为该对象的私有(private)数据。
② 代码用于描述对象的动态属性,即功能行为方面的属性。当外界的某种请求作用于该对象时(如请求修改学号、请求计算并输出平均成绩),由代码提供相应的“行为”(修改其学号,计算并输出其平均成绩)。这些代码以内部“过程”的形式存在,它围绕对象中的数据并提供相应服务,包括:访问私有数据,对外界发来的“消息(message)”作出特定的反应。在“面向对象”的术语中,这些代码过程被称为“方法(method)”或“行为”。在一般情况下,这些代码过程可以通过“接口(interface)”被外界直接访问,因此这些代码接口具有公有(public)属性。
所谓“面向对象”,就是建立在对象和消息传递机制上的一种编程思想,按照这一思想,程序设计的首要任务是设计“对象”,包括对象中的数据和代码,然后,通过对象间的消息交互来实现程序功能。
“面向对象”技术的最本质的特征体现在对象的构成上。首先,对象具有很好的“自治”性,对象内不但包含了描述对象的数据,也捆绑了处理这些数据的代码,不再需要对象外的代码来直接处理这些数据;其次,“对象”中的数据与内部代码呈紧耦合状态(内部代码可随意访问对象中的数据),与外部代码一般呈松耦合状态(外部代码需通过特定的接口访问对象中的数据)。这种“内紧外松”的结构(附图),使得“对象”可以成为一种很好的、可被重复利用的软件“模块”,即,只要对象的“接口”不变,对象内部的改动基本上不影响外部对对象的使用。因此,当描述某类通用事物的“对象”(比如字符串、文件、Windows下的窗体)一旦被设计好,它可以作为一个模块,被不同的人、不同的程序、在不同的时间场合多次地重复使用;同时,“对象”内部仍保有继续改进的便利。
“面向对象”的主要特征和关键技术可以概括为以下几点:
1、抽象(abstraction)和封装(encapsulation):该特征描述了对象的生成机制,即:① 对象是数据抽象和功能抽象的产物。② 对象内部的实现细节被适当地隐蔽起来,包括:将内部数据隐藏起来,不让外界直接访问,以提高数据操作的安全性;将内部代码封闭在“接口”以内,限制规范它与外界的联系渠道。③ 对象的“内紧外松”结构使得对象具有很高的抽象能力和表现能力,并将软件的可复用层次从“库函数”提升到规模和档次更高的“对象”。
2、继承性(inheritance):该特征描述了对象作为模块资源的一种被利用机制,即:经过简单的声明操作,从已知对象可以迅速派生出新的对象,后者可全面继承前者的属性,同时也可改变从前者继承来的局部属性,并增添自己的新属性。这种特性很象生物学上的“遗传”和“变异”,即子代在继承亲代的基础上可以有变异。
3、多态性(polymorphism):该特征描述了用消息操作对象时的一种灵活机制,即:① 同一消息发送给同一对象,因“环境”不同可导致不同的行为;② 同一消息发送给具有不同“遗传”关系的对象,因对象不同也可导致不同的行为。采用多态技术可精简消息、提高消息在使用上的灵活性。
实际上,各软件公司已经为我们提供了一批通用的、商品化的对象“模块库”(如微软的MFC和Borland的OWL),使得我们在此基础上只做少量设计和编程就能实现以往不可想象的、非常复杂的程序功能。以应用程序的界面设计为例,在DOS年代有人做过统计,全部软件开发工作量的50~70%为界面设计;进入Windows年代,近600个API(Application Programming Interface)函数使界面设计的难度更是提升到让人望而生畏的程度;而建立在“面向对象”技术上的“可视化”编程却使这项工作变得十分简单。在可视化编程工具(如Visual Basic、Visual C++、Delphi、C++ builder等)的支持下,程序界面上各种可操作的屏幕元素(如窗口、菜单、对话框、按钮等)已被系统封装为对象、即控件,编程者要做的工作被简化到了以下几项:① 选择要使用的控件并确定其属性,确定使用者对屏幕控件的操作方式;② 观察屏幕控件的显示效果(可视化)并作相应调整;③ 编写少量代码,以响应使用者对屏幕控件的操作。
三、从C到C++
C和C++均出自美国AT&T贝尔实验室,并在各自的领域里获得了巨大的成功:诞生于七十年代初的C是一种非常优秀的“面向过程”程序设计语言,它强大、高效、简洁、灵活、接近硬件,甫一诞生便获得了程序员们的普遍喜爱,在“面向过程”的年代里曾风靡天下、无所不在;诞生于1983年的C++借助了C的巨大优势,很快在面向对象的领域里打开局面,成为该领域里的编程利器。
早期的C++没有自己的编译器,需要通过预处理先将程序转为C源程序,然后再用C的编译器处理为目标程序,所以C++从一开始便继承了C的全部资源并全面兼容了C。直到今天,C的源程序几乎仍可不加修改地在C++的环境下编译运行。所以,可以这样说:C++是C的超集,C是C++的子集,如果一个人从未学过C,那么在学习C++时,很大一块内容是在学习C。
尽管这样,C++和C还是有着根本的不同:C是“面向过程”的;C++既可“面向过程”,也可“面向对象”,但其立意是“面向对象”。C++对C的改进表现在以下两个方面:
① 对C的传统手段进行改进。例如:强化数据类型的检查,允许更灵活地定义自动变量,引入引用型变量,引入函数重载(overloading)和函数模板,引入单行注释,用带有类型属性的const常数代替用#define定义的符号常数,用内联函数取代带参宏定义,将动态内存的申请和释放由函数操作改为运算符操作,建立异常处理的模块机制等。由于这些改进,C++首先是一个更安全、更好用的C。
② 引进“面向对象”机制。C++构造了一种全新的抽象数据类型(Abstruct Data Type-ADT)——“类(class)”,用它来描述某类“群体”事物的共同属性。在“类”中,数据和代码并存,分别描述该“群体”(比如学生)的状态属性和功能属性。当程序需要涉及类中某个特定事物(比如学生张三)时,可以用“类”来定义“对象”。在C++中,“对象”和“类”的关系与传统C程序中“变量”和“数据类型”的关系十分相似。通过“类”和“对象”,C++实现了数据及功能的抽象和封装。
回顾C++诞生以来的发展历史,我们可以看到它的匆匆脚步:1985年,由AT&T贝尔实验室推出的C++1.0版提供了面向对象的初步机制,即:函数及运算符重载、公有及私有类成员、友元、虚函数等;到1989年,其C++2.0版开始配备独立的C++编译器,并引入多继承、保护类成员、静态成员函数和抽象类等机制;到1993年,其C++3.0版又引入类模板、异常处理等机制;1994年,美国国家标准局颁布ANSI C++标准。C++至今仍在发展,不断完善其面向对象的技术和手段、包括类库的标准化也在积极的进行中。
四、C++中的主要概念及技术实现
在C++的教学中,以下概念和技术比较抽象又十分重要,必须仔细加以辨别和强调:
1、类和对象
类(class)和对象(object)有以下的区别:① 类描述的是同类事物的“群体”属性(比如学生);对象描述的是群体中的“个体”属性(比如学生张三和李四);② 类是对象的生成模板,对象是类的实例(instance)化;③ 类在内存中无实体存在,对象在内存中有实体存在,而且可以同时存在多个对象实体。
2、类的聚集、派生和关联
类的聚集、派生和关联反映了类作为一种模块资源,可以被重复利用的不同机制。其中:
“类的聚集(aggregation)”描述的是类之间的一种包含关系,即复杂事物(比如直线)中包含着简单事物(比如直线的两个端点),这种关系可以用“有一个(has-a)”来进行描述(比如直线有一个端点,当然还有另一个端点)。在C++中,当一个类中定义有其它类的内嵌子对象时这个类被称为“聚集类”。
“类的派生(derived)”也称“类的继承(inheritance)”,描述的是类之间的一种派生或继承关系,即抽象程度较高的(一般化)事物(比如多边形)通过进一步的“刻画”可以派生出抽象程度较低的(具体化)事物(比如矩形),前者称“父类”或“基类”,后者称“子类”或“派生类”,因后者继承了前者的属性,其关系可以用“是一个(is-a)”来进行描述(矩形是一种多边形)。
值得注意的是,在实际问题中,两类有着派生关系的事物,除了属性继承的一面外,有时还有属性约束或属性改变的一面,比如描述企鹅的类可以从描述鸟的类中派生出来,因为企鹅也是一种鸟;但是,鸟普遍会飞,企鹅却不会飞(会游泳)。所以,C++为类的派生提供了以下3种机制:① 子类全面继承父类的属性(默认),② 子类可以在父类属性的基础上追加自己的新属性(通过定义子类的新成员来实现),③ 子类可以改造或去除父类的某些属性(通过定义与父类同名的类成员进行同名覆盖来实现)。
“类的关联(assosiation)”是指一个类与另一个类之间的联系沟通,是一个内涵较宽的概念。比如:类的聚集和派生就是类之间的一种强关联;声明一个类是另一个类的友元,也可造成两个类之间的关联。此外,在C++中,类之间的关联还可以通过以下方式来实现:① 在一个类的数据成员中定义另一个类的指针;② 在一个类的成员函数中,以对象或指针的形式定义另一个类的局部对象、形参或者返回类型。通过类的关联,一个类可以调用另一个类的内部资源。
3、消息和成员函数
早期计算机一般工作于单任务环境,即一段时间里只做一件事,等这件事完了再做另一件事,比如在DOS操作系统的管理下,命令需要一条一条输到计算机里并一条一条地执行,这种工作模式为命令驱动模式。现在的计算机一般工作于多任务环境,即它可以同时做好几件事(实际上计算机是将时间切割成小的碎片,轮流提供给不同的任务,由于计算机的速度太快,给人的感觉是这些任务在同时进行)。比如在Windows操作系统的管理下,屏幕上可同时打开多个窗口(执行多个程序),在有的窗口下还可同时打开多个文档子窗口,为了及时响应用户对不同窗口/子窗口的操作,要求计算机采用消息/事件驱动模式。
从操作的角度来看,消息是用户操作计算机输入设备(针对窗口和屏幕元素)所产生的信息,比如按动键盘上的按键,拖动鼠标或点击鼠标左键等;实际上,消息也可以由内部设备或内部过程来产生,比如当时钟运行到某个时刻时产生要求做某件事情的消息,又比如在屏幕内容变动后产生要求“刷新”屏幕的消息等。
从程序的角度来看,消息和消息所引发的行为反映了对象的某种动态属性。在C++中,当我们调用某个对象的成员函数时,实际上就是在向该对象发送消息,其中:函数的名字反映了消息的类型(要对象做某件事),函数的参数反映了消息发生的环境(对象做事所需要的条件),函数的执行结果反映了该对象针对该消息所产生的行为(对象做事的结果)。
4、类成员的被访问属性
类成员的被访问属性也称类成员的访问控制或访问权限,它主要讨论两个问题:①该成员是否可以被类中的其它成员所访问,②该成员是否可以被类外(非友元)的代码所访问。C++使用3个关键字public(公有)、private(私有)、protected(保护)来描述类成员的访问属性。但是,由于类的派生关系,类成员的被访问属性变得比较复杂,须分两个层次来进行讨论。
① 该成员在当前类中定义,并只在当前类中讨论其被访问属性,情况如表1所示。

② 该成员在直接父类中定义,通过public(公有)、private(私有)或protected(保护)等3种派生方式继承到当前子类中,被继承过来的这部分原父类成员,在派生类中所具有的被访问属性如表2所示。
下面,对其稍加讨论:
① 如果不考虑继承,一个类中拥有公有和私有两类成员就足够了。在C++中,最一般的用法是将类中的数据定义为私有成员,将类中的函数定义为公有成员。其中,私有机制为数据提供保护,公有机制为对象提供它与外界的接口。
② 公有派生是最常使用的派生机制,派生后原父类成员在子类中的被访问属性均不改变。
③ 私有派生是最少使用的派生机制,派生后产生了在类内和类外均不能访问的“不可访问成员”,原父类中的公有成员和保护成员在子类中均成为私有成员,这将“阻断”向下继承的“链条”并“堵塞”有限的访问“通道”。
④ 保护成员和保护派生提供了这样一种机制,即父类可向子类“传递”某些成员,对这些成员,外界不能直接访问,但它们可以不断向后代传递。打个比方,这些成员有点象“祖传秘方”,它可以被后代“子孙”继承并访问,但永远不能被外界所访问。
⑤ 在特别强调访问效率的场合,为了外部代码能直接访问类中的私有成员,C++提供了友元(friend)机制:即在类中用关键字friend说明的外部函数或外部类,是该类的“友元函数”或“友元类”,其代码可无碍地访问该类中的私有成员。使用友元应谨慎并权衡利弊:一方面它提高了访问效率,另一方面它破坏了类的封装。
5、单继承、多继承和虚拟继承
子类若继承单一的父类,我们称之为“单继承”;子类若同时继承多个父类,我们称之为“多继承”。多继承会产生一些连带问题,试考虑这样的情况:B1和B2两个派生类均从A类派生而来(单继承),而C类同时继承了B1类和B2类(多继承)。由于C中同时存在着A的两份成员拷贝(分别来自B1和B2),如果不加说明,在访问这部分成员时将产生歧义;同时,在C中同时保留A的两份拷贝也不够经济。解决的办法是,在派生B1和B2时将它的父类A用关键字virtual说明其为虚基类,此后在从B1和B2派生C时将只保留一份A的有效拷贝,这种继承方式被称为“虚拟继承”。

6、编译多态和运行多态、虚函数和纯虚函数
“多态(polymorphism)”的字面意思是“多形”。例如,碳、石墨、金钢石是碳的“多形(同质异构)”;在面向对象术语中,“多态”是指同样的消息会使对象产生不同的表现。在日常生活中这样的例子也很多,比如同样一个“打”字,因上下文环境的不同可表现不同的语义,象“打牌”、“打架”、“打牙祭”等。采用多态机制,可以精减与对象打交道时所使用的“消息”或“词汇”。
在C++中,“多态”是通过调用名字相同但功能不同的函数来实现的,具体“决定”调用哪个同名函数的过程被称为“绑定(binding)”。C++中有着两种多态——绑定机制,分别称“编译多态”(即“早期绑定”或“静态绑定”)和“运行多态”(即“后期绑定”或“动态绑定”):
① 编译多态——这种多态在编译时进行绑定处理。C++允许在某个操作域中定义若干名字相同但参数不同的函数,这就是函数名重载。编译时,看所调函数的实参与哪个同名函数的形参相匹配就决定调用那个同名函数,从而实现绑定。编译多态可以表现这样一种多态机制,即同一消息被同一对象所接收,因环境(函数参数)不同而导致不同的对象行为(函数功能)。
② 运行多态——这种多态在程序运行中进行绑定处理。它表现的多态机制是:有着继承关系的不同对象在收到同一消息后,可产生不同的表现。运行多态建立在以下的事实和技术基础上。即:
多个类因派生关系可形成类的“树枝”或“链条”,我们称它们为“类族(hierarchy)”。“类族”中的类往往有着既相似又不同的某些功能属性,因此可以用同名函数来描述这些相似属性。比如从“空心图形(shape)”类可派生出“矩形(rectangle)”、“三角形(triangle)”和“圆(circle)”3个类,从而构成一个类族,其中的3个子类都能计算面积但算法有所不同,我们可以用名字相同、参数相同、但功能不同的成员函数Area(void)来加以实现。为了实现运行多态,C++要求这些成员函数要用关键字virtual说明为虚函数,以便指示绑定是在运行中而不是在编译中完成。
有些父类因高度抽象其某些行为属性已无法描述,如上面提到的空心图形(shape)类中的Area( )函数,在C++中,它只能设计成无函数体的纯虚函数,即Area(void)=0。这种含有纯虚函数的类被称为“抽象类”。抽象类没有生成实例的能力,但可以继续派生子类。实际上,包括纯虚函数在内的虚函数描述了该“类族”的某种共有“行为”,提供了该“类族”对外的公共“接口”。换句话说,你可以通过同名的虚函数Area( )来跟以上任何一个非抽象类的对象打交道。
C++的“赋值兼容”规则允许父类“指针”或父类“引用”指向或引用其子类对象(包括多次派生后的子类对象),并靠这一机制和虚函数的机制来实现运行多态。在C++中,实现运行多态的前提是,必须通过父类“指针”或父类“引用”来调用类族的虚成员函数。运行中要看当时基类“指针”指向的对象是哪个类、或者基类“引用”所引用的对象是哪个类,是哪个类的对象就将绑定那个类的同名虚成员函数,从而实现运行多态。
下一页 教学论文——我校电子信息类专业的建设构想


