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

 

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

 

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

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

两者都未必。

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

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

 

4.1 漫谈面向对象

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

世界可以分解为事实;

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

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

对象是简单的;

对象形成了世界的基础。

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

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

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

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

²        CoadYourdon学派

²        Booch学派

²        Jocobson学派

²        Rumbaugh学派

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

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

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

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

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

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

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

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

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

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

 

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

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

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

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

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

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

class WhoAmI

{

    public:

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

 

protected:

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

 

private:

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

   

};

 

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

 

4.3 类的继承特性

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

如果A是基类,BA的派生类,那么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();      // BA继承了函数Func1

        b.Func2();      // BA继承了函数Func2

        b.Func3();

        b.Func4();

}

 

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

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

 

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

  class Human                 // Human是基类

{

       

};

 

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

{

       

};

 

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

{

       

};

 

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 若在逻辑上BA的“一种”,并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性。

4.4 类的组合特性

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

 

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

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

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;

};

 

如果允许HeadEyeNoseMouthEar派生而成,那么Head将自动具有Look SmellEatListen这些功能。程序如下:

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

{

};

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

 

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

因为母鸡下了鸭蛋。

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

4.5 动态特性

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

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

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

4.5.1 虚函数

假定几何形状的基类为Shape,其派生类有CircleRectangleEllipse等,每个派生类都能够用绘制自己的形状。不管派生类的形状如何,我们希望用统一的方式来调用绘制函数,最好是使用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,第二层也是抽象类Shape2DShape3D,在第三层才是可以被实例化为对象的具体类,如二维形状类CircleRectangleEllipse,三维形状类CubeCylinderSphere

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 BasicJava就非常容易。面向对象不会是编程语言的终点。我们现在不知道OO之后的“XO”是什么东西,但至少可以推知,“XO”的核心概念必然高于并包容对象这一概念。正如对象高于并包容了函数和变量一样。

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

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

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值