多态(C++版)

前些日子面了个试,结果深受刺激,总结学习一下吧,从“多态”开始。此文算是对多态的一个简单说明,仍系转载  (真够懒的

--------------------------------------------------------------------------------

1.    什么是多态 
2.    多态带来的好处 
3.    C++中实现多态的方式 
4.    细说用函数重载实现的多态 
5.    细说用模板函数实现的多态 
6.    小结 
7.    细说用虚函数实现的多态 
7.1.     虚函数是怎么回事 
7.2.     向上转型 
7.3.     为什么要用指针或引用来实现动态多态 
7.4.     为什么动态多态要用public继承 
8.    总结 
9.    附录: 
  
1.   什么是多态 
多态是C++中的一个重要的基础,可以这样说,不掌握多态就是C++的门外汉。然而长期以来,C++社群对于多态的内涵和外延一直争论不休。大有只见树木不见森林之势。多态到底是怎么回事呢?说实在的,我觉的多态这个名字起的不怎么好(或是译的不怎么好)。要是我给起名的话,我就给它定一个这样的名字--“调用’同名函数’却会因上下文不同会有不同的实现的一种机制”。这个名字长是长了点儿,可是比“多态”清楚多了。看这个长的定义,我们可以从中找出多态的三个重要的部分。一是“相同函数名”,二是“依据上下文”,三是“实现却不同”。嘿,还是个顺口溜呢。我们且把它们叫做多态三要素吧。 
2.   多态带来的好处 
多态带来两个明显的好处:一是不用记大量的函数名了,二是它会依据调用时的上下文来确定实现。确定实现的过程由C++本身完成另外还有一个不明显但却很重要的好处是:带来了面向对象的编程。 
3.   C++中实现多态的方式 
C++中共有三种实现多态的方式。由“容易说明白”到“不容易说明白”排序分别为。第一种是函数重载;第二种是模板函数;第三种是虚函数。 
4.   细说用函数重载实现的多态 
函数重载是这样一种机制:允许有不同参数的函数有相同的名字。 
具体一点讲就是:假如有如下三个函数: 
void test(int arg){}         //函数1 
void test(char arg){}         //函数2 
void test(int arg1,int arg2){}    //函数3 
如果在C中编译,将会得到一个名字冲突的错误而不能编译通过。在C++中这样做是合法的。可是当我们调用test的时候到底是会调用上面三个函数中的哪一个呢?这要依据你在调用时给的出的参数来决定。如下: 
    test(5);       //调用函数1 
    test(‘c‘);//调用函数2 
    test(4,5); //调用函数3 
C++是如何做到这一点的呢?原来聪明的C++编译器在编译的时候悄悄的在我们的函数名上根据函数的参数的不同做了一些不同的记号。具体说如下: 
void test(int arg)            //被标记为 ‘test有一个int型参数’ 
void test(char arg)           //被标记为 ‘test有一个char型的参数’ 
void test(int arg1,int arg2) //被标记为 ‘test第一个参数是int型,第二个参数为int型’ 
这样一来当我们进行对test的调用时,C++就可以根据调用时的参数来确定到底该用哪一个test函数了。噢,聪明的C++编译器。其实C++做标记做的比我上面所做的更聪明。我上面哪样的标记太长了。C++编译器用的标记要比我的短小的多。看看这个真正的C++的对这三个函数的标记: 
[email protected]@[email protected] 
[email protected]@[email protected] 
[email protected]@[email protected] 
是不是短多了。但却不好看明白了。好在这是给计算机看的,人看不大明白是可以理解的。 
还记得cout吧。我们用
    cout //输出int型 
    cout //输出double型 
    cout ‘a‘;   //输出char型 
    cout "abc";//输出char数组型 
    cout //输出一个函数 
cout之所以能够用一个函数名
    cout int//输出int型 
    cout double//输出double型 
    cout char‘a‘;            //输出char型 
    cout charArray"abc";     //输出char数组型 
    cout function(…)//输出函数 
为每一种要输出的类型起一个函数名,这岂不是很麻烦呀。 
不过函数重载有一个美中不足之处就是不能为返回值不同的函数进行重载。那是因为人们常常不为函数调用指出返回值。并不是技术上不能通过返回值来进行重载。 
5.   细说用模板函数实现的多态 
所谓模板函数(也有人叫函数模板)是这样一个概念:函数的内容有了,但函数的参数类型却是待定的(注意:参数个数不是待定的)。比如说一个(准确的说是一类或一群)函数带有两个参数,它的功能是返回其中的大值。这样的函数用模板函数来实现是适合不过的了。如下。 
template typename T> 
T getMax(T arg1, T arg2) 

    return arg1 > arg2 ? arg1:arg2; //代码段1 

这就是基于模板的多态吗?不是。因为现在我们不论是调用getMax(1, 2)还是调用getMax(3.0, 5.0)都是走的上面的函数定义。它没有根据调用时的上下文不同而执行不同的实现。所以这充其量也就是用了一个模板函数,和多态不沾边。怎样才能和多态沾上边呢?用模板特化呀!象这样: 
template 
char* getMax(char* arg1, char* arg2) 

    return (strcmp(arg1, arg2) > 0)?arg1:arg2;//代码段2 

这样一来当我们调用getMax(“abc”, “efg”)的时候,就会执行代码段2,而不是代码段1。这样就是多态了。 
更有意思的是如果我们再写这样一个函数: 
char getMax(char arg1, char arg2) 

    return arg1>arg2?arg1:arg2; //代码段3 
}

当我们调用getMax(‘a’, ‘b’)的时候,执行的会是代码段3,而不是代码段1或代码段2。C++允许对模板函数进行函数重载,就象这个模板函数是一个普通的函数一样。于是我们马上能想到写下面这样一个函数来做三个数中取大值的处理: 
int getMax( int arg1, int arg2, int arg3) 

    return getMax(arg1, max(arg2, arg3) ); //代码段4 

同样我们还可以这样写: 
template typename T> 
T getMax(T arg1, T arg2, T arg3) 

    return getMax(arg1, getMax(arg2, arg3) ); //代码段5 

现在看到结合了模板的多态的威力了吧。比只用函数重载厉害多了。 
6.   小结 
上面的两种多态在C++中有一个总称:静态多态。之所以叫它们静态多态是因为它们的多态是在编译期间就确定了。也就是说前面所说的函数1,2,3代码段1,2,3,4,5这些,在编译完成后,应该在什么样的上下文的调用中执行哪一些就确定了。比如:如果调用getMax(0.1, 0.2, 0.3)就会执行代码段5。如果调用test(5)就执行函数1。这些是在编译期间就能确定下来的。 
静态多态还有一个特点,就是:“总和参数较劲儿”。 
下面所要讲的一种多态就是必需是在程序的执行过程中才能确定要真正执行的函数。所以这种多态在C++中也被叫做动态多态。 
7.   细说用虚函数实现的多态 
7.1.虚函数是怎么回事 
首先来说一说虚函数,所谓虚函数是这样一个概念:基类中有这么一些函数,这些函数允许在派生类中其实现可以和基类的不一样。在C++中用关键字virtual来表示一个函数是虚函数。 
C++中还有一个术语 “覆盖”与虚函数关系密切。所谓覆盖就是说,派生类中的一个函数的声明,与基类中某一个函数的声明一模一样,包括返回值,函数名,参数个数,参数类型,参数次序都不能有差异。说覆盖和虚函数关系密切的原因有两个:一个原因是,只有覆盖基类的虚函数才是安全的。第二个原因是,要想实现基于虚函数的多态就必须在派生类中覆盖基类的虚函数。 
接下来让我们说一说为什么要有虚函数,分析一下为什么派生类非要在某些情况下覆盖基类的虚函数。就以那个非常著名的图形绘制的例子来说吧。假设我们在为一个图形系统编程。我们可能有如下的一个类结构。 
 
图7-1 
形状对外公开一个函数来把自己绘制出来。这是合理的,形状就应该能绘制出来,对吧?由于继承的原因,多边形和圆形也有了绘制自己这个函数。 
现在我们来讨论在这三个类中的绘制自己的函数都应该怎么实现。在形状中嘛,什么也不做就行了。在多边形中嘛,只要把它所有的顶点首尾相连起来就行了。在圆形中嘛,依据它的圆心和它的半径画一个360度的圆弧就行了。 
可是现在的问题是:多边形和圆形的绘制自己的函数是从形状继承而来的,并不能做连接顶点和画圆弧的工作。 
怎么办呢?覆盖它,覆盖形状中的绘制自己这个函数。于是我们在多边形和圆形中各做一个绘制自己的函数,覆盖形状中的绘制自己的函数。为了实现覆盖,我们需要把形状中的绘制自己这个函数用virtual修饰。而且形状中的绘制自己这个函数什么也不干,我们就把它做成一个纯虚函数。纯虚函数还有一个作用,就是让它所在的类成为抽象类。形状理应是一个抽象类,不是吗?于是我们很快写出这三个类的代码如下: 
class Shape//形状 

public: 
    virtualvoid DrawSelf()//绘制自己 
    { 
       cout "我是一个什么也绘不出的图形"  
    } 
}; 
  
class Polygo:public Shape//多边形 

public: 
    void DrawSelf()   //绘制自己 
    { 
       cout "连接各顶点"  
    } 
}; 
  
class Circ:public Shape//圆 

public: 
    void DrawSelf()   //绘制自己 
    { 
       cout "以圆心和半径为依据画弧"  
    } 
}; 
下面,我们将以上面的这三个类为基础来说明动态多态。在进行更进一步的说明之前,我们先来说一个不得不说的两个概念:“子类型”和“向上转型”。 
7.2.向上转型 
子类型很好理解,比如上面的多边形和圆形就是形状的子类型。关于子类型还有一个确切的定义为:如果类型X扩充或实现了类型Y,那么就说X是Y的子类型。 
向上转型的意思是说把一个子类型转的对象换为父类型的对象。就好比把一个多边形转为一个形状。向上转型的意思就这么简单,但它的意义却很深远。向上转型中有三点需要我们特别注意。第一,向上转型是安全的。第二,向上转型可以自动完成。第三,向上转型的过程中会丢失子类型信息。这三点在整个动态多态中发挥着重要的作用。 
假如我们有如下的一个函数: 
void OutputShape( Shape arg)//专门负责调用形状的绘制自己的函数 

    arg.DrawSelf(); 

那么现在我们可以这样使用OutputShape这个函数: 
    Polygon shape1; 
    Circ shape2; 
    OutputShape(shape1); 
    OutputShape(shape2); 
我们之所以可以这样使用OutputShape函数,正是由于向上转型是安全的(不会有任何的编译警告),是由于向上转弄是自动的(我们没有自己把shape1和shape2转为Shape类型再传给OutputShape函数)。可是上面这段程序运行后的输出结果是这样的: 
我是一个什么也绘不出的图形 
我是一个什么也绘不出的图形 
明明是一个多边形和一个圆呀,应该是输出这下面这个样子才合理呀! 
连接各顶点 
以圆心和半径为依据画弧 
造成前面的不合理的输出的罪魁祸首正是‘向上转型中的子类型信息丢失’。为了得到一个合理的输出,得想个办法来找回那些丢失的子类型信息。C++中用一种比较巧妙的办法来找回那些丢失的子类型信息。这个办法就是采用指针或引用。 
7.3.为什么要用指针或引用来实现动态多态 
对于一个对象来说无论有多少个指针指向它,这些个指针所指的都是同一个对象。(即使你用一个void的指针指向一个对象也是这样的,不是吗?)同理对于引用也一样。 
这究竟有多少深层次的意义呢?这里的深层的意义是这样的:子类型的信息本来就在它本身中存在,所以我们用一个基类的指针来指出它,这个子类型的信息也会被找到,同理引用也是一样的。C++正是利用了指针的这一特性。来做到动态多态的。现在让我们来改写OutputShape函数为这样: 
void OutputShape( Shape& arg)//专门负责调用形状的绘制自己的函数 

    arg.DrawSelf(); 

现在我们的程序的输出为: 
连接各顶点 
以圆心和半径为依据画弧 
这样的输出才是我们真正的想要的。我们实现的这种真正想要的输出就是动态多态的实质。 
7.4.为什么动态多态要用public继承 
在我们上面的代码中,圆和多边形都是从形状公有继承而来的。要是我们把圆的继承改为私有或保护会怎么样呢?我们来试一试。哇,我们得到一个编译错误。这个错误的大致意思是说:“请不要用一个私有的方法”。怎么回事呢? 
是这么回事。它的意思是说下面这样说不合理。 
所有的形状都可以画出来,圆这种形状是不能画出来的。 
这样合理吗?不合理。所以请在多态中使用公有继承吧。 
8.   总结 
多态的思想其实早在面向对象的编程出现之前就有了。比如C语言中的+运算符。这个运算符可以对两个int型的变量求和,也可以对两个char的变量求和,也可以对一个int型一个char型的两个变量求和。加法运算的这种特性就是典型的多态。所以说多态的本质是同样的用法在实现上却是不同的。 
9.   附录: 
C++会悄悄地在含有虚函数的类里面加一个指针。用这个指针来指向一个表格。这个表格会包含每一个虚函数的索引。用这个索引来找出相应的虚函数的入口地址。对于我们所举的形状的例子来说,C++会悄悄的做三个表,Shape一个,Polygon一个,Circ一个。它们分别记录一个DrawSelf函数的入口地址。在程序运行的过程中,C++会先通过类中的那个指针来找到这个表格。再从这个表格中查出DrawSelf的入口地址。然后现通过这个入口地址来调用正直的DrawSelf。正是由于这个查找的过程,是在运行时完成的。所以这样的多态才会被叫做动态多态(运行时多态)

阅读(298) | 评论(2) | 转发(0) |

0

上一篇:关于 重入

下一篇:实现WIN CE下截屏并且保存到文件

相关热门文章

  • test123
  • 编写安全代码——小心有符号数...
  • 使用openssl api进行加密解密...
  • 一段自己打印自己的c程序...
  • sql relay的c++接口

热门推荐

    -->

    给主人留下些什么吧!~~

    chinaunix网友2011-01-03 17:54:10

    很好的, 收藏了
    推荐一个博客,提供很多免费软件编程电子书下载:
    http://free-ebooks.appspot.com

    回复 | 举报

    chinaunix网友2011-01-03 17:53:58

    很好的, 收藏了
    推荐一个博客,提供很多免费软件编程电子书下载:
    http://free-ebooks.appspot.com

    回复 | 举报

    评论热议

    多态(C++版)

    时间: 2024-10-14 00:10:03

    多态(C++版)的相关文章

    超级简单版多态计算器

    个人笔记学习黑马四期 一.编写一个操作父类 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 多态计算器 { /// <summary> /// 计算父类 /// </summary> class Operation { public int NumA { get; set; } p

    C++编程实践: 继承与多态

    本实例及代码来自<C++ Primer Plus>(第六版) 第十三章 题目要求: 假如你是某银行首席程序员.银行要求你开发两个类,一个用于表示基本支票账户--Brass Account,另一个用于表示代表Brass Plus支票账户,它添加了透支保护的特性.也就是说,如果持有此卡的用户签出了一张超出其存款余额的支票--但是超出的数额并不是很大,银行将支付这张支票,对超出的部分收取额外的费用,并追加罚款. 下面是用于Brass Account支票账户的信息: 客户姓名 帐号 当前结余 下面是可

    阅读《代码大全》(第二版)体会小结

    这一次阅读了著名的<代码大全>(第二版).全书虽然章节众多,但是主要就是几个大部分,这些部分我大多有一些浅显的了解但还未深入,当然也有一些全新的体验,例如表驱动法.全书内容丰富而详细,我在阅读的其中问题并不是太多,只不过很多的内容都觉得了解的还太浅,需要更多的实践以及阅读去体会.在这里记录下的也就是一些自己的体会,主要是对书中一些论断的不同看法与讨论,大部分是关于面向对象和结构化设计的内容:以及对于全新接触的表驱动法的学习体会. Question 1: “7.1 创建子程序的正当理由”中,提到

    [转]Java中继承、多态、重载和重写介绍

    什么是多态?它的实现机制是什么呢?重载和重写的区别在那里?这就是这一次我们要回顾的四个十分重要的概念:继承.多态.重载和重写. 继承(inheritance) 简单的说,继承就是在一个现有类型的基础上,通过增加新的方法或者重定义已有方法(下面会讲到,这种方式叫重写)的方式,产生一个新的类型.继承是面向对象的三个基本特征--封装.继承.多态的其中之一,我们在使用JAVA时编写的每一个类都是在继承,因为在JAVA语言中,java.lang.Object类是所有类最根本的基类(或者叫父类.超类),如果

    C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数、抽象类、虚析构函数、动态创建对象

    C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数.抽象类.虚析构函数.动态创建对象 一.纯虚函数 1.虚函数是实现多态性的前提 需要在基类中定义共同的接口 接口要定义为虚函数 2.如果基类的接口没办法实现怎么办? 如形状类Shape 解决方法 将这些接口定义为纯虚函数 3.在基类中不能给出有意义的虚函数定义,这时可以把它声明成纯虚函数,把它的定义留给派生类来做 4.定义纯虚函数: class <类名> { virtual <类型> <函

    Java面试宝典2013版(超长版)

    一. Java基础部分......................................................................................................2 1.一个".java"源文件里能否够包含多个类(不是内部类)?有什么限制?.....2 2.Java有没有goto?........................................................................

    继承,多态..

    “是一个”与“有一个”继承关系 ,和拥有关系一定要搞清楚啊,兄弟.IS A用测试继承关系 .这个是单向的. 例1.狗是动物,反过来不能成立,“动物是狗”不能成立,这就是单向.例2.狗是狗,反过来还是“狗是狗”.这就是双向,这样就错了. 重载版的方法只是刚好有相同名字的不同方法 . 抽象类代表没有人能够创建出该类的实例.你还是可以使用抽象类来声明为引用类型给多态使用. 抽象类除了被继承过之外,是没有用途,没有值,没有目的 如果你声明出一个抽象的方法,就必须将类也标记为抽象的.你不能在非抽象类中拥有

    (1) 深入理解Java面向对象三大特性 封装 继承 多态

    转眼已经工作快6年了,最开始做了2年J2EE:然后整了2年的数据仓库,主要是Cognos的报表开发:现在从事4G LTE核心网的开发,用的语言任然是Java,但写代码的机会不多,基本都是看代码找BUG,偶尔做点new feature也是在比较成熟的框架上复制.粘贴.修改,大部分时间还是在理解业务,钱多.事少.离家近,当时来这家公司图的是后面2点,2年过去了,英文水平有所提升,对敏捷开放也有一定的了解,但技术方面明显退步了或者说没有进步吧,本来以前也不怎么好,因为工作上用不到,自己也没怎么学习,所

    可回收重复使用的链表,类似于Android消息链(并记录多态使用)

    在尾部生产,头部消耗的链表,数据增加可重复使用的功能. 新数据类型继承Nod,实现newNod方法即可.使用时没有模板那么方便,需要强转. 感觉newNod和Windows好多结构体有个表示结构体大小的成员主要告诉new多大,使用时强转和CPtrList差不多.感觉这样设计也没啥不妥. 回顾C++ // Demo.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<stdio.h> #include <tchar.