站到巨人面前才知道自己的渺小 ——谈C++中的面向对象

    什么是面向对象或许我们不会过分的关注,也许我们想知道的是面向对象需要什么属性(技术)来支持。用B.Stroustrup先生的话说:一种面向对象的语言或技术一定要直接支持抽象、继承、运行时多态。

一、抽象

    何为抽象?一种语言或一门技术支持抽象就说明它提供了一些类与对象的机制,这也说明了封装性在语言或技术上的体现。何为封装性?封装性的意义何在?

    自然界中人、动植物之所以可以生存主要原因在于人有皮肤,动植物有毛皮,这样使得人与动植物可以与外界“分离”使其内部形成一个小型的生态系统,这样人与动植物在自然界中才能发挥出它们独特的“角色”作用。当然自然界中也有没有边界的事物比如:空气、水,我们必须使用有边界的杯子盛水才能发挥出水的独特作用。

    计算机体系结构发展至今已有许多年,我们沿用的仍然是冯诺依曼体系结构。当年冯诺依曼为追求计算机的执行效率而摒弃了“图灵机”中关于数据与代码的物理隔离,只是在逻辑上将代码与数据进行分离,这也造成了一系列的安全问题(经典的缓冲区溢出问题)。那么如何处理数据与代码相融合这一问题呢?计算机的设计与使用者高瞻远瞩为计算机设计了“电子皮肤”,即在逻辑上对数据与代码进行区分,比如:操作系统的设计者设计了“进程”的概念,用task_struct这一数据结构限定了几乎进程的所有属性,这样使得每个进程都与其他进程分离开来、使得同一优先级的进程是不能随便拿取对方的数据与相关信息的。如果将每一个进程看成是一个事务的话,那么应用封装性的进程为现在操作系统的实时多任务打下了基础。

    所以说,事物的封装性在事物的功能实现上起到了举足轻重的作用。

 

二、继承

    说到继承相信大家都不陌生,“继承”这个字眼有着浓厚的遗传味道。说我们与父母有遗传关系就必定代表着我们继承了父母的某些生理性状,那么父母双方的遗传性状能全部遗传给我们吗?我们为什么不能继承父母所有的生理性状?

    反观计算机中的“继承”,使用支持面向对象的语言进行继承派生时我们需要一个基类,之后的派生类要对基类进行派生,使得基类与派生类之间具有典型的父子关系,这一切看起来似乎很合乎常理。我们看一个例子:

class Father

{

    int iF;

    void fun();

};

class Child : Father

{

    int iC;

    void fun();

};

void main()

{

    cout << sizeof(Father) <<sizeof(Child);

}

    这里的继承是一种“加机制”:子类继承了父类的“全部”属性,这样就导致子类会越派越大、子类的规格越派越臃肿。这与自然界的遗传继承有着本质的区别,自然界的遗传是典型的减数分裂首先将父母双方的基因减半,这也就避免了我们继承了父母的所有生理性状。(估计如果全部继承的话就不能是人了吧)如果你对“加机制”有所怀疑那么你可能想到了C++中所谓的公有继承、私有继承这一类的东西。我们再看一个例子:

class Father

{

public:   

       int iF;

       void fun();

};

 

class Child : private Father

{

       int iC;

       void fun();

};

 

void main()

{

       Child child;

       int i = child.iF;  //不可访问

       cout << sizeof(Father) <<endl<< sizeof(Child) << endl;  //结果不变!

}

    我们暂且放下“加机制”不谈,你可能会说我对基类进行私有继承的话就可以限制对基类成员的访问,这样不就达到了选择性的继承了吗?这看起来也似乎合情合理。但是如果我们想在派生类的子类中访问基类的父类中的成员时,不巧你已经在父类中将其设置成禁止访问了,这样的话就没办法访问了。例如:

class Grandfather

{

public:   

       int iG;

       void fun();

};

 

class Father: private Grandfather

{

public:

       int iF;

       void fun();

};

 

Class Child : public Father

{

public:

       int iC;

       void fun();

};

    此时如果在Child的对象中需要Grandfather中的成员我们怎么办,你也许会说这有何难我们再实例化一个Grandfather的对象进行访问不就行了,这也许能暂时解决问题但也埋下了深深的隐患:child将与grandfather形成了一种“紧耦合”的不良关系。Grandfather出现了问题Child就活不了,我想这都不是我们想看到的。你也许会说我以前摒弃的东西以后还会有用吗?或许你忘了,人类也好、动植物也好自然界在不断的进化当中,谁敢保证我们今天“百无一用”的阑尾在将来不会发挥重要的作用。我想身为数学家出身的B.Stroustrup当年在设计“类权限”的时候也是想描述自然界进化这一普遍的现象(B.Stroustrup没有明确表示,在此实属个人推测),然而深陷自然界中的人类想真正从宏观去理解、去描述自然界我想也是一件不容易的事吧。

    继承是人类不断前进、是自然界不断发展的重要特征,也许计算机将来要实现自动化编程,这就需要有自适应程序,这样的话如何实现计算机程序的“进化”将是我们探讨的重要问题。

                                                  

三、运行时多态

    好好的多态为什么要分家?难道还有其他类型的多态吗?为什么面向对象需要“运行时多态”的支持?

    先回答第二个问题,还有另外一种多态:编译时多态,这种多态确定了“同名操作的具体操作对象的过程”是在编译时完成的,比如:函数或运算符的重载。那么与此对应的“运行时多态” 确定“同名操作的具体操作对象的过程”是在程序运行时完成的,比如:虚函数。那么多态为什么要有“编译”与“运行”之分呢?在计算机历史上“编译”与“运行”这两个字眼由来已久,早期的操作系统是单任务操作系统,程序员在编程的时候发现不同的程序总会用到一些相同的代码(比如说打开文件操作、文件保存、显示字符串等等)这样程序员将这些相同的代码抽取出来在程序使用的时候在编译时链接进去,这样大大减少了程序的开发周期以及出错的概率,这样的链接我们称之为编译时链接;之后出现了实时多任务操作系统,程序员发现同时运行的不同程序可能会使用一些相同的代码,这样程序员将相同的代码抽取出来在程序运行时才根据需要将代码载入程序(进程)的地址空间,这样大大缩减了程序的体积但也降低了执行效率,这样的链接我们称之为运行时链接。那么为什么面向对象要选择“运行时多态”呢?这要从“编译时”与“运行时”的别名说起。

    “编译时”又称“静态”,“运行时”又称“动态”。比如说函数的重载:

void fun(int i)

{

   cout << i <<endl;

}

 

void fun(int i, int j)

{

   cout << i+j <<endl;

}

void main()

{

   int i = 1;

   int j = 2;

 

   fun(i);      //调用一个参数的fun

   fun(i, j);    //调用两个参数的fun

}

    上面的程序在编译阶段就确定了我们该调用哪个函数。在比如说虚函数:

 

class Base

{

public:

   virtual void fun()

   {

          cout <<"In Base class\n";

   }

};

 

class Derived :public Base

{

public:

   void fun()

   {

          cout <<"In Derived class\n";

   }

};

 

void test(Base& b)

{

   b.fun();    //运行时才能确定调用基类还是派生类的fun函数

}

    从语言语法的角度来说我们在函数前面加入virtual关键字就表明此函数是需要动态绑定的(在运行时具体确定我们调用哪个函数);从语言实现的角度说加入virtual关键字就意味着将虚函数的函数地址存到一张表中(虚表),在程序运行时通过函数地址在虚表中索引函数的实现代码以达到动态实现的目的。动态绑定好吗?使用虚函数好吗?B.Stroustrup先生十分推崇虚函数,他不止在他的一篇著作中论述虚函数的好处在此不做过多论述。我想动态绑定达到了一种“用时则取,不用则置”的效果,虽然我们需要一个存储的空间但“轻装”去远行也未必是件坏事。在“运行时”操作固然好但我们为什么要采用多态呢?总的来说多态是实现程序的一种上行机制,例如下面的两段程序:

class Base

{

public:

   virtual void fun()

   {

          cout <<"In Base class\n";

   }

};

 

class Derived :public Base

{

public:

   /*virtual*/ void fun()

   {

          cout <<"In Derived class\n";

   }

};

 

void test(Base& b)

{

   b.fun();

}

 

void main()

{

   Base bc;

   Derived dc;

          

   test(bc);       //打印“In Base class”

   test(dc);       //打印“In Derived”

}

    当我们将Derived类的函数fn的实现去掉时,test(dc)将会打印“In Base class”。

 

class Base

{

public:

   void fn()

   {

          cout <<"In Base class\n";

   }

};

 

class Derived :public Base

{

public:

   void fn()

   {

          cout <<"In Derived \n";

   }

};

 

void test(Base& b)

{

   b.fn();

}

 

void main()

{

   Base bc;

   Derived dc;

   test(dc);       //打印“In Base class”

   test(bc);       //打印“In Base class”

}

     换句话说多态想要达到的效果是:派生类写了就按照你的来,你不写就去找基类,基类没有再去找基类的基类……。这种上行的机制意义重大:计算机发展至今大多数的程序员已经不用从底层去开发一个应用程序,如果写一个“Hello World”还要去写操作显存、显卡驱动等一大堆代码我想就没有几个人想当程序员了,为了应用程序的开发效率就要求操作系统设计者、编译器设计者为程序员事先准备好一系列的公用代码(要以接口的形式给出),尤为重要的是要采用多态或者类似多态的这种具有“上行”机制的技术去编写一些能够保证操作系统正常运行的相关代码,这样既保证了程序员编程时的灵活性又可以保证由于程序员的误操作造成系统的崩溃(用C++编写操作系统是完全有可能的)。

    综合上面的代码我们不难看出:运行时多态与编译时多态相比,在面向对象的体系上与抽象、继承的关系更为密切,抽象、继承与运行时多态的相互支持才构成了我们的面向对象体系的主体。

 

后记:

    不知何时对《动物世界》产生了浓厚的兴趣为自然界多姿多彩的生物所折服。记得小的时候学习自然,至今还记忆犹新的是物种的树状图以及“门、纲、目、科、属、种”这些耳熟能详的词汇。有一次朋友对我说面向对象好难理解啊,怎么也搞不懂。我说也许是你把问题想的过于复杂了,面向对象思想是人类认识自然认识社会的一种思想,你平常用的东西、想事情的方式都有面向对象的影子。面向对象的精髓在于“多态”,我想也许多态恰恰想描述的就是地球上经过千百万年进化而形成的生物的多样性。例如:哺乳动物是我们分析一大类动物将其中的体温恒定、用肺呼吸、胎生等特征“抽象”出来,并将一大类动物中的一部分具有这些特征的动物组合起来称之为哺乳动物。当我们说哺乳动物走路时我们无法确定究竟是哪种哺乳动物,但当我们说两腿交替走路也许我们会想到人;当我们说两腿跳跃走路也许我们会想到袋鼠;当我们说四条腿交替走路也许我们会想到老虎、狮子……。

    也许我们都习惯了复杂,什么理论都坚信背后一定是隐藏着复杂的算法,这种想法在学生中尤为普遍。记得大学毕业那会要写毕业论文,当我查看历届毕业生的论文时候惊讶的发现一个特点:大家都喜欢使用复杂的、长长的公式去计算,并且随着论文“质量”的提高,公式越来越长、越来越复杂。硕士生的论文公式比本科生的长,博士生的论文公式比硕士生的论文长,开3/5根号不算各种积分求导(怀疑在公式编辑时会不会出错)……。当然我不否认复杂事物的价值与意义,14世纪著名的逻辑学家奥卡姆曾提出了著名的“奥卡姆剃刀原理”他认为对于复杂的事物我们应该抓住其主要矛盾剔除掉次要矛盾,这样我们才能抓住事物最本质的东西。我想简单的思维往往能揭示事物最本质的东西,记得学习物理的时候著名的牛顿力学第二定律F = ma,自由落体定律:h = gt^2/2,这些在经典力学中揭示物体运动规律的公式都是简单的、和谐的。再比如20世纪著名科学家、“世纪伟人”爱因斯坦先生的质能联系方程:E = c^2m,将c^2看成常量我想这个函数简单的不会有人看不懂(初中函数第一节课:正比例函数)但它却能揭示宇宙万物的质量与能量之间的关系。在MFC中最不起眼也最简单的类CPoint类,其中只有x、y两个成员变量,但这一如此简单的类却揭示了MFC作为一个“可视化”类库的本质特征(像素点)。

     最后,解释一下本文的题目:上学那会就习惯去查一些资料去答疑解惑,查的多了就发现国内的计算机著作抄袭之风甚重,值得一读的很少,有幸碰到中国人翻译的外文文献翻译的又是“惨不忍睹”,我觉得我们应该多读一些外国原著、大师写得书(最好是外文原版,比如学习C++,B.Stroustrup的著作是不能不读的)。与大师对话才知道自己的无知,站到巨人面前才知道自己的渺小,以此共勉祝愿大家在C++的道路上越走越远!

 

参考文献:

《Why C++ is not just anObject-Oriented Programming Language》Bjarne Stroustrup

《The C++ ProgrammingLanguage》Bjarne Stroustrup

《The Art of Linux KernelDesign》新设计团队

《加密与解密》(第三版)段钢

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值