形状对外公开一个函数来把自己绘制出来。这是合理的,形状就应该能绘制出来,对吧?由于继承的原因,多边形和圆形也有了绘制自己这个函数。
现在我们来讨论在这三个类中的绘制自己的函数都应该怎么实现。在形状中嘛,什么也不做就行了。在多边形中嘛,只要把它所有的顶点首尾相连起来就行了。在圆形中嘛,依据它的圆心和它的半径画一个360度的圆弧就行了。
可是现在的问题是:多边形和圆形的绘制自己的函数是从形状继承而来的,并不能做连接顶点和画圆弧的工作。
怎么办呢?覆盖它,覆盖形状中的绘制自己这个函数。于是我们在多边形和圆形中各做一个绘制自己的函数,覆盖形状中的绘制自己的函数。为了实现覆盖,我们需要把形状中的绘制自己这个函数用virtual修饰。而且形状中的绘制自己这个函数什么也不干,我们就把它做成一个纯虚函数。纯虚函数还有一个作用,就是让它所在的类成为抽象类。形状理应是一个抽象类,不是吗?于是我们很快写出这三个类的代码如下:
class Shape//形状
{
public:
virtualvoid DrawSelf()//绘制自己
{
cout << "我是一个什么也绘不出的图形" << endl;
}
};
class Polygo:public Shape//多边形
{
public:
void DrawSelf() //绘制自己
{
cout << "连接各顶点" << endl;
}
};
class Circ:public Shape//圆
{
public:
void DrawSelf() //绘制自己
{
cout << "以圆心和半径为依据画弧" << endl;
}
};
下面,我们将以上面的这三个类为基础来说明动态多态。在进行更进一步的说明之前,我们先来说一个不得不说的两个概念:“子类型”和“向上转型”。
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++正是利用了指针的这一特性。来做到动态多态的。注2现在让我们来改写OutputShape函数为这样:
void OutputShape( Shape& arg)//专门负责调用形状的绘制自己的函数
{
arg.DrawSelf();
}
现在我们的程序的输出为:
连接各顶点
以圆心和半径为依据画弧
这样的输出才是我们真正的想要的。我们实现的这种真正想要的输出就是动态多态的实质。
7.4.为什么动态多态要用public继承
在我们上面的代码中,圆和多边形都是从形状公有继承而来的。要是我们把圆的继承改为私有或保护会怎么样呢?我们来试一试。哇,我们得到一个编译错误。这个错误的大致意思是说:“请不要用一个私有的方法”。怎么回事呢?
是这么回事。它的意思是说下面这样说不合理。
所有的形状都可以画出来,圆这种形状是不能画出来的。
这样合理吗?不合理。所以请在多态中使用公有继承吧。
8.总结
多态的思想其实早在面向对象的编程出现之前就有了。比如C语言中的+运算符。这个运算符可以对两个int型的变量求和,也可以对两个char的变量求和,也可以对一个int型一个char型的两个变量求和。加法运算的这种特性就是典型的多态。所以说多态的本质是同样的用法在实现上却是不同的。
9.附录:
注1:严格地讲返回值可以不同,但这种不同是有限制的。详细情况请看有关协变的内容。
注2:C++会悄悄地在含有虚函数的类里面加一个指针。用这个指针来指向一个表格。这个表格会包含每一个虚函数的索引。用这个索引来找出相应的虚函数的入口地址。对于我们所举的形状的例子来说,C++会悄悄的做三个表,Shape一个,Polygon一个,Circ一个。它们分别记录一个 DrawSelf函数的入口地址。在程序运行的过程中,C++会先通过类中的那个指针来找到这个表格。再从这个表格中查出DrawSelf的入口地址。然后现通过这个入口地址来调用正直的DrawSelf。正是由于这个查找的过程,是在运行时完成的。所以这样的多态才会被叫做动态多态(运行时多态)
现在我们来讨论在这三个类中的绘制自己的函数都应该怎么实现。在形状中嘛,什么也不做就行了。在多边形中嘛,只要把它所有的顶点首尾相连起来就行了。在圆形中嘛,依据它的圆心和它的半径画一个360度的圆弧就行了。
可是现在的问题是:多边形和圆形的绘制自己的函数是从形状继承而来的,并不能做连接顶点和画圆弧的工作。
怎么办呢?覆盖它,覆盖形状中的绘制自己这个函数。于是我们在多边形和圆形中各做一个绘制自己的函数,覆盖形状中的绘制自己的函数。为了实现覆盖,我们需要把形状中的绘制自己这个函数用virtual修饰。而且形状中的绘制自己这个函数什么也不干,我们就把它做成一个纯虚函数。纯虚函数还有一个作用,就是让它所在的类成为抽象类。形状理应是一个抽象类,不是吗?于是我们很快写出这三个类的代码如下:
class Shape//形状
{
public:
virtualvoid DrawSelf()//绘制自己
{
cout << "我是一个什么也绘不出的图形" << endl;
}
};
class Polygo:public Shape//多边形
{
public:
void DrawSelf() //绘制自己
{
cout << "连接各顶点" << endl;
}
};
class Circ:public Shape//圆
{
public:
void DrawSelf() //绘制自己
{
cout << "以圆心和半径为依据画弧" << endl;
}
};
下面,我们将以上面的这三个类为基础来说明动态多态。在进行更进一步的说明之前,我们先来说一个不得不说的两个概念:“子类型”和“向上转型”。
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++正是利用了指针的这一特性。来做到动态多态的。注2现在让我们来改写OutputShape函数为这样:
void OutputShape( Shape& arg)//专门负责调用形状的绘制自己的函数
{
arg.DrawSelf();
}
现在我们的程序的输出为:
连接各顶点
以圆心和半径为依据画弧
这样的输出才是我们真正的想要的。我们实现的这种真正想要的输出就是动态多态的实质。
7.4.为什么动态多态要用public继承
在我们上面的代码中,圆和多边形都是从形状公有继承而来的。要是我们把圆的继承改为私有或保护会怎么样呢?我们来试一试。哇,我们得到一个编译错误。这个错误的大致意思是说:“请不要用一个私有的方法”。怎么回事呢?
是这么回事。它的意思是说下面这样说不合理。
所有的形状都可以画出来,圆这种形状是不能画出来的。
这样合理吗?不合理。所以请在多态中使用公有继承吧。
8.总结
多态的思想其实早在面向对象的编程出现之前就有了。比如C语言中的+运算符。这个运算符可以对两个int型的变量求和,也可以对两个char的变量求和,也可以对一个int型一个char型的两个变量求和。加法运算的这种特性就是典型的多态。所以说多态的本质是同样的用法在实现上却是不同的。
9.附录:
注1:严格地讲返回值可以不同,但这种不同是有限制的。详细情况请看有关协变的内容。
注2:C++会悄悄地在含有虚函数的类里面加一个指针。用这个指针来指向一个表格。这个表格会包含每一个虚函数的索引。用这个索引来找出相应的虚函数的入口地址。对于我们所举的形状的例子来说,C++会悄悄的做三个表,Shape一个,Polygon一个,Circ一个。它们分别记录一个 DrawSelf函数的入口地址。在程序运行的过程中,C++会先通过类中的那个指针来找到这个表格。再从这个表格中查出DrawSelf的入口地址。然后现通过这个入口地址来调用正直的DrawSelf。正是由于这个查找的过程,是在运行时完成的。所以这样的多态才会被叫做动态多态(运行时多态)