多态

8 篇文章 0 订阅

 C++多态性

 

C++编程语言是一款应用广泛,支持多种程序设计的计算机编程语言。我们今天就会为大家详细介绍其中C++多态性的一些基本知识,以方便大家在学习过程中对此能够有一个充分的掌握。
  多态性可以简单地概括为一个接口,多种方法,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。多态(polymorphism),字面意思多种形状。
  C++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。(这里我觉得要补充,重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性)而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。但这并没有体现多态性。
  多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
  那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

  最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。

笔试题目:

[cpp] view plain copy

1. #include<iostream>  

2. using namespace std;  

3.   

4. class A  

5. {  

6. public:  

7.     void foo()  

8.     {  

9.         printf("1\n");  

10.     }  

11.     virtual void fun()  

12.     {  

13.         printf("2\n");  

14.     }  

15. };  

16. class B : public A  

17. {  

18. public:  

19.     void foo()  

20.     {  

21.         printf("3\n");  

22.     }  

23.     void fun()  

24.     {  

25.         printf("4\n");  

26.     }  

27. };  

28. int main(void)  

29. {  

30.     A a;  

31.     B b;  

32.     A *p = &a;  

33.     p->foo();  

34.     p->fun();  

35.     p = &b;  

36.     p->foo();  

37.     p->fun();  

38.     return 0;  

39. }  

      第一个p->foo()p->fuu()都很好理解,本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数,因此输出结果就是12
    第二个输出结果就是14p->foo()p->fuu()则是基类指针指向子类对象,正式体现多态的用法,p->foo()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的foo()函数的代码了,因此输出的结果还是1。而p->fun()指针是基类指针,指向的fun是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fun()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fun()函数的地址,因此输出的结果也会是子类的结果4
  笔试的题目中还有一个另类测试方法。即
       B *ptr = (B *)&a;  ptr->foo();  ptr->fun();
  问这两调用的输出结果。这是一个用子类的指针去指向一个强制转换为子类地址的基类对象。结果,这两句调用的输出结果是32
  并不是很理解这种用法,从原理上来解释,由于B是子类指针,虽然被赋予了基类对象地址,但是ptr->foo()在调用的时候,由于地址偏移量固定,偏移量是子类对象的偏移量,于是即使在指向了一个基类对象的情况下,还是调用到了子类的函数,虽然可能从始到终都没有子类对象的实例化出现。
  而ptr->fun()的调用,可能还是因为C++多态性的原因,由于指向的是一个基类对象,通过虚函数列表的引用,找到了基类中fun()函数的地址,因此调用了基类的函数。由此可见多态性的强大,可以适应各种变化,不论指针是基类的还是子类的,都能找到正确的实现方法。

[cpp] view plain copy

1. //小结:1、有virtual才可能发生多态现象  

2. // 2、不发生多态(无virtual)调用就按原类型调用  

3. #include<iostream>  

4. using namespace std;  

5.   

6. class Base  

7. {  

8. public:  

9.     virtual void f(float x)  

10.     {  

11.         cout<<"Base::f(float)"<< x <<endl;  

12.     }  

13.     void g(float x)  

14.     {  

15.         cout<<"Base::g(float)"<< x <<endl;  

16.     }  

17.     void h(float x)  

18.     {  

19.         cout<<"Base::h(float)"<< x <<endl;  

20.     }  

21. };  

22. class Derived : public Base  

23. {  

24. public:  

25.     virtual void f(float x)  

26.     {  

27.         cout<<"Derived::f(float)"<< x <<endl;   //多态、覆盖  

28.     }  

29.     void g(int x)  

30.     {  

31.         cout<<"Derived::g(int)"<< x <<endl;     //隐藏  

32.     }  

33.     void h(float x)  

34.     {  

35.         cout<<"Derived::h(float)"<< x <<endl;   //隐藏  

36.     }  

37. };  

38. int main(void)  

39. {  

40.     Derived d;  

41.     Base *pb = &d;  

42.     Derived *pd = &d;  

43.     // Good : behavior depends solely on type of the object  

44.     pb->f(3.14f);   // Derived::f(float) 3.14  

45.     pd->f(3.14f);   // Derived::f(float) 3.14  

46.   

47.     // Bad : behavior depends on type of the pointer  

48.     pb->g(3.14f);   // Base::g(float)  3.14  

49.     pd->g(3.14f);   // Derived::g(int) 3   

50.   

51.     // Bad : behavior depends on type of the pointer  

52.     pb->h(3.14f);   // Base::h(float) 3.14  

53.     pd->h(3.14f);   // Derived::h(float) 3.14  

54.     return 0;  

55. }  

令人迷惑的隐藏规则
本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。
这里隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual
关键字,基类的函数将被隐藏(注意别与重载混淆)。
2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual
关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
上面的程序中:
1)函数Derived::f(float)覆盖了Base::f(float)
2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。
3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。

C++纯虚函数
 一、定义
  纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0” 
  virtual void funtion()=0 
二、引入原因
   1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。 
   2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
三、相似概念
   1、多态性 
  指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。 
  a、编译时多态性:通过重载函数实现 
  b、运行时多态性:通过虚函数实现。 
  2、虚函数 
  虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override
  3、抽象类 
  包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象

 

下面是一个承上启下的例子。一方面它是有关继承和运算符重载内容的综合应用的例子,通过这个例子可以进一步融会贯通前面所学的内容,另一方面又是作为讨论多态性的一个基础用例。

[12.1] 先建立一个Point()类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle()类,增加数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增加数据成员h()。要求编写程序,重载运算符“<<”“>>”,使之能用于输出以上类对象。

这个例题难度不大,但程序很长。对于一个比较大的程序,应当分成若干步骤进行。先声明基类,再声明派生类,逐级进行,分步调试。

1) 声明基类Point

类可写出声明基类Point的部分如下:

1. #include <iostream>

2. //声明类Point

3. class Point

4. {

5. public:

6.    Point(float x=0,float y=0);  //有默认参数的构造函数

7.    void setPoint(float ,float);  //设置坐标值

8.    float getX( )const {return x;}  //x坐标

9.    float getY( )const {return y;}  //y坐标

10.    friend ostream & operator <<(ostream &,const Point &);  //重载运算符“<<”

11. protected:  //受保护成员

12.    float x, y;

13. };

14. //下面定义Point类的成员函数

15. Point::Point(float a,float b) //Point的构造函数

16. {  //x,y初始化

17.    x=a;

18.    y=b;

19. }

20. void Point::setPoint(float a,float b) //设置xy的坐标值

21. {  //x,y赋新值

22.    x=a;

23.    y=b;

24. }

25. //重载运算符“<<”,使之能输出点的坐标

26. ostream & operator <<(ostream &output, const Point &p)

27. {

28.    output<<"["<<p.x<<","<<p.y<<"]"<<endl;

29.    return output;

30. }

以上完成了基类Point类的声明。

为了提高程序调试的效率,提倡对程序分步调试,不要将一个长的程序都写完以后才统一调试,那样在编译时可能会同时出现大量的编译错误,面对一个长的程序,程序人员往往难以迅速准确地找到出错位置。要善于将一个大的程序分解为若干个文件,分别编译,或者分步调试,先通过最基本的部分,再逐步扩充。

现在要对上面写的基类声明进行调试,检查它是否有错,为此要写出main函数。实际上它是一个测试程序。

1. int main( )

2. {

3.    Point p(3.5,6.4);  //建立Point类对象p

4.    cout<<"x="<<p.getX( )<<",y="<<p.getY( )<<endl;  //输出p的坐标值

5.    p.setPoint(8.5,6.8);  //重新设置p的坐标值

6.    cout<<"p(new):"<<p<<endl;  //用重载运算符“<<”输出p点坐标

7.    return 0;

8. }

getXgetY函数声明为常成员函数,作用是只允许函数引用类中的数据,而不允许修改它们,以保证类中数据的安全。数据成员xy声明为protected,这样可以被派生类访问(如果声明为private,派生类是不能访问的)。

程序编译通过,运行结果为:
x=3.5,y=6.4
p(new):[8.5,6.8]

测试程序检查了基类中各函数的功能,以及运算符重载的作用,证明程序是正确的。

2)声明派生类Circle

在上面的基础上,再写出声明派生类Circle的部分:

1. class Circle:public Point  //circlePoint类的公用派生类

2. {

3. public:

4.    Circle(float x=0,float y=0,float r=0);  //构造函数

5.    void setRadius(float );  //设置半径值

6.    float getRadius( )const;  //读取半径值

7.    float area ( )const;  //计算圆面积

8.    friend ostream &operator <<(ostream &,const Circle &);  //重载运算符“<<”

9. private:

10.    float radius;

11. };

12. //定义构造函数,对圆心坐标和半径初始化

13. Circle::Circle(float a,float b,float r):Point(a,b),radius(r){}

14. //设置半径值

15. void Circle::setRadius(float r){radius=r;}

16. //读取半径值

17. float Circle::getRadius( )const {return radius;}

18. //计算圆面积

19. float Circle::area( )const

20. {

21.    return 3.14159*radius*radius;

22. }

23. //重载运算符“<<”,使之按规定的形式输出圆的信息

24. ostream &operator <<(ostream &output,const Circle &c)

25. {

26.    output<<"Center=["<<c.x<<","<<c.y<<"],r="<<c.radius<<",area="<<c.area( )<<endl;

27.    return output;

28. }


为了测试以上Circle类的定义,可以写出下面的主函数:

1. int main( )

2. {

3.    Circle c(3.5,6.4,5.2);  //建立Circle类对象c,并给定圆心坐标和半径

4.    cout<<"original circle:\\nx="<<c.getX()<<", y="<<c.getY()<<", r="<<c.getRadius( )<<", area="<<c.area( )<<endl;  //输出圆心坐标、半径和面积

5.    c.setRadius(7.5);  //设置半径值

6.    c.setPoint(5,5);  //设置圆心坐标值x,y

7.    cout<<"new circle:\\n"<<c;  //用重载运算符“<<”输出圆对象的信息

8.    Point &pRef=c;  //pRefPoint类的引用变量,被c初始化

9.    cout<<"pRef:"<<pRef;  //输出pRef的信息

10.    return 0;

11. }

程序编译通过,运行结果为:
original circle:(输出原来的圆的数据)
x=3.5, y=6.4, r=5.2, area=84.9486
new circle:(输出修改后的圆的数据)
Center=[5,5], r=7.5, area=176.714
pRef:[5,5] (输出圆的圆心的数据)

可以看到,在Point类中声明了一次运算符“ <<”重载函数,在Circle类中又声明了一次运算符“ <<”,两次重载的运算符“<<”内容是不同的,在编译时编译系统会根据输出项的类型确定调用哪一个运算符重载函数。main函数第7行用“cout<< ”输出c,调用的是在Circle类中声明的运算符重载函数。

请注意main函数第8行:
    Point & pRef = c;

定义了 Point类的引用变量pRef,并用派生类Circle对象c对其初始化。前面我们已经讲过,派生类对象可以替代基类对象为基类对象的引用初始化或赋值(详情请查看:C++基类与派生类的转换)。现在 CirclePoint的公用派生类,因此,pRef不能认为是c的别名,它得到了c的起始地址, 它只是c中基类部分的别名,与c中基类部分共享同一段存储单元。所以用“cout<<pRef”输出时,调用的不是在Circle中声明的运算符重载函数,而是在Point中声明的运算符重载函数,输出的是的信息,而不是的信息。

3) 声明Circle的派生类Cylinder

前面已从基类Point派生出Circle类,现在再从Circle派生出Cylinder类。

1. class Cylinder:public Circle// CylinderCircle的公用派生类

2. {

3. public:

4.    Cylinder (float x=0,float y=0,float r=0,float h=0);  //构造函数

5.    void setHeight(float );  //设置圆柱高

6.    float getHeight( )const;  //读取圆柱高

7.    loat area( )const;  //计算圆表面积

8.    float volume( )const;  //计算圆柱体积

9.    friend ostream& operator <<(ostream&,const Cylinder&);  //重载运算符<<

10. protected:

11.    float height;//圆柱高

12. };

13. //定义构造函数

14. Cylinder::Cylinder(float a,float b,float r,float h):Circle(a,b,r),height(h){}

15. //设置圆柱高

16. void Cylinder::setHeight(float h){height=h;}

17. //读取圆柱高

18. float Cylinder::getHeight( )const {return height;}

19. //计算圆表面积

20. float Cylinder::area( )const { return 2*Circle::area( )+2*3.14159*radius*height;}

21. //计算圆柱体积

22. float Cylinder::volume()const {return Circle::area()*height;}

23. ostream &operator <<(ostream &output,const Cylinder& cy)

24. {

25.    output<<"Center=["<<cy.x<<","<<cy.y<<"],r="<<cy.radius<<",h="<<cy.height <<"\\narea="<<cy.area( )<<", volume="<<cy.volume( )<<endl;

26.    return output;

27. } //重载运算符“<<”


可以写出下面的主函数:

1. int main( )

2. {

3.    Cylinder cy1(3.5,6.4,5.2,10);//定义Cylinder类对象cy1

4.    cout<<"\\noriginal cylinder:\\nx="<<cy1.getX( )<<", y="<<cy1.getY( )<<", r="

5.       <<cy1.getRadius( )<<", h="<<cy1.getHeight( )<<"\\narea="<<cy1.area()

6.       <<",volume="<<cy1.volume()<<endl;//用系统定义的运算符“<<”输出cy1的数据

7.    cy1.setHeight(15);//设置圆柱高

8.    cy1.setRadius(7.5);//设置圆半径

9.    cy1.setPoint(5,5);//设置圆心坐标值x,y

10.    cout<<"\\nnew cylinder:\\n"<<cy1;//用重载运算符“<<”输出cy1的数据

11.    Point &pRef=cy1;//pRefPoint类对象的引用变量

12.    cout<<"\\npRef as a Point:"<<pRef;//pRef作为一个输出

13.    Circle &cRef=cy1;//cRefCircle类对象的引用变量

14.    cout<<"\\ncRef as a Circle:"<<cRef;//cRef作为一个输出

15.    return 0;

16. }

运行结果如下:
original cylinder:(输出cy1的初始值)
x=3.5, y=6.4, r=5.2, h=10 (圆心坐标x,y。半径r,高h)
area=496.623, volume=849.486 (圆柱表面积area和体积volume)
new cylinder: (输出cy1的新值)
Center=[5,5], r=7.5, h=15 ([5,5]形式输出圆心坐标)
area=1060.29, volume=2650.72(圆柱表面积area和体积volume)
pRef as a Point:[5,5] (pRef作为一个输出)
cRef as a Circle:Center=[5,5], r=7.5, area=176.714(cRef作为一个输出)

说明:在Cylinder类中定义了 area函数,它与Circle类中的area函数同名,根据前面我们讲解的同名覆盖的原则(详情请查看:C++多重继承的二义性问题),cy1.area( ) 调用的是Cylinder类的area函数(求圆柱表面积),而不是Circle类的area函数(圆面积)。请注意,这两个area函数不是重载函数,它们不仅函数名相同,而且函数类型和参数个数都相同,两个同名函数不在同 个类中,而是分别在基类和派生类中,属于同名覆盖。重载函数的参数个数和参数类型必须至少有一者不同,否则系统无法确定调用哪一个函数。

main函数第9行用“cout<<cy1”来输出cy1,此时调用的是在Cylinder类中声明的重载运算符“<<”,按在重载时规定的方式输出圆柱体cy1的有关数据。

main函数中最后4行的含义与在定义Circle类时的情况类似。pRefPoint类的引用变量,用cy1对其初始化,但它不是cy1的别名,只是cy1中基类Point部分的别名,在输出pRef时是作为一个Point类对象输出的,也就是说,它是一个。同样,cRefCircle类的引用变量,用cy1对其初始化,但它只是cy1中的直接基类Circle部分的别名, 在输出 cRef 时是作为Circle类对象输出的,它是一个",而不是一个圆柱体。从输 出的结果可以看出调用的是哪个运算符函数。

在本例中存在静态多态性,这是运算符重载引起的(注意3个运算符函数是重载而不是同名覆盖,因为有一个形参类型不同)。可以看到,在编译时编译系统即可以判定应调用哪个重载运算符函数。

Java的多态性

面向对象编程有三个特征,即封装、继承和多态。

    封装隐藏了类的内部实现机制,从而可以在不影响使用者的前提下改变类的内部结构,同时保护了数据。
    继承是为了重用父类代码,同时为实现多态性作准备。那么什么是多态呢?
    方法的重写、重载与动态连接构成多态性。Java之所以引入多态的概念,原因之一是它在类的继承问题上和C++不同,后者允许多继承,这确实给其带来的非常强大的功能,但是复杂的继承关系也给C++开发者带来了更大的麻烦,为了规避风险,Java只允许单继承,派生类与基类间有IS-A的关系(即“猫”is a “动物”)。这样做虽然保证了继承关系的简单明了,但是势必在功能上有很大的限制,所以,Java引入了多态性的概念以弥补这点的不足,此外,抽象类和接口也是解决单继承规定限制的重要手段。同时,多态也是面向对象编程的精髓所在。

要理解多态性,首先要知道什么是“向上转型”。

我定义了一个子类Cat,它继承了Animal类,那么后者就是前者是父类。我可以通过

Cat c = new Cat();
实例化一个Cat的对象,这个不难理解。但当我这样定义时:
Animal a = new Cat();
这代表什么意思呢?
    很简单,它表示我定义了一个Animal类型的引用,指向新建的Cat类型的对象。由于Cat是继承自它的父类Animal,所以Animal类型的引用是可以指向Cat类型的对象的。那么这样做有什么意义呢?因为子类是对父类的一个改进和扩充,所以一般子类在功能上较父类更强大,属性较父类更独特,

    定义一个父类类型的引用指向一个子类的对象既可以使用子类强大的功能,又可以抽取父类的共性。

    所以,父类类型的引用可以调用父类中定义的所有属性和方法,而对于子类中定义而父类中没有的方法,它是无可奈何的;

    同时,父类中的一个方法只有在在父类中定义而在子类中没有重写的情况下,才可以被父类类型的引用调用;

    对于父类中定义的方法,如果子类中重写了该方法,那么父类类型的引用将会调用子类中的这个方法,这就是动态连接。

看下面这段程序:
class Father{
public void func1(){
func2();
}
//这是父类中的func2()方法,因为下面的子类中重写了该方法
//所以在父类类型的引用中调用时,这个方法将不再有效
//取而代之的是将调用子类中重写的func2()方法
public void func2(){
System.out.println("AAA");
}
}

class Child extends Father{
//func1(int i)是对func1()方法的一个重载
//由于在父类中没有定义这个方法,所以它不能被父类类型的引用调用
//所以在下面的main方法中child.func1(68)是不对的
public void func1(int i){
System.out.println("BBB");
}
//func2()重写了父类Father中的func2()方法
//如果父类类型的引用中调用了func2()方法,那么必然是子类中重写的这个方法
public void func2(){
System.out.println("CCC");
}
}

public class PolymorphismTest {
public static void main(String[] args) {
Father child = new Child();
child.func1();//打印结果将会是什么?
   }
}
   上面的程序是个很典型的多态的例子。子类Child继承了父类Father,并重载了父类的func1()方法,重写了父类的func2()方法。重载后的func1(int i)和func1()不再是同一个方法,由于父类中没有func1(int i),那么,父类类型的引用child就不能调用func1(int i)方法。而子类重写了func2()方法,那么父类类型的引用child在调用该方法时将会调用子类中重写的func2()。

那么该程序将会打印出什么样的结果呢?

很显然,应该是“CCC”。

对于多态,可以总结它为:

一、使用父类类型的引用指向子类的对象;

二、该引用只能调用父类中定义的方法和变量;

三、如果子类中重写了父类中的一个方法,那么在调用这个方法的时候,将会调用子类中的这个方法;(动态连接、动态调用)

四、变量不能被重写(覆盖),”重写“的概念只针对方法,如果在子类中”重写“了父类中的变量,那么在编译时会报错。

****************************************************************************************************************************

多态详解(整理)2008-09-03 19:29多态是通过:
1 接口 和 实现接口并覆盖接口中同一方法的几不同的类体现的
2 父类 和 继承父类并覆盖父类中同一方法的几个不同子类实现的.

一、基本概念

多态性:发送消息给某个对象,让该对象自行决定响应何种行为。
通过将子类对象引用赋值给超类对象引用变量来实现动态方法调用。

java 的这种机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。

1. 如果a是类A的一个引用,那么,a可以指向类A的一个实例,或者说指向类A的一个子类。
2. 如果a是接口A的一个引用,那么,a必须指向实现了接口A的一个类的实例。

二、Java多态性实现机制

SUN目前的JVM实现机制,类实例的引用就是指向一个句柄(handle)的指针,这个句柄是一对指针:
一个指针指向一张表格,实际上这个表格也有两个指针(一个指针指向一个包含了对象的方法表,另外一个指向类对象,表明该对象所属的类型);
另一个指针指向一块从java堆中为分配出来内存空间。

三、总结

1、通过将子类对象引用赋值给超类对象引用变量来实现动态方法调用。

DerivedC c2=new DerivedC();
BaseClass a1= c2; //BaseClass 基类,DerivedC是继承自BaseClass的子类
a1.play(); //play()在BaseClass,DerivedC中均有定义,即子类覆写了该方法

分析:
* 为什么子类的类型的对象实例可以覆给超类引用?
自动实现向上转型。通过该语句,编译器自动将子类实例向上移动,成为通用类型BaseClass;
* a.play()将执行子类还是父类定义的方法?
子类的。在运行时期,将根据a这个对象引用实际的类型来获取对应的方法。所以才有多态性。一个基类的对象引用,被赋予不同的子类对象引用,执行该方法时,将表现出不同的行为。

a1=c2的时候,仍然是存在两个句柄,a1和c2,但是a1和c2拥有同一块数据内存块和不同的函数表。

2、不能把父类对象引用赋给子类对象引用变量

BaseClass a2=new BaseClass();
DerivedC c1=a2;//出错

    java里面,向上转型是自动进行的,但是向下转型却不是,需要我们自己定义强制进行。
c1=(DerivedC)a2; 进行强制转化,也就是向下转型.

3、记住一个很简单又很复杂的规则,一个类型引用只能引用引用类型自身含有的方法和变量。
你可能说这个规则不对的,因为父类引用指向子类对象的时候,最后执行的是子类的方法的。
其实这并不矛盾,那是因为采用了后期绑定,动态运行的时候又根据型别去调用了子类的方法。而假若子类的这个方法在父类中并没有定义,则会出错。
例如,DerivedC类在继承BaseClass中定义的函数外,还增加了几个函数(例如 myFun())

分析:
当你使用父类引用指向子类的时候,其实jvm已经使用了编译器产生的类型信息调整转换了。
这里你可以这样理解,相当于把不是父类中含有的函数从虚拟函数表中设置为不可见的。注意有可能虚拟函数表中有些函数地址由于在子类中已经被改写了,所以对象虚拟函数表中虚拟函数项目地址已经被设置为子类中完成的方法体的地址了。

4、Java与C++多态性的比较 

jvm关于多态性支持解决方法是和c++中几乎一样的,
只是c++中编译器很多是把类型信息和虚拟函数信息都放在一个虚拟函数表中,但是利用某种技术来区别。

Java把类型信息和函数信息分开放。Java中在继承以后,子类会重新设置自己的虚拟函数表,这个虚拟函数表中的项目有由两部分组成。从父类继承的虚拟函数和子类自己的虚拟函数。
虚拟函数调用是经过虚拟函数表间接调用的,所以才得以实现多态的。

Java的所有函数,除了被声明为final的,都是用后期绑定。

. 1个行为,不同的对象,他们具体体现出来的方式不一样,
比如: 方法重载 overloading 以及 方法重写(覆盖)override
class Human{
void run(){输出 人在跑}
}
class Man extends Human{
void run(){输出 男人在跑}
}
这个时候,同是跑,不同的对象,不一样(这个是方法覆盖的例子)
class Test{
void out(String str){输出 str}
void out(int i){输出 i}
}
这个例子是方法重载,方法名相同,参数表不同

ok,明白了这些还不够,还用人在跑举例
Human ahuman=new Man();
这样我等于实例化了一个Man的对象,并声明了一个Human的引用,让它去指向Man这个对象意思是说,把 Man这个对象当 Human看了.

比如去动物园,你看见了一个动物,不知道它是什么, "这是什么动物? " "这是大熊猫! "
2句话,就是最好的证明,因为不知道它是大熊猫,但知道它的父类是动物,所以, 这个大熊猫对象,你把它当成其父类 动物看,这样子合情合理.

这种方式下要注意 new Man();的确实例化了Man对象,所以 ahuman.run()这个方法 输出的 是 "男人在跑 "

如果在子类 Man下你 写了一些它独有的方法 比如 eat(),而Human没有这个方法,

在调用eat方法时,一定要注意 强制类型转换 ((Man)ahuman).eat(),这样才可以...

对接口来说,情况是类似的...

实例:

package domatic;

//定义超类superA
class superA {
int i = 100;

void fun(int j) {
j = i;
System.out.println("This is superA");
}
}

// 定义superA的子类subB
class subB extends superA {
int m = 1;

void fun(int aa) {
System.out.println("This is subB");
}
}

// 定义superA的子类subC
class subC extends superA {
int n = 1;

void fun(int cc) {
System.out.println("This is subC");
}
}

class Test {
public static void main(String[] args) {
superA a = new superA();
subB b = new subB();
subC c = new subC();
a = b;
a.fun(100);
a = c;
a.fun(200);
}
}
/*
* 上述代码中subB和subC是超类superA的子类,我们在类Test中声明了3个引用变量a, b,
* c,通过将子类对象引用赋值给超类对象引用变量来实现动态方法调用。也许有人会问:
* "为什么(1)和(2)不输出:This is superA"。
* java的这种机制遵循一个原则:当超类对象引用变量引用子类对象时,
* 被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,
* 但是这个被调用的方法必须是在超类中定义过的,
* 也就是说被子类覆盖的方法。
* 所以,不要被上例中(1)和(2)所迷惑,虽然写成a.fun(),但是由于(1)中的a被b赋值,
* 指向了子类subB的一个实例,因而(1)所调用的fun()实际上是子类subB的成员方法fun(),
* 它覆盖了超类superA的成员方法fun();同样(2)调用的是子类subC的成员方法fun()。
* 另外,如果子类继承的超类是一个抽象类,虽然抽象类不能通过new操作符实例化,
* 但是可以创建抽象类的对象引用指向子类对象,以实现运行时多态性。具体的实现方法同上例。
* 不过,抽象类的子类必须覆盖实现超类中的所有的抽象方法,
* 否则子类必须被abstract修饰符修饰,当然也就不能被实例化了
*/
以上大多数是以子类覆盖父类的方法实现多态.下面是另一种实现多态的方法-----------重写父类方法

1.JAVA里没有多继承,一个类之能有一个父类。而继承的表现就是多态。一个父类可以有多个子类,而在子类里可以重写父类的方法(例如方法print()),这样每个子类里重写的代码不一样,自然表现形式就不一样。这样用父类的变量去引用不同的子类,在调用这个相同的方法print()的时候得到的结果和表现形式就不一样了,这就是多态,相同的消息(也就是调用相同的方法)会有不同的结果。举例说明:
//父类
public class Father{
//父类有一个打孩子方法
public void hitChild(){
}
}
//子类1
public class Son1 extends Father{
//重写父类打孩子方法
public void hitChild(){
System.out.println("为什么打我?我做错什么了!");
}
}
//子类2
public class Son2 extends Father{
//重写父类打孩子方法
public void hitChild(){
System.out.println("我知道错了,别打了!");
}
}
//子类3
public class Son3 extends Father{
//重写父类打孩子方法
public void hitChild(){
System.out.println("我跑,你打不着!");
}
}

//测试类
public class Test{
public static void main(String args[]){
Father father;

father = new Son1();
father.hitChild();

father = new Son2();
father.hitChild();

father = new Son3();
father.hitChild();
}
}
都调用了相同的方法,出现了不同的结果!这就是多态的表现!


 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值