zoukankan      html  css  js  c++  java
  • (C/C++学习笔记) 十八. 继承和多态

    十八. 继承和多态

    ● 继承的概念

    继承(inheritance):

    以旧类为基础创建新类, 新类包含了旧类的数据成员和成员函数(除了构造函数和析构函数), 并且可以派生类中定义新成员. 形式:

    class <派生类名>: <继承方式1> <基类名1>

                         <继承方式2> <基类名2>

         ...,

         <继承方式n> <基类名n>

    {

        <派生类新定义的成员>

    }

    #include <iostream>

    using namespace std;

     

    class Person

    {

    public:

        int i;

        Person()

        {

            i=11;

            j=12;

            k=13;

        }

    private:

        int j;

    protected:

        int k;

    };

     

    class People:public Person    //以公有继承方式建立基类People

    {

        //下面在基类中建立新的成员

    public:

        void Show()

        {

        cout<<i<<endl;

        //cout<<j<<endl;    //j在基类中是私有成员, 在子类中不可见

        cout<<k<<endl;    //j在基类中是保护成员, 在子类中可见

        }

    };

     

    void main()

    {

        People p;

        p.Show();

    }

     

    ● 派生类对对基类成员的访问能力

    把握三者的"兼并"能力: public<protected<private

     

    ● 根据结果查看构造函数和析构函数的调用顺序

    #include <iostream>

    using namespace std;

     

    class Person

    {

    public:

        int i;

        Person()    //父类的构造函数

        {

            cout<<"Peron"<<endl;

        }

        ~Person()    //父类的析造函数

        {

            cout<<"~Person"<<endl;

        }

    };

     

    class People:private Person

    {

    public:

        People()    //子类的构造函数

        {

            cout<<"People"<<endl;

        }

        ~People()    //子类的析造函数

        {

            cout<<"~People"<<endl;

        }

    };

     

    void main()

    {

        People p;

    }

    调用顺序为: 父类构造函数→子类构造函数→父类析构函数→子类析构函数

    ※ 注意: 父类的构造函数不可以被子类继承, 我们看到p对象被创造之前构造函数已经被调用了,所以子类没有继承基类的构造函数, 不过基类的构造函数还是会被系统自动调用(这是初始化对象).

    析构函数也不会被继承, 如果要继承, 要加关键字virtual. (virtual不能修饰构造函数)

     

    ● 联编(binding) & 多态(polymorphism)

    例如:

    class A

    {    

        void func() {cout<<"It's A"<<endl;

     

    };

     

    class B

    {    

        void func() {cout<<"It's B"<<endl;

     

    };

    int main()

    {

        func();

    }

    联编就是决定将main()函数中的fun()的函数调用映射到A中的func函数还是B中的func函数的过程。

    main()函数和fun()函数之间的映射关系就是联编.

    ※设两个集合AB,和它们元素之间的对应关系R,如果对于A中的每一个元素,通过RB中都存在唯一一个元素与之对应,则该对应关系R就称为从AB的一个映射(mapping). 映射/射影,在数学及相关的领域经常等同于函数。

     

    联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址,它是计算机程序自身彼此关联的过程。

    按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。

    静态联编说的是在编译时就已经确定好了调用和被调用两者的关系。

    动态联编说的是程序在运行时才确定调用和被调用者的关系。

    ※ 多态性包括编译时的多态性和运行时的多态性

     

    拿上面的例子来说,静态联编就是在编译的时候就决定了main函数中调用的是A中的func还是B中的func; 动态联编在编译的时候还不知道到底应该选择哪个func函数,只有在真正执行的时候,它才确定。

     

    静态联编和动态联编都是属于多态性的, 它们是在不同的阶段进对不同的实现进行不同的选择; 实多态性的本质就是选择, 因为存在着很多选择,所以就有了多态。

     

    C++多态有两种形式,动态多态和静态多态:

    静态多态通过模板来实现,因为这种多态实在编译时实现,所以称为静态多态。

    动态多态是指一般的多态,通过类继承和虚函数来实现多态;因为这种多态实在运行时实现,所以称为动态多态。

     

    ● 运算符的重载(operator overloading)

    运算符实际上是一个内置的函数, 所以运算符的重载实际上就是函数的重载.

    Operators can be extended to work out not just with variables of built-in types but also with objects of classes.

    重载运算符的声明形式:

    <返回类类型> operator <运算符符号> (<参数表>)

    {

        <函数体>

    }

    ※ 一下操作符不能重载:

    "::"、"."、"*"、"?:"、sizeoftypedefnewdeletestatic_castdynamic_castconst_castreinterpret_cast

    //通过成员函数实现对象的相加

    #include <iostream>

    using namespace std;

     

    class Book

    {

    public:

        Book (int page)

        {

            bk_page=page;

        }

        

        int Add(Book bk) //以对象bk作为函数的形参, 这种情况一般会设计对象成员的访问

        {

            return bk_page+bk.bk_page;    //bk_page是本对象的数据成员, bk.bk_page另一个对象的数据成员

        }    

        

    protected:

        int bk_page;

    };

     

    void main()

    {

        Book bk1(10);

        Book bk2(20);

        cout<<bk1.Add(bk2)<<endl;

        //bk1.add(bk2)的意思调用对象bk1的成员函数Add(), 对象bk1本身有一个数据成员bk_page, bk2也有一个数据成员bk2.bk_page

    }

     

    //通过运算符的重载实现对象的相加

    #include <iostream>

    using namespace std;

     

    class Book

    {

    public:

        Book (int page)

        {

            bk_page=page;

        }

        void display()

        {

            cout << bk_page << endl;

        }

        Book operator+(Book bk) //运算符的重载, 此时运算符"+"也是Book类的成员函数了

            {

                return Book(bk_page+bk.bk_page); //Book可以省略; book_page指的是当前对象的数据成员, bk.book_page代表另一个对象的数据成员

            }

        operator int() //将转换运算符int()进行重载

        {

            return bk_page;

        }

    protected:

        int bk_page;

    };

     

    void main()

    {

        Book bk1(10);

        Book bk2(20);

        Book tmp(0);

        tmp= bk1+bk2; //bk1+bk2的结果是Book类类型的, 所以这个结果只能赋给Book型的tmp对象

        tmp.display();

        cout<<int(bk1)+int(bk2)<<endl; //int()是转换运算符, 即将bk1bk2Book类类型转换至int类型

    }

     

     

    //上面是在类体内, "+"会重载为Book类的成员函数.

    //双目运算符"+"有两个操作数, 如果定义在类体内, 参数要少一个; 在类体外, 参数是两个.

    //也就是说, 当运算符重载为类的成员函数时, 函数的参数个数比原来的操作数个数要少一个(后置的++—除外);当重载为非成员函数时, 参数个数与原操作数个数相同. 原因是: 第一个操作数(第一个形参)就是对象本身, 它仅以this指针的形式隐式存在与参数表中.

     

    #include <iostream>

    using namespace std;

     

    class Book

    {

    public:

        Book (int page)

        {

            bk_page=page;

        }

        void display()

        {

            cout << bk_page << endl;

        }

        int bk_page;    //在类体外重载运算符, 此时bk_page变量的属性不能是私有或protected

    };

     

    Book operator+(Book bk_1, Book bk_2) //不能写成(Book x,y)

    {

        return bk_1.bk_page+bk_2.bk_page; //在类体外重载运算符, "+"不是Book类的重载运算符

    }

     

    void main()

    {

        Book bk1(10);

        Book bk2(20);

        Book tmp(0);

        tmp= bk1+bk2; //bk1+bk2的结果是Book类类型的, 所以这个结果只能赋给Book型的tmp对象

        tmp.display();

    }

     

    //通过运算符的重载实现对象和普通类型变量的相加

    #include <iostream.h>

     

    class Add

    {

    public:

        int m_Operand;

        Add()    //构造函数

        {

            m_Operand=0;

        }

        Add(int value)    //重载构造函数

        {

            m_Operand=value;

        }

     

    };

     

    Add operator+(Add a, int b)    //在类体外声明重载运算符, 此时运算符"+"不是Book类的成员函数

    {

        Add sum;

        sum.m_Operand=a. m_Operand +b;

        return sum;

    }

     

     

    void main()

    {

        Add a(5),b;

        b=a+8;

        cout<<"the sum is: "<<b.m_Operand<<endl;

    }

     

     

    ● 重载/过载(overload) & 重写/覆盖(override) & 隐藏(hide)/重定义(redefine)

    重载与重写都与C++语言的多态性有关, 即不同功能的函数可以用同一个函数名.

    下面是几个版本的比较:

     

    版本一:

    一、重载(overload

    指函数名相同,但是它的参数个数/顺序/类型不同, 但是不能靠返回类型来判断某函数是否为重载函数

    1)相同的范围(在同一个作用域中) ;

    2)函数名相同;

    3)参数不同;

    4virtual 关键字可有可无

    5)返回值可以相同或不同, 但参数个数/顺序/类型必须不同

     

    二、重写(也称为覆盖 override

    是指派生类重新定义基类的虚函数,特征是:

    ※ 虚函数:

    ① 概念: virtual关键字修饰的成员函数

    ② 格式: virtual 函数返回类型 函数名(参数表){函数体}

    ③ 实现多态性: 将父类指针或引用指向派生类对象, 从而访问派生类中同名成员函数.

    1)不在同一个作用域(分别位于派生类与基类) ;

    2)函数名相同;

    3)参数相同;

    4)基类函数必须有 virtual 关键字,不能有 static; 子类函数可加也可不加virtual, 但为了保险起见, 最好加virtual关键字.

    另外: 一个中将所有的成员函数都尽可能地设置为虚函数总是有益的。(钱能, C++程序设计教程)

    常见的不能声明为虚函数的有:普通函数(非成员函数)、静态成员函数、内联成员函数、构造函数、友元函数。

    5)返回值相同(或是协变),否则报错;

    6)重写函数的访问修饰符可以不同。如果 virtual private 的,派生类中重写改写为 public,protected 也是可以的

     

     

     

    三、重定义(也成隐藏)

    1)不在同一个作用域(分别位于派生类与基类) ;

    2)函数名相同;

    3)返回值可以不同;

    4)如果参数不同, 那么不论有没有 virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆) 。

    5)参数相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆) 。

     

    版本二:

    父类是virtual方法 形参表相同 ---> 构成重写

    父类是virtual方法 形参表不同 ---> 隐藏父类方法

    父类不是virtual方法 形参表相同 --->隐藏父类方法

    父类不是virtual方法 形参表不同 --->隐藏父类方法

     

    版本三

    从函数的实际调用来理解三者的区别:

    ①函数重载:同一作用域,寻找适当函数的过程

    ②函数重写:用于父类与子类之间的虚函数, 虚函数是通过关键字virtual来实现的,在具体的实现时父类函数的指针会被子类相同的函数指针所覆盖,所以才称为override

    ③函数重定义(个人觉得称为隐藏更恰当):函数调用时,在两个不同作用域(大的作用域包含小的作用域情况下),在小的作用域中寻找到合适的函数后直接调用,不用再在大的作用域中搜索,故可以称为隐藏.

     

    ● 隐藏与重写的区别

    隐藏的案例

    #include <iostream>

    using namespace std;

     

    class A

    {

    public:

        void print()

        {

            cout<<"This is A"<<endl;

        }

    };

     

    class B:public A

    {

    public:

        void print()

        {

            cout<<"This is B"<<endl;

        }

    };

     

    int main()

    {

        A a;

        B b;

        a.print();

        b.print();

        b.A::print();    //使用派生类的对象访问基类中被派生类隐藏了的函数或变量

    }

     

    上面的案例并不是多态性的实现.

    多态性的有一个关键:

    我们应该用指向基类的指针或引用来操作基类或派生类的对象:

    #include <iostream>

    using namespace std;

     

    class A

    {

    public:

        virtual void print()

        {

            cout<<"This is A"<<endl; //现在成了虚函数了

        }

    };

     

    class B:public A

    {

    public:

        void print()    //子类虚函数的virtual可以省略, 不过最好写上, 使程序更加清晰

        {

            cout<<"This is B"<<endl;

        }

    };

     

    int main()

    {

        A a;

        B b;

        A* p1=&a; //p1基类A指针, 指向派生类对象a

        A* p2=&b; //p2也是基类A指针, 指向派生类对象b

        p1->print();

        p2->print();

        p2->A::print(); //使用派生类的对象访问基类中被派生类隐藏了的函数或变量(比较Python中的super()方法)

    }

     

    如果不用基类指针, 那么在基类中定义的虚函数就会被子类的同名函数隐藏, 与第一个隐藏案例的结果一样, 也就是说, 如果不用基类指针, 这个虚函数的定义没有用武之地

    #include <iostream>

    using namespace std;

     

    class A

    {

    public:

        virtual void print()

        {

            cout<<"This is A"<<endl; //现在成了虚函数了

        }

    };

     

    class B:public A

    {

    public:

        void print()

        {

            cout<<"This is B"<<endl;

        }

    };

     

    int main()

    {

        A a;

        B b;

        a.print();

        b.print();

    }

     

    如果不定义虚函数, 但还是硬要使用指向基类的指针或引用来操作基类或派生类的对象, 那么就算基类A的指针p2明明指向的是派生类B的对象b, 它调用的还是基类Aprint()函数

    #include <iostream>

    using namespace std;

     

    class A

    {

    public:

        void print()

        {

            cout<<"This is A"<<endl;

        }

    };

     

    class B:public A

    {

    public:

        void print()

        {

            cout<<"This is B"<<endl;

        }

    };

     

    int main()

    {

        A a;

        B b;

        A* p1=&a; //p1基类A指针, 指向派生类对象a

        A* p2=&b; //p2也是基类A指针, 指向派生类对象b

        p1->print();

        p2->print();

    }

     

    ● 再一个虚函数案例

    #include <iostream>

    using namespace std;

     

    class Animal

    {

    public:

        virtual void Breathe()

        {

            cout<<"Breathe with a kind of organ"<<endl;

        }

    };

     

    class Mammal:public Animal

    {

    public:

        void Breathe()

        {

            cout<<"Breathe with lung"<<endl;

        }

    };

     

    class Fish:public Animal

    {

    public:

        void Breathe()

        {

            cout<<"Breathe with gill"<<endl;

        }

    };

     

    void main()

    {

        Animal animal1;

        Mammal mammal1;

        Fish fish1;

        Animal *p1=&animal1;

        Animal *p2=&mammal1;

        Animal *p3=&fish1;

        p1->Breathe();

        p2->Breathe();

        p3->Breathe();

    }

     

    ● 多重继承 (multiple inheritance) & 二义性(ambiguity) & 虚继承(virtual inheritance)

    多重继承: 

    子类从多个父类继承

    二义性: 有两种情况

    情况1:

    当派生类Derived的对象obj访问fun()函数时, 由于无法确定是访问基类Base1中的fun()函数, 还是Base2中的fun()函数, 如下面的a图所示;

    情况2:

    当一个派生类(Derived2)从多个基类派生(Derived11类和Derived12), 而这些基类又有一个共同的基类(Base), 当对该基类中说明的成员进行访问时,可能出现二义性, 如下面的b图所示;

     

    • 解决二义性的方法: ① 使用作用域运算符; ② 使用同名覆盖(函数隐藏)的原则; ③虚继承

    使用作用域解析运算符进行限定的一般格式:

    <对象名>.<基类名>::<数据数据>

    <对象名>.<基类名>::<数据成员>(<参数表>)

    例如: obj.Base1::fun()    //调用Base1的函数

    obj.Base2::fun()    //调用Base2的函数

    虚继承:在继承定义中包含了virtual关键字的继承关系;

    虚基类:被虚继承的基类(不是包含虚函数或纯虚函数的基类)

    //虚继承是为了解决上面的第二种二义性问题; 例如, A类是B类和C类的父类, B类和C类是D类的父类, D类中将存在两个A类的复制, 那么如何在D类中使其只存在一个A类呢.

    #include <iostream>

    using namespace std;

     

    class Animal                                //定义一个动物类

    {

    public:

        Animal()                                //定义构造函数

        {

            cout << "动物类被构造"<< endl;                

        }

        void Move()                            //定义成员函数

        {

            cout << "动物能运动"<< endl;                

        }

    };

     

    class Bird : virtual public Animal            //Animal类虚继承Bird

    {

    public:

        Bird()                                //定义构造函数

        {

            cout << "鸟类被构造"<< endl;                

        }

    void Fly()                            //定义成员函数

        {

            cout << "鸟能飞翔"<< endl;        

        }

        void Breath()                            //定义成员函数

        {

            cout << "鸟能呼吸"<< endl;                //输出信息

        }

    };

    class Fish: virtual public Animal            //CAnimal类虚继承CFish

    {

    public:

        Fish()                                //定义构造函数

        {

            cout << "鱼类被构造"<< endl;                

        }

        void Swim()                        //定义成员函数

        {

            cout << "鱼能游"<< endl;            

        }

        void Breathe()                            //定义成员函数

        {

            cout << "鱼能呼吸"<< endl;                //输出信息

        }

    };

    class WaterBird: public Bird, public Fish //多重继承, BirdFish类派生子类WaterBird

    {

    public:

        WaterBird()                            //定义构造函数

        {

            cout << "水鸟类被构造"<< endl;            

        }

    void Action()                            //定义成员函数

        {

            cout << "水鸟能飞又能游"<< endl;        

        }

    };

     

    int main()    

    {

        WaterBird waterbird;    //定义水鸟对象

    }

     

    ● 声明纯虚函数的形式为

    声明纯虚函数的形式为:

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

     

    抽象类:

    ① 包含有纯虚函数(pure virtual function)的类称为抽象类, 一个抽象类至少有一个纯虚函数

    ② 抽象类可以作为基类派生出新的子类, 但抽象类的纯虚函数不可以被继承

    1. 抽象类不能在程序中被实例化为对象, 但是可以使用指向抽象类的指针
    2. 在抽象类的派生类中, 我们必须给出基类中纯虚函数的定义, 或在该派生类中再声明其为纯虚函数

     

    抽象类的意义: 在开发程序的过程中, 并不是所有代码都是由软件构造师自己写的, 有时需要调用库函数(很多库函数的功能可以自己写代码实现, 但很麻烦), 有时候分给别人写. 一名软件设计师可以通过纯虚函数建立接口, 然后让程序员填写代码实现接口, 而自己主要负责建立抽象类.

    #include <iostream>

    using namespace std;

    const double PI=3.14;

     

    class Figure    //基类, 一个抽象类

    {

    public:

        virtual double GetArea() =0;    //纯虚函数

    };

    ////////////////////////////////

    class Circle : public Figure    //派生类Circle

    {

    private:

        double radius;

    public:

        Circle(double x)

        {

            radius=x;

        }

        double GetArea()    //实现抽象类的成员函数

        {

            return radius*radius*PI;

        }

    };

    ////////////////////////////////

    class Rectangle : public Figure    //派生类Rectangle

    {

    protected:

        double height,width;

    public:

        Rectangle(double x,double y)

        {

            height=x;

            width=y;

        }

        double GetArea()    //实现抽象类的成员函数

        {

            return height*width;

        }

    };

    ////////////////////////////////

    void main()

    {

        Figure *fg1;    //声明一个抽象基类的指针, 目的是用基类指针来访问基类和派生类的同名函数

        fg1= new Rectangle(4.0,5.0);    //动态构造一个子类, 即Rectangle类型的对象(动态对象), 然后将基类, 即Figure类型的指针指向该动态对象, 这样就可以实现c++中的动态绑定功能. 因为基类Figure中一个成员函数是virtual在子类Rectangle中又重载了该函数,那么通过Figure会调用Rectangle中的函数.

        cout << fg1->GetArea() << endl;    //根据动态绑定的内容, 就可以知道调用那些成员函数来实现

        delete fg1;

        fg1=NULL;

        Figure *fg2;

        fg2= new Circle(4.0);

        cout << fg2->GetArea() << endl;

        delete fg2;

    }

     

  • 相关阅读:
    Mac OS X上安装配置apache服务器
    eclipse 发布web工程,修改tomcat端口
    masonry瀑布流的使用
    fullcalendar 使用教程
    -webkit-line-clamp下多行文字溢出点点点...显示实例页面
    HTML5 Audio标签方法和函数API介绍
    Canvas与Image互相转换
    iOS绘图UIBezierPath 和 Core Graphics框架
    实现外卖选餐时两级 tableView 联动效果
    零行代码为 App 添加异常加载占位图
  • 原文地址:https://www.cnblogs.com/ArrozZhu/p/8378015.html
Copyright © 2011-2022 走看看