C++、Java、C# 多态知识总结

一、C++的多态

1.静态联编

指程序之间的匹配、连接在编译阶段,即程序运行之前完成,也称早期匹配。简单理解为,编译期间就能准确获得函数入口地址、返回地址和参数信息等,从而完成匹配。(不可实现多态)

静态联编的情况:
a. 一个类中的重载。
b. 基类的成员函数在派生类中重载。例:X是基类,Y是X的派生类,X类的成员函数show()也是Y类的成员函数,C++编译时有三种情况:
(1).根据函数参数特征区分;
(2).使用“::”区分,X::show();与Y::show();
(3).根据对象区分:如:X xx; Y yy; xx.show(); yy.show();

2.动态联编

指程序联编推迟到运行时判断,又称晚期联编。(可实现多态)
C++的动态联编依赖虚函数和基类指针实现。

基类指针和派生类指针与积累对象和派生类对象有4种可能匹配的使用方式:
a.基类指针引用基类对象
b.派生类指针引用派生类对象
c.基类指针引用派生类对象
d.派生类指针引用基类对象

基类指针引用派生类对象:
基类指针指向派生类对象时,只能引用基类成员。
如果想引用派生类中的特有成员,必须通过强制类型转换,把基类指针转换成派生类指针,否则语法错误。
简单例子:

#include<iostream>
#include<cstring>
using namespace std;
class A
{
    char name[20];
public:
    void put(char *s)
    {
        strcpy(name,s);
    }
    void show()const
    {
        cout<<name<<endl;
    }
};
class B:public A
{
    char phonename[20];
public:
    void putp(char *num)
    {
        strcpy(phonename,num);
    }
    void showp()const
    {
        cout<<phonename<<endl;
    }
};
int main()
{
    A *ap;
    A a;
    B b;
    ap=&a;
    ap->put("张三");
    ap->show();

    ap=&b;//基类指针指向派生类对象
    ap->put("李四");//调用基类成员函数
    ap->show();

    a.show();
    b.show();//验证确实对派生类的对象进行了操作

    b.putp("1234567890");//调用派生类成员函数
    ((B*)ap)->showp();//对基类指针进行强制类型转换
    /*
    ap=&b;
    ap->putp("1234567890");基类指针调用派生类自定义的(新增)成员函数
    ap->showp();
    这样会编译错误。
    */
    return 0;
}

注意:基类指针获得派生类对象的地址后,可以调用基类的成员函数 对派生类对象进行操作,但不能直接用基类指针调用派生类自定义的成员函数(基类中没有该函数,派生类新增的函数),必须对基类指针强制类型转换成派生类指针才能对派生类自定义的成员函数调用。
派生类指针引用基类对象
派生类指针只有经过强制类型转换后才能引用基类对象。

DateTime dt;//创建派生类对象,基类为Date;
DateTime *pdt=&dt;//创建派生类对象指针
((Date)dt).print();//派生类对象类型强制转换成基类对象,调用基类成员函数
((Date *)pdt)->print();//派生类对象指针强制类型转换成基类指针,调用基类成员函数

虚函数和动态联编

冠以关键字virtual的成员函数称为虚函数。
运行时的多态:使用同一个基类指针访问虚函数。
基类指针:无需类型转换就能指向派生类对象,只能访问派生类从基类继承的成员。

注意:如果基类成员函数不是虚函数,当用基类指针指向派生类对象时,通过this指针可以看到当前对象继承基类的属性值(数据成员),但调用的却是基类定义的成员函数,使得派生类对象的数据执行基类版本函数的代码。例如:

Bace b('A');//基类
Bace *p;
First f('T','O');//派生类
p=&b;
p->who();//基类指针指向基类对象调用基类的成员函数
p=&f;
p->who();//基类指针指向派生类对象调用基类的成员函数
f.who();//派生类对象调用派生类函数
((First *)p)->who();//基类指针强制类型转换成派生类指针调用派生类对象
结果:
Bace class:A
Bace class:T
First class:T,O
First class:T,O

所以,虚函数的作用就是让基类指针可以依赖运行时的地址值调用不同版本的成员函数。

虚函数使用的注意:
1.一旦一个成员函数被说明为虚函数,不管经历多少派生类层次,所有界面相同的重载函数都保持虚特性,因为派生类也是基类。
2.虚函数必须是类的成员函数,不能将虚函数说明为全局(非成员)函数,也不能说明为静态成员函数。因为虚函数的动态联编必须依靠this指针实现,静态成员函数没有this指针。
3.不能将友元说明为虚函数,但虚函数可以是另一个类的友元。
4.析构函数可以是虚函数,构造函数不可以是虚函数。

虚函数的重载特性:
即在派生类中重定义基类的虚函数。(我觉得是重写)
类层次重载各个虚函数,要求函数名、返回类型、参数个数、参数类型和顺序完全相同,而一般的函数重载,函数的返回类型、参数个数、参数类型不同,仅要求函数名相同。
虚析构函数: 解决class A{ public:~A(){} }; … A *ap=new B;… delete ap;时只能调用基类的析构函数,不能释放派生类对象占有的资源。
正确的方式:class A{ public:virtual~A(){} };

纯虚函数和抽象类

解决了基类函数定义无意义的情况。
一个具有纯虚函数的基类称为抽象类。
纯虚函数是在基类中说明的虚函数,它在该基类中没有实现定义(无函数体),要求所有派生类必须定义自己的版本。
纯虚函数:virtual 类型 函数名(参数表)=0;

抽象类至少有一个纯虚函数,如果抽象类的一个派生类没有为继承的纯虚函数定义实现版本,则它仍是抽象类,定义了纯虚函数实现版本的派生类称为具体类。
C++抽象类使用的注意事项:
1.抽象类只能用作其他类的基类。
2.抽象类不能建立对象。
3.抽象类不能用作参数类型、函数返回类型或显示类型转换。

但是可以说明抽象类的指针、引用。抽象类指针通过获取不同派生类对象地址来改变指向,利用多态性调用纯虚函数在派生类中的不同实现版本。

二、Java的多态

多态存在的三个必要条件:
1.继承:要有继承关系,子类继承父类;
2.重写:子类要重写父类的方法;
3.父类引用指向子类对象。

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

public class Test {
    public static void main(String[] args) {
      show(new Cat());  // 以 Cat 对象调用 show 方法
      show(new Dog());  // 以 Dog 对象调用 show 方法
                
      Animal a = new Cat(); // 向上转型  
      a.eat();// 调用的是 Cat 的 eat
      Cat c = (Cat)a; // 向下转型  
      c.work(); // 调用的是 Cat 的 work
  }  

补充:向上转型 把子类创建的对象赋值给父类对象。 向上转型对象具有以下特点:
1.上转型对象不能操作子类新增成员(子类新增属性和子类新增方法)。
2.上转型对象可以对子类继承或重写的成员进行访问、调用。
3.若子类重写了父类的某方法后,当对象的上转型对象调用这个重写方法时,调用的是子类的重写方法。 注意:
1.对象的上转型对象的实体是子类负责创建的,但上转型对象会失去原对象的一些属性和功能。
2.可以将对象的上转型对象再强制转换为一个子类对象,此时该子类对象又具备了子类的所有属性和功能。
3.父类、接口、抽象类都可以接受子类创建的对象。

1.虚函数

Java没有虚函数,它的普通函数就相当于C++的虚函数,函数可以加final关键字变成非虚函数,取消虚函数特性。

方法的覆盖(重写)

即子类对父类继承来的方法重新加以定义。
当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。
要想调用父类中被重写的方法,则必须使用关键字 super。
重写规则:
1.参数列表必须完全与被重写方法的相同
2.返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类。(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
3.访问权限不能 比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
4.父类的成员方法只能被它的子类重写
5.声明为 final 的方法不能被重写
6.声明为static 的方法不能被重写,但是能够被再次声明。
7.子类和父类在同一个包中,那么子类可以重写父类所有方法除了声明为 private 和 final 的方法。
8.子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法
9.重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
10.构造方法不能被重写
11.如果不能继承一个方法,则不能重写这个方法。

重载

一个类里,方法名相同,参数不同,返回类型可同可不同。
重载规则:
1.必须改变参数列表(参数个数或类型不一样);
2.可以改变返回类型
3.可以改变访问修饰符
4.可以声明新的或更广的检查异常
5. 方法能够在同一个类中或者在一个子类中被重载。
无法以返回值类型作为重载函数的区分标准

方法重载是一个类多态性表现,而方法重写是子类与父类的一种多态性表现。

2.抽象类

抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
抽象类不能实例化对象,必须被继承,才能被使用。
定义:

public abstract class 抽象类名称{
    ...
}

抽象方法:
Abstract 关键字声明抽象方法,只包含一个方法名,没有方法体。
声明抽象方法注意:
1.如果一个类包含抽象方法,那么该类必须是抽象类。
2.任何子类必须重写父类的抽象方法,或者声明自身为抽象类。

总结一下:
1.抽象类必须被继承。
2.抽象类不能直接实例化,可以有构造函数。
3.抽象类中可以不包含抽象方法,但是有抽象方法的类必定是抽象类。
4.抽象方法在子类中必须被重写。
5.抽象方法只有声明,不能实现。
6.构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。

3.接口

是特殊的抽象类,仅含有抽象方法和常量,接口中抽象方法的具体实现由实现接口的类来完成,实现接口的类必须覆盖接口中的所有抽象方法。
定义接口如下例:

public interface ICalculate{
     public static final double PI=3.14159;
     public double getArea();//抽象方法,可以省去abstract
}

在定义接口时,所有的方法都为公共的抽象方法,且abstract,public可以省略。

接口与类相似点:
1.一个接口可以有多个方法。
2.接口文件保存在 .java 结尾的文件中,文件名使用接口名。
3.接口的字节码文件保存在 .class 结尾的文件中。
4.接口相应的字节码文件必须在与包名称相匹配的目录结构中。
接口与类的区别:
1.接口不能用于实例化对象。
2.接口没有构造方法。
3.接口中所有的方法必须是抽象方法。
4.接口不能包含成员变量,除了 static 和 final 变量。
5.接口不是被类继承了,而是要被类实现。 接口支持多继承。

接口的继承:

public interface IC extends IA,IB{
}

接口的实现:

public class Yuan implements Icalculate{
}

抽象类和接口的区别:
1.抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

三、C#的多态

1.静态多态性:

两种方式:函数重载与运算符重载
运算符重载:

public static Box operator+ (Box b, Box c)
{
   Box box = new Box();
   box.length = b.length + c.length;
   box.breadth = b.breadth + c.breadth;
   box.height = b.height + c.height;
   return box;
}

2.动态多态性

动态多态性是通过抽象类和虚方法实现的。

虚方法

虚方法是使用关键字 virtual 声明的。
虚方法可以在不同的继承类中有不同的实现。
对虚方法的调用是在运行时发生的。

方法覆盖:
父类成员函数声明为虚拟的,即在返回类型之前添加 virtual关键字。子类可以选择使用override关键字,将父类实现替换为它自己的实现,这就是方法重写。

class Animal
{
    ...
    public virtual string Shout()
    {
        return "";
    }
}
class Cat : Animal
{
    ...
    public override string Shout()
    {
        return "喵";
    }
}

抽象类:

abstract class Animal
{
    ……
    protected abstract string getShoutSound();
}

注意:
1.抽象类不能实例化。
2.抽象方法若要实现功能必须被子类重写。(抽象方法是没有实现体的虚方法)
3.如果类中包含抽象方法,那么类就必须定义为抽象类。 抽象类拥有尽可能多的共同代码,拥有尽可能 少的数据。
4.抽象类是被用来继承的,在等级节点中,抽象类是树枝节点,具体类是树叶节点。

接口:
接口不能实例化,不能有构造方法和字段; 不能有public、private等修饰符;不能声明虚拟的或静态的

interface IChange
{
    string ChangeThing(string thing);
}

接口与抽象类的区别:
1.抽象类在定义类型方法的时候,可以给出方法的实现部分,也可以不给出;而对于接口来说, 其中所定义的方法都不能给出实现部分。
2.继承类对于抽象类所定义的抽象方法,可以不用重写,也就是说,可以延用抽象类的方法; 而对于接口类所定义的方法或者属性来说,在 继承类中必须要给出相应的方法和属性实现。
3.在抽象类中,新增一个方法的话,继承类中可以不用作任何处理;而对于接口来说,则需要修改继承类,提供新定义的方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值