1.9 多态


返回目录 1 面向对象技术
上一节 1.8 继承


多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。

引用Charlie Calverts对多态的描述——多态性是允许你将父对象设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(摘自“Delphi4 编程技术内幕”)。

简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。

多态性在C++中是通过虚函数实现的。

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

  • 虚基类

    虚基类使得其派生类在间接地多次继承本类时,只继承本类的一份成员,避免出现多次继承产生多份拷贝的二义性。

    源代码

    /* VirtualBaseClass.cpp 虚基类实例*/#include <iostream>class Animal // 定义动物类{protected:    int age; // 存储年龄public:    Animal(int age_t):age(age_t){}; // 参数列表初始化年龄    ~Animal(){}; // 析构函数,这里用不上};class Bird:virtual public Animal // 鸟虚继承动物类{private:    int flyHight; // 定义鸟飞的高度protected:    int getFlyHight(); // 获取鸟飞的高度public:    Bird(int flyHight_t, int age_t):flyHight(flyHight_t), Animal(age_t){}; // 参数列表初始化高度和年龄    ~Bird(){}; // 析构函数};int Bird::getFlyHight(){    return flyHight; // 返回鸟飞的高度}class Fish:virtual public Animal // 鱼虚继承动物类{private:    int divingDepth; // 定义鱼潜水深度protected:    int getDivingDepth(); // 获取鱼潜水深度public:    Fish(int divingDepth_t, int age_t):divingDepth(divingDepth_t), Animal(age_t){}; // 参数列表初始化深度年龄    ~Fish(){}; // 析构函数};int Fish::getDivingDepth(){    return divingDepth; // 返回鱼潜的深度}class WaterBird:virtual public Bird, Fish // 多重继承鸟和鱼,此时间接地两次继承了动物类,但是之前鸟和鱼继承时使用了虚继承,因此此处默认虚继承,但是为了可读性,这里最好写上virtual{public:    WaterBird(int flyHight_t, int divingDepth_t, int age_t):Bird(flyHight_t, 5), Fish(divingDepth_t, 6), Animal(age_t){};    ~WaterBird(){};    void displayInfos();};void WaterBird::displayInfos(){    std::cout << "种族:飞鱼" << std::endl;    std::cout << "年龄:" << age << std::endl;    std::cout << "最高飞行高度:" << getFlyHight() << std::endl;    std::cout << "最大潜水深度:" << getDivingDepth() << std::endl;}int main(){    WaterBird wb(200, 50, 4);    wb.displayInfos();        return 0;    }

    编译运行

    种族:飞鱼年龄:4最高飞行高度:200最大潜水深度:50

    我们发现,在水鸟内继承age时,初始化的值分别为56,然后虚继承动物类的age时,赋值为4,最终的结果为4

    这意味着我们通过创建虚基类继承的成员默认为最高基类的成员。

    如果想要用鸟或者鱼的age,需要使用域作用符。

  • 虚函数

    多态的本质是同一个函数的多种形态

    一般而言,C++支持的多态有两种:

    • 编译时多态

      静态联编在编译时就已经确定了多态性,一般通过重载进行实现;

    • 运行时多态

      动态联编则在运行时才能确定多态性 ,一般通过继承和虚函数来实现。

    若某个基类函数声明为虚函数,当派生类使用基类指针基类引用操作派生类对象时,系统会自动用派生类同名函数代替基类虚函数。

如果基类函数没有声明为虚函数,那么使用基类指针调用时,调用到的是基类的函数。

源代码

/* VirtualFunction.cpp 虚函数实例*/#include <iostream>class BaseClass // 基类{private:public:    BaseClass(){};    ~BaseClass(){};    void speakNormal() // 提示信息的函数,这里是普通函数    {        std::cout << "这里是基类!" << std::endl;    }    virtual void speakVirtual() // 提示信息的函数,这里是虚函数    {        std::cout << "这里是基类!" << std::endl;    }};class DerivedClass:public BaseClass // 公有继承{private:public:    DerivedClass(){};    ~DerivedClass(){};    void speakNormal() // 提示信息的函数,这里是普通函数    {        std::cout << "这里是派生类!" << std::endl;    }    virtual void speakVirtual() // 提示信息的函数,这里是虚函数,虚函数默认继承时虚函数,但是为了可读性,建议在写的时候加上virtual关键字    {        std::cout << "这里是派生类!" << std::endl;    }};int main(){    DerivedClass dercs; // 创建一个普通的派生类对象    BaseClass* bascs = &dercs; // 创建一个基类指针指向刚刚的派生类对象    bascs->speakNormal(); // 基类指针调用普通函数    bascs->speakVirtual(); //基类指针调用虚函数    return 0;}

编译运行

这里是基类!这里是派生类!

我们可以看到:

使用普通函数,基类指针调用的是基类同名函数;

使用虚函数,基类指针调用的是基类指针指向对象的同名函数。

注意:派生类有时候需要销毁资源,如果使用基类指针,那么必须要将基类析构函数设为虚函数,否则无法销毁派生类资源

另外,构造函数不能作为虚函数。

  • 纯虚函数和抽象类

    纯虚函数是指如下模式的函数:

    virtual 返回值 函数名( 参数列表 ) = 0;

    抽象类是指包含至少一个纯虚函数的类:

    class 类名{public:    virtual 返回值 函数名( 参数列表 ) = 0;    其它函数声明}

    有时候我们不知道如何实现某个功能,比如要给一个形状求面积,但是三角形和矩形的求面积方法并不相同,但是求面积又是必须要做的。

    这种情况我们就可以使用纯虚函数。

    纯虚函数只需声明,无需实现,具体的实现在其派生出子类以后再实现,如果子类声明了这个纯虚函数但是没有实现,那么这个函数依然被视作纯虚函数。

    例如定义一个形状类,里面有一个求面积的纯虚函数作为接口,在三角形和矩形、圆形等不同的形状里具体实现即可。

    包含至少一个纯虚函数的类是抽象类,抽象类不能实例化对象,必须要所有纯虚函数实现的子类才能实例化对象,如果子类依然有纯虚函数,那么这个类依然是一个抽象类。

    源代码

    /* AbstractClass.cpp 抽象类实例*/#include <iostream>class Shapes // 定义形状类{private:public:    Shapes(){};    ~Shapes(){};    virtual void disp() = 0; // 定义求面积的纯虚函数,此时形状类作为抽象类,不能实例化对象,纯虚函数要在派生类中实现};class Squera:public Shapes{public:    Squera(int length, int width); // (矩形长度, 矩形宽度)    ~Squera(){};    class Squera_data // 定义一个内部类    {    public:        int length_t; // 长        int width_t; // 宽    };    void disp(); // 实现基类的纯虚函数private:    Squera_data data_t; // 存储矩形的数据};Squera::Squera(int length, int width){    data_t.length_t = length;    data_t.width_t = width;}void Squera::disp(){    std::cout << "矩形面积为:" << data_t.length_t * data_t.width_t << std::endl;}class Triangle{private:    double bottomL_l; // 底边    double heightL_l; // 高    class Triangle_run // 内部类来运算面积    {    private:    public:        Triangle_run(Triangle& tri_l){            std::cout << "三角形面积为:" << tri_l.bottomL_l * tri_l.heightL_l / 2 << std::endl;        }    };public:    Triangle(double bottomL, double heightL):bottomL_l(bottomL), heightL_l(heightL){}; // (底, 高),三角形的属性    ~Triangle(){};    void disp(); // 实现纯虚函数};void Triangle::disp(){    Triangle_run tri_r_l(*this); // 把刚刚实例化的本类作为参数传入}int main(){    Squera squera_l(3, 4); // 矩形求面积实现    squera_l.disp();    Triangle tri_t(3, 4); // 三角形求面积实现    tri_t.disp();        return 0;}

    编译运行

    矩形面积为:12三角形面积为:6

    我们发现,我们实现了子类不同求面积的函数,但是都以disp为名字,这样,我们直接调用disp就可以在不同的派生类中做到同一件事情——求面积,尽管它们的具体实现方式不同。

    值得注意的是,这里用到了内部类,又称嵌套类,在类中定义类,内部类和外部类可以互相调用对方私有成员,但是内部类调用外部类时需要传入参数。


返回目录 1 面向对象技术
上一节 1.8 继承


参考资料:

  • 《C++程序设计》传智播客
  • 博客园
  • CSDN
  • 百度百科