您的位置:首页 > 运维架构

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

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  oop 对象 继承 C++