十八. 继承和多态

● 继承的概念

继承(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;

}

 

 posted on 2018-01-29 16:28  Arroz  阅读(320)  评论(0编辑  收藏  举报