第四章 C++面向对象程序设计方法概述

By | 07月15日
Advertisement

第四章 C++面向对象程序设计方法概述

会用C++的程序员一定懂得面向对象程序设计吗?

不会用C++的程序员一定不懂得面向对象程序设计吗?

两者都未必。

我曾经和很多C++程序员一样,在享用到C++语法的好处时便以为自己已经明白了面向对象程序设计方法。我就这样糊里糊涂地编写了十几万行C++程序,如此使用C++,就象挤掉牙膏卖牙膏皮那样,真是暴殄天物呀。

本章目的不是阐述面向对象的理论,而是用浅显的示例来解说面向对象程序设计的一些重要概念,如封装、继承、组合、虚函数、抽象基类、动态联编、多态性等。本章对本书的后面十章有指导意义。

4.1 漫谈面向对象

在第一次世界大战结束前夕,一个叫做路德维希·维特根斯坦的维也纳人,在意大利的战俘营里写了一本《逻辑哲学论》。这本75页的小册子提出了对象的观点:

世界可以分解为事实;

事实是由原子事实组成的;

一个原子事实是多个对象的组合;

对象是简单的;

对象形成了世界的基础。

五十年之后,面向对象(Object-Oriented, OO)方法论火起来了,现在“对象”真的成为了软件世界的基础。

面向对象分析设计(OOAD)方法兴起于20世纪80年代,从90年代起至今它已经在分析设计领域占据了无可争议的主流地位。

我在读本科(90年至94年)时就充分地感受到了人们对“面向对象”的狂热。关于“面向对象”的课堂、学术报告会常常人满为患。搞软件研发的人都“言必谈对象”,并引以为荣。

面向对象分析设计领域有一些比较著名的学派,如:

² Coad和Yourdon学派

² Booch学派

² Jocobson学派

² Rumbaugh学派

有趣的是,这些学派的掌门人就像上帝、真主、如来佛,他们用各自的方式定义了这个世界,并留下一堆经书来解释这个世界。这种混乱的局面被学术界称为百家争鸣,每年诞生了许多论著和教授。叫苦的是软件企业和开发人员:没有统一的方法,不好干活啊!

终于等到了那一天,Rational公司招纳了Booch, Jocobson, Rumbaugh,这三位“面向对象”业界的老大强强联手,制定了“统一建模语言”(UML)。1997年11月,UML被国际对象管理组织(OMG)采纳,此后UML成为OOAD建模语言的国际标准。

有趣的是,面向对象编程语言比OOAD方法论更早地问世。最早的面向对象编程语言是Smalltalk,由施乐公司研究中心于1970年研制。在软件开发领域,编程实践往往先行于相应的理论。就如人类的进化:先学会讲话,后来才产生文字。用程序员的行话讲,这叫“编程是硬道理”。

六七年前,我刚“热恋”面向对象时,急切地想知道什么是面向对象,于是买了一堆书来阅读。

不少书籍建议这样找“对象”:分析一个句子的语法,找出名词和动词,名词就是对象,动词则是对象的方法(即函数)。

天哪,这不是程序员的做法!我除了发现自己有些“弱智”之外别无收获。

当年国民党的文人为了对抗毛泽东的《沁园春·雪》,特意请清朝遗老们写了一些对仗工整的诗,请蒋介石过目。老蒋看了气得大骂:“娘希匹,全都有一股棺材里腐尸的气味。”

不好意思,我初读面向对象理论书籍的感觉与老蒋的有点相似。

现在我有些心得体会了,我建议程序员应当先学习用C++或者Java编写程序,当他们对面向对象程序设计有了感性认识之后,再阅读面向对象理论书籍,这样才能深入理解面向对象方法。

面向对象编程语言很多,如Smalltalk、Ada、Eiffel、C++、Java等等。C++语言最受程序员喜欢,因为它兼容C语言,所以应用最广泛。Java是一种纯面向对象语言,它诞生之初曾红极一时,不少人叫喊着要用Java革C++的命。我认为Java好比是C++的外甥,虽然不是直接遗传的,但也有几分象样。外甥在舅舅身上玩耍时洒了一泡尿,俩人不该为此而争吵。

4.2 信息隐藏与类的封装特性

在一节不和谐的课堂里,老师叹气道:“要是坐在后排聊天的同学能象中间打牌的同学那么安静的话,就不会影响到前排睡觉的同学了。”

这个故事告诉我们,如果不想让坏事传播开来,就应该把坏事隐藏起来,“家丑不可外扬”就是这个道理。

对于软件设计而言,为了尽量避免某个模块的行为干扰同一系统中的其它模块,应该让模块仅仅公开必须要让外界知道的内容,而隐藏其它一切内容。

“信息隐藏”这种设计理念产生了C++类(Class)的封装特性。

类可以将数据和函数封装在一起,其中函数表示了类的行为(或称服务)。类提供关键字public、protected和private用于声明哪些数据和函数是公有的、受保护的或者是私有的。这样可以达到信息隐藏的目的,即让类仅仅公开必须要让外界知道的内容,而隐藏其它一切内容。例如:

class WhoAmI

{

public:

void GetMyName(void); // 名字是可以公开的

protected:

void GetMyAsset(void); // 财产是受保护的,只有我和继承者可以使用

private:

void GetMyGuilty(void); // 罪过是要保密的,只有我自己才能偷看

};

类的封装特性是C++的基本语法之一,易学易用。要注意的是,我们不可以滥用类的封装特性,不要把毫不相干的数据和函数封装到类里头,不要把类当成火锅,什么东西都往里扔。

4.3 类的继承特性

对象是类的一个实例(Instance)。如果将对象比作房子,那么类就是房子的设计图纸。所以面向对象设计的重点是类的设计,而不是对象的设计。对于C++程序而言,设计孤立的类是比较容易的,难的是正确设计基类及其派生类。

如果A是基类,B是A的派生类,那么B将继承A的数据和函数。例如:

class A

{

public:

void Func1(void);

void Func2(void);

};

class B : public A

{

public:

void Func3(void);

void Func4(void);

};

main()

{

B b;

b.Func1(); // B从A继承了函数Func1

b.Func2(); // B从A继承了函数Func2

b.Func3();

b.Func4();

}

这个简单的示例程序说明了这样一个事实:C++的“继承”特性可以提高程序的可复用性。正因为“继承”太有用、太容易用,才要防止乱用“继承”。我们应当给“继承”立一些使用规则:

l 【规则4-3-1 如果类A和类B毫不相关,不可以为了使B的功能更多些而让B继承A的功能和属性。不要觉得“白吃白不吃”,让一个好端端的健壮青年无缘无故地吃人参补身体。

l 【规则4-3-2 若在逻辑上B是A的“一种”(a kind of ),则允许B继承A的功能和属性。例如男人(Man)是人(Human)的一种,男孩(Boy)是男人的一种。那么类Man可以从类Human派生,类Boy可以从类Man派生。

class Human // Human是基类

{

};

class Man : public Human // Man是Human的派生类

{

};

class Boy : public Man // Boy是Man的派生类

{

};

u 注意事项

【规则4-3-2看起来很简单,但是实际应用时可能会有意外,继承的概念在程序世界与现实世界并不完全相同。

例如从生物学角度讲,鸵鸟(Ostrich)是鸟(Bird)的一种,按理说类Ostrich应该可以从类Bird派生。但是鸵鸟不能飞,那么Ostrich::Fly是什么东西?

class Bird

{

public:

virtual void Fly(void); // 鸟能飞行

};

class Ostrich : public Bird // 鸵鸟是鸟的一种

{

public:

virtual void Fly(void); // 如何让鸵鸟飞起来?

};

再例如,从数学角度讲,圆(Circle)是一种特殊的椭圆(Ellipse),按理说类Circle应该可以从类Ellipse派生。但是椭圆有长轴和短轴之分,如果圆继承了椭圆的长轴和短轴,岂非画蛇添足?

所以更加严格的继承规则应当是:

l 【规则4-3-3 若在逻辑上B是A的“一种”,并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性。

4.4 类的组合特性

组合(Composition)用于表示类的“整体/部分”关系。例如主机、显示器、键盘、鼠标组合成一台计算机。继承则表示类的“一般/特殊”关系。继承与组合显然不是相似的概念,但奇怪的是,程序员经常在编程时把继承与组合混为一谈。

l 【规则4-4-1若在逻辑上A是B的“一部分”(a part of),则不允许B从A派生,而是要用A和其它东西组合出B。

例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head应该由类Eye、Nose、Mouth、Ear组合而成,不是派生而成。

class Eye

{

public:

void Look(void);

};

class Nose

{

public:

void Smell(void);

};

class Mouth

{
public:

void Eat(void);

};

class Ear

{

public:

void Listen(void);

};

// 正确的设计,虽然代码冗长。

class Head

{

public:

void Look(void) { m_eye.Look(); }

void Smell(void) { m_nose.Smell(); }

void Eat(void) { m_mouth.Eat(); }

void Listen(void) { m_ear.Listen(); }

private:

Eye m_eye;

Nose m_nose;

Mouth m_mouth;

Ear m_ear;

};

如果允许Head从Eye、Nose、Mouth、Ear派生而成,那么Head将自动具有Look、 Smell、Eat、Listen这些功能。程序如下:

class Head : public Eye, public Nose, public Mouth, public Ear

{

};

采用继承方法来实现的Head程序十分简短并且运行正确,但是这种设计方法却是不对的。

一只公鸡使劲地追打一只刚下了蛋的母鸡,你知道为什么吗?

因为母鸡下了鸭蛋。

许多刚刚接触C++的程序员恨不得在所有的地方都使用继承,然后得意洋洋的宣称已经充分利用了面向对象的好处。“运行正确”的程序不见得是高质量的程序,此处就是一个例证。

4.5 动态特性

在绝大多数情况下,程序的功能是在编译的时候就确定下来了,我们称之为静态特性。反之,如果程序的功能是在运行时刻才确定下来的,称之为动态特性。

动态特性是面向对象语言最强大的功能之一,因为它在语言级别上支持程序的可扩展性,而可扩展性则是软件设计追求的重要目标之一。

C++的虚函数、抽象基类、动态联编和多态性(Polymorphism)构成了出色的动态特性。

4.5.1 虚函数

假定几何形状的基类为Shape,其派生类有Circle、Rectangle、Ellipse等,每个派生类都能够用绘制自己的形状。不管派生类的形状如何,我们希望用统一的方式来调用绘制函数,最好是使用Shape定义的函数接口Draw,并让程序在运行时动态地确定应该使用那个派生类的Draw函数。

为了使这种行为可行,我们把基类Shape中的函数Draw声明为虚函数,然后在派生类中重新定义Draw使之绘制正确的形状。虚函数的声明方法是在基类的函数原型之前加上关键词virtual。

一旦类的一个函数被声明为虚函数,那么其派生类的对应函数也成为虚函数。虽然函数在类层次结构的高层中声明为虚函数将会使它在底层自动(隐式)地成为虚函数,但是为了提高程序的清晰性,建议在每一层中将它显式地声明为虚函数(即加virtual)。

例如:

class Shape

{

public:

virtual void Draw(void); // Draw 为虚函数

};

class Rectangle : public Shape

{

public:

virtual void Draw(void); // Draw 为虚函数

}

4.5.2 抽象基类

当我们把类看作是一种数据类型时,通常会认为该类肯定是要被实例化为对象的。但是在很多情况下,定义那些不被实例化为对象的类是很有用的,这种类称为抽象类(Abstract Class)。能够被实例化为对象的类称为具体类(Concrete Class)。抽象类的唯一目的就是让其派生类继承它的函数接口,因此它通常也被称为抽象基类(Abstract Base Class)。[Deitel, p175]

如果将基类的虚函数声明为纯虚函数,那么该类就成为抽象基类。纯虚函数是在声明时其“初始化值”为0的函数,例如:

class Shape // Shape是抽象基类

{

public:

virtual void Draw(void)=0; // Draw 为纯虚函数

};

抽象基类Shape的纯虚函数Draw根本不知道它自己能干什么,具体功能必须由派生类的Draw函数来实现。

很多良好的面向对象系统中,其类层次结构的顶部通常是抽象基类,甚至可以有好几层的抽象类。例如几何形状的类结构可分三层(如图4-1所示),顶层是抽象基类Shape,第二层也是抽象类Shape2D和Shape3D,在第三层才是可以被实例化为对象的具体类,如二维形状类Circle、Rectangle和Ellipse,三维形状类Cube、Cylinder和Sphere。

第四章	C++面向对象程序设计方法概述

Shape

Shape2D

Shape3D

Circle

Rectangle

Ellipse

Cube

Cylinder

Sphere

抽象基类

抽象类

图4-1 几何形状的类结构

4.5.3 动态联编

如果将基类Shape的函数Draw声明为virtual,然后用指向派生类对象的基类指针调用Draw,那么程序会动态地(即在运行时)选择该派生类的Draw函数,这种特性称为动态联编。例如:

Shape *aShape;

Circle aCircle;

Cube aCube;

Sphere aSphere;

aShape = &aCircle;

aShape->Draw(); // 绘制一个circle

aShape = &aCube;

aShape->Draw(); // 绘制一个cube

aShape = &aSphere;

aShape->Draw(); // 绘制一个Shpere

动态联编可以使独立软件供应商(ISV)在不透露技术秘密的情况下发行软件包,即只发行头文件和二进制目标码,不必公开源代码。软件开发者可以利用继承机制从ISV提供的类库中派生出新的类。和ISV类库一起运行的软件也能够和新的派生类一起运行,并且能够通过动态联编使用新派生类的虚函数。

4.5.4 多态性

当许多派生类因为继承了共同的基类而发生关系时,每一个派生类的对象都可以被当成基类的对象来使用,这些派生类对象能对同一函数调用作出不同的反应,这就是多态性。多态性是通过虚函数和动态联编实现的。例如:

void Draw(Shape *aShape) // 多态函数

{

aShape->Draw();

}

main()

{

Circle aCircle;

Cube aCube;

Sphere aSphere;

Draw(&aCircle); // 绘制一个circle

Draw(&aCube); // 绘制一个cube

Draw(&aSphere); // 绘制一个Sphere

}

综合C++的“虚函数”和“多态”,有如下突出优点:

² 应用程序不必为每一个派生类编写功能调用,只需要对基类的虚函数进行处理即可。这一招叫“以不变应万变”,可以大大提高程序的可复用性和可扩展性。

² 派生类的功能可以被基类指针引用,这叫向后兼容。以前写的程序可以被将来写的程序调用不足为奇,但是将来写的程序可以被以前写的程序调用那可了不起,这正是动态特性的妙处。

4.6 小结

C++是应用最广泛的面向对象编程语言,在作者心目中C++/C是程序员的正宗语言。学好C++/C后,再学习其它编程语言如Visual Basic、Java就非常容易。面向对象不会是编程语言的终点。我们现在不知道OO之后的“XO”是什么东西,但至少可以推知,“XO”的核心概念必然高于并包容对象这一概念。正如对象高于并包容了函数和变量一样。

C++/C程序设计如同少林寺的武功一样博大精深,作者练了十年,大概只有五成功力。所以无论什么时候,都不要觉得自己的编程水平很高,要虚心学习。

如果你会编写C++/C程序,不要因此得意洋洋,这只是程序员的基本技能而已。如果把系统分析和系统设计比作“战略和战术”,那么编程充其量只是“格斗技能”。如果指挥官是个大笨蛋,士兵再勇敢也会打败仗的。所以程序员不要只把眼光盯在程序上,要让自己博学多才。我们应该向北京胡同里的小孩们学习,他们小小年纪就能指点江山,评论世界大事。

Similar Posts:

  • 第六章 C++面向对象程序设计

    第六章 C++面向对象程序设计 六年前,我刚热恋"面向对象"(Object-Oriented)时,一口气记住了近十个定义.六年 后,我从几十万行程序中滚爬出来准备写点心得体会时,却无法解释什么是"面向对象", 就象说不清楚什么是数学那样. 软件工程中的时髦术语"面向对象分析"和"面向对象设计",通常是针对"需求分析" 和"系统设计"环节的."面向对象"有几大学派,就象

  • 【JavaSE入门系列】第02章_面向对象程序设计

    第02章面向对象程序设计--v512工作室   编辑:玄玉 面向对象思想 构造方法   我的博客 类的属性和方法 Java API文档 v512工作室 对象的创建和使用 信息的封装和隐藏 中科院新科海学校 面向对象思想 分析阶段:分析并确定用户需求,采用适当的模型规范地表述这一需求.形成分析模型,为要处理的现实世界中的事物建立抽象建模 设计阶段:确定系统如何实现所需功能,采用适当的数据结构和控制逻辑,将分析模型细化 编码实现:选定一种适当的编程语言,编码实现上述的设计,并在开发过程中引入测试,完

  • c++ primer(第五版)学习笔记及习题答案代码版(第十五章)面向对象程序设计

    笔记较为零散,都是自己不熟悉的知识点. 习题答案至于一个.h 和.cc 中,需要演示某一题直接修改 #define NUM****, 如运行15.30题为#define NUM1530: chapter 15 1. 面向对象程序设计的核心思想是数据抽象.继承和动态绑定.通过使用数据抽象,我们可以将类的实现和接口分离. 使用继承可以定义相似额类型并对其相似关系建模:使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象. 基类将类型相关的函数与派生类不做改变直接继承的函数区

  • 有关JAVA的面向对象程序设计方法较全面介绍的一本书

    以后慢慢消化吧 虽然不是权威的著作,不过对我这样一个新手来说,还有非常有参考价值的. Java 与 UML 面向对象程序设计 java设计模式

  • Java-第十四章-代参的方法(二)-编程实现,输入班里10名学生的身高,获得身高最高的学生要求对象数组类型方法

    package com.ww.yzpA; public class Students { int No; int Height; } package com.ww.yzpA; public class Height { public Students getMaxHeigth(Students[] str) { Students A = new Students(); for (int i = 0; i < str.length; i++) { if (str[i].Height > A.He

  • Java-第十四章-带参的方法(二)-添加Search方法(),实现学生姓名查找

    package com.ww.yzpC; public class Find { public boolean searchNames(String[] names, String name) { boolean find = false; for (int i = 0; i < names.length; i++) { if (names[i].equals(name)) { find = true; } } return find; } } package com.ww.yzpC; impo

  • 第四章 时钟 windows程序设计 王艳平版

    /////////////////////////////////////////////////////////////// // Clock.cpp文件 #include <windows.h> #include "resource.h" #include <math.h> LRESULT __stdcall WndProc(HWND, UINT, WPARAM, LPARAM); int __stdcall WinMain(HINSTANCE hInsta

  • 《Java程序设计》第四章-认识对象

    20145221<Java程序设计>第四章-认识对象 总结 教材学习内容总结 类与对象 定义:对象是Java语言中重要的组成部分,之前学过的C语言是面向过程的,而Java主要是面向对象的.Java中变量有2种类型,一个是基本类型,另一个则是类类型.使用Java撰写程序几乎都是在使用对象,要产生对象必须先定义类,类是对象的设计图,对象是类的实例. 特点: 有别于C语言的程序编写,在用Java编写中,如果需要什么功能,我们就可去找一个对象,而这个对象就包含这个功能,然后通过new建立对象,通过&q

  • C++11读书笔记——第十五章 面向对象程序设计

    第十五章 面向对象程序设计 --人为了得到什么需要付出同等的代价,这就是等价交换原则. 面向对象程序设计基于三个基本概念:数据抽象.继承和动态绑定.前面已经介绍了数据抽象的知识,本章将介绍继承和动态绑定.继承和动态绑定对程序的编写有两方面的影响:一是我们可以更容易地定义与其他类相似但不完全相同的新类:二是在使用这些彼此相似的类编写程序时,我们可以在一定程度上忽略它们的区别.这一章的知识能够使我们在别人的类的基础上构造属于自己的类(以及定义自己的类的操作),减轻了程序员编写程序的负担,实现了程序的

  • 《Java面向对象程序设计——图形化方法》译序

    还记得几年前刚刚走出校园时候的一次面试经历.负责面试的技术主管出了一道Java编程题,我很容易就完成了.在和他讨论的时候,他又提出一个问题:"面向对象的3个主要特征是什么"?我就毫不犹豫地就说出了答案:"封装.继承和多态".接下来,他又问道:"什么是封装.继承和多态呢"?这时候我急得满头大汗,就是答不上来,最终仓皇败下阵来,错失一次良机. 其实,今天很多大学生在学习OO编程的时候(不管是Java还是C++),几乎都面临着和我当年相同的窘境:也许能

Tags: