OOP(2)类和对象,继承和多态
2016-06-26 10:28
399 查看
本篇主要对面向对象中几个重要的概念进行讲解,它们是:
类
对象(类的实例)
继承(基类和子类)
多态(动态绑定的实现)
参考书目 :《C++ Primer》《Java 编程思想》
本篇所有类中的标签都默认为public,因此不用考虑private带来的影响
类
现实中的类我们在平时的生活中对类这个词并不陌生,“一类人”,“哺乳类”。类最简单的理解是:一堆有共性的东西的统称,比如语文书、数学书都可以叫做教材,大众、奥迪都可以叫车。类最大的特点就是抽象,比如我对你说车,你能马上想到,这是有轮子有方向盘有发动机的物体,但再具体的,你不知道他是什么颜色,什么品牌;这就是类的抽象。
程序中的类
可能通过我的描述,你能够理解现实中类是什么东西,但是结合一种编程语言,类到底是什么?有编程基础的人肯定能看懂int a=0;这行代码,你也知道int是一种类型。其实你可以把程序中的类理解为一种自定义的,复杂的类型,类似于int double float。
我这里定义一个类
class car{ string name; //车的名字 int price; //车的价格 string color; //车的颜色 bool run( ) const; //让车发动的函数 };
对象
现实中的对象刚刚说了现实中的类,比如车,对象和类的关系式,对象是类的一个实例,更通俗一点,是一个真东西,,比如我跟你说动物,你很难想象一个具体化的东西,但是我跟你说猴子,你马上就知道他是什么了,甚至你可以画出来。这里猴子就是动物这个类的对象。
程序中的对象
再次拿出刚刚的那句简单代码
int a=0;
如果你把int当作一个类来理解,a就是他的对象,也就是由类来定义出来的具体东西,你可以给他赋值,对他做运算,但是你不能对int这个类型做运算。
同样的,我们回看刚才的那个car类
class car{ string name; //车的名字 int price; //车的价格 string color; //车的颜色 bool run( ) const; //让车发动的函数 };
在这里,我用car类定义一个具体的对象
car audi;
此时,audi就是由car定义的一个对象了,是不是跟 int a很像,没错,这么理解起来就很简单了。那为什么说类比较复杂呢,在赋值阶段,int a之后,你对a的操作限制在给他赋以整形的一个数字,a=0;但此时car 定义的audi,可赋值的东西更丰富。
audi.name="audi"; audi.price=500000; audi.color ="blue";
不仅可以赋值,还可以让车开动起来
audi.run();
好的,到这里类和对象就介绍完了,有人会问为什么类这个东西很重要,你需要理解“数据抽象和封装”这个概念,这也是类背后蕴含的最基本的思想,关于这一部分就比较晦涩,大家可以参考我上述的参考书目,这里我简单解释一句,类可以将数据和功能封装到一起,你可以调用已有的类,而不用关心怎么实现。不懂什么是抽象和封装暂时不会影响你对类和对象的理解,我们直接进入继承。
继承
继承这个东西是怎么出来的呢?设想这样一种情况,定义了一个类以后,又定义了一个新类,写了代码之后,发现这两个类很多功能和数据可以抽象到一起,比如车类和跑车类,你会发现诸如启动,加速等一些功能是通用的,这时候你觉得再完全重写一个太麻烦了,因为可能只有发动机这一项不一样。继承,解决了这一问题,当有了一个父类之后,你可以写一个用来继承父类的子类,子类不但拥有父类的所有功能,还可以加入自己特别的功能,这样就很方便了,很多函数不用重复写了,棒棒的。如果你写过比较复杂的程序,你会发现继承是好东西。
以C++为例,先看一段代码
class father{ //基类 father void A(); void B(); }; class son:public father{ //子类 son,注意冒号和它后面的继承关系 void C(); };
通过上边对继承的介绍,我们知道son这个类可以使用father中A、B两个函数,那么当在一段真实的代码里,son的对象就可以使用ABC三个函数,father的对象可以使用AB两个函数。
多态(面向对象编程的关键思想)
多态这个概念太重要了,多态可以说是基于虚函数来实现的。这里我会结合C++来进行解释。结尾会比较C++、 Java 和C#三种面向对象语言在使用多态特性时的不同。继续看刚才那个例子
class father{ void A(); void B(); virtual void C(); //注意C函数前边的virtual关键字 }; class son:public father{ void C(); };
可以看到基类里多了一个 C++关键字 virtual,加了这个关键字以后,基类里的C函数在子类中必须被重新定义。为什么要这么做呢,这里我通过一个程序的例子来做一下解释:
现在我需要实现一款大富翁游戏,游戏中有甲乙丙丁4个玩家,通过骰子确定那个玩家行走。
如果是面向过程的语言,此时我们的编程思路应该是
void run(int &number){ //这里定义一个走的函数 number++; } void main(){ int jia,yi,bing,ding=0; //定义各个玩家步数 string name; get(name); //得到骰子的结果,get不一定是什么函数 //进行判断,执行函数 if(甲==name) run(jia); if(乙==name) run(yi); if(丙==name) run(bing); if(丁==name) run(ding); }
run函数的功能是,传入jia yi bing ding四个变量,进行增长,但是在程序运行之前,程序并不知道骰子会出现什么情况,所以需要加入判断,并对判断结果做出相应的选择。
下面应用面向对象编程完成这个程序。
class man{ virtual void run(int &number); }; class jia: public man{ void run();//这里有一点很关键,继承于父类的虚函数run在子类中必须重新定义。后边会解释为什么 }; class yi: public man{ void run(); }; class bing: public man{ void run(); }; class ding: public man{ void run(); }; void main(){ man *P=new man; //P是man类的指针对象 jia j; yi y; bing b; ding d; get name;//name是 j y b d的一种,get不一定是什么函数 P=&name; p->run(); }
看到这里有的人开始有点懵逼了,貌似面向对象写的比面向过程还要复杂啊,与其这么复杂还不如写判断然后依次执行呢。
NO NO NO,刚刚这个例子其实简化了问题,因为例子中run()函数在子类和父类中是一样的,实际问题如果真都是这么简单,我们就不需要虚函数了,直接继承就得了。
但其实在一些复杂的应用中,如windows自带的画图工具,虽然同样是“画”这样一个函数,但其实在画圆和画正方形的时候的实现是不一样的。回到上面的例子,游戏复杂一下,甲乙丙丁四个人的走路方式不相同,也就是说run()函数是不一样的,骰子的结果是甲的时候,你需要按照甲的方式来行走。此时如果使用面向过程,你需要这么做:
void Jrun(int &number){ //这里定义一个甲走的函数 number++;//函数内容有变化,具体实现不写,下同 } void Yrun(int &number){ //这里定义一个乙走的函数 number++; } void Brun(int &number){ //这里定义一个丙走的函数 number++; } void Drun(int &number){ //这里定义一个丁走的函数 number++; } void main(){ string name; get(name); //得到骰子的结果,get不一定是什么函数 //进行判断,执行函数 if(甲==name) Jrun(jia); if(乙==name) Yrun(yi); if(丙==name) Brun(bing); if(丁==name) Drun(ding); }
除了run()函数变成了4种不同的函数之外,判断的过程也进行了相应的更改。
看一下面向对象是怎么处理这个过程的
class man{ virtual void run(int &number); }; class jia: public man{ void run();//这里有一点很关键,继承于父类的虚函数run在子类中必须重新定义。后边会解释为什么 }; class yi: public man{ void run(); }; class bing: public man{ void run(); }; class ding: public man{ void run(); }; void main(){ man *P=new man; //P是man的指针对象 jia j; yi y; bing b; ding d; get name;//name是 j y b d的一种,get不一定是什么函数 P=&name; p->run(); }
没错,不考虑run()的具体实现方法,面向过程编程的主函数并没有发生改变。子类中对run()函数的重定义,就是为了满足不同子类下对于run()的不同行为,但你可以清晰地看到,它们都叫做run()。
这里给出多态对于复杂程序的好处:
1.在复杂的问题中,程序员应用属于一个体系的类家族时,只需要合理使用run()这一个函数就行了,多个相似的函数名字不同,会给人混淆,而这种多种相似的函数多态到一个基类函数的形式,会让代码的设计和阅读更为简单。
2.上述游戏中的4种判断看起来并不复杂,但如果涉及到成百上千的判断,可能程序的设计者和使用者都会很头痛。
man *P=new man; //P是man的指针对象 jia j; yi y; bing b; ding d; get name; P=&name;//name不同 p->run(); //run函数会被动态绑定。
在上述面向对象的代码中可以看到,设计程序时不需要考虑到到底哪个子类会被用到,只需要定义一个父类的指针,将指针指向某个子类就可以使用子类的run()函数,很方便有木有。
《C++ Primer》称上述这个过程为动态绑定,下面是书中的一段原话,我觉得写的很清楚。类似的话也出现在了《程序员面试宝典中》
在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
同时,在书中还出现了另一句话。
将基类类型的引用或指针绑定到派生类对象对基对象没有影响,对象本身不会改变,仍为派生类对象。对象的实际类型可能不同于该对象引用或指针的静态类型,这是C++中动态绑定的关键。
上述这句话也是代码
P=&name; //P指向的不是基类。
能实现的关键。
同样,在Java和C#中的多态思维实现也大同小异。C++中,类中的函数默认是非虚的,必须使用virtual关键字来定义哪些函数是可以被动态绑定的。
C#中, 函数默认的情况也是非虚的,但在重定义函数是,需要用override显式声明。
Java中,所有的函数都是虚的。
如有问题可以留言或访问个人网站PW
或发送邮件到:lifeliyan@163.com
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Python动态类型的学习---引用的理解
- 关于指针的一些事情
- c++ primer 第五版 笔记前言
- 如何创建对象以及jQuery中创建对象的方式
- share_ptr的几个注意点
- 土人系列AS入门教程 -- 对象篇
- PostgreSQL教程(三):表的继承和分区表详解
- C#托管堆对象实例包含内容分析
- C#实现获取不同对象中名称相同属性的方法
- Lua中调用C++函数示例
- Lua面向对象之类和继承浅析
- Lua教程(一):在C++中嵌入Lua脚本
- 浅析Ruby中继承和消息的相关知识
- Lua教程(二):C++和Lua相互传递数据示例
- javascript asp教程第十一课--Application 对象
- PowerShell中使用Out-String命令把对象转换成字符串输出的例子
- VBS教程:对象-正则表达式(RegExp)对象
- 设计引导--一个鸭子游戏引发的设计理念(多态,继承,抽象,接口,策略者模式)
- C++联合体转换成C#结构的实现方法