代码改变世界

白话C++系列(26) -- 接口类

2016-06-20 20:27  Keiven_LY  阅读(860)  评论(0编辑  收藏  举报

接口类

问题:什么是接口类??

如果在一个抽象类中,仅含有纯虚函数,而不含有其他的任何东西,那么我们就称之为接口类。即:仅含有纯虚函数的类称为接口类

那么,我们如何理解接口类的定义呢?

也就是说,在类当中,没有任何的数据成员,只有成员函数,而这仅有的成员函数当中,其又都是纯虚函数,此时,我们就把这样的类称之为接口类。

下面通过一个例子来说明接口类的定义方法。如下:

Shape这个类在之前的课程中已经讲过,此时,如果我们将Shape类中的计算面积和计算周长这两个成员函数都定义成纯虚函数,并且Shape类此时还不含有别的成员函数以及数据成员,那么,此时我们就称Shape这个类为接口类。

在实际的使用过程中,接口类更多的是用来表达一种能力或协议。这句话又该如何理解呢?我们还是通过一个例子来进行说明。

比如,我们有上面的一个类,这个类的意思就是“会飞的”。如果我们要是有会飞这种能力,那么我们就应该事先以下两个函数:起飞和降落。在这里,我们可以看到,起飞和降落这两个函数都是虚函数,那么继承Flyable这个类的子类就必须要实现在Flyable这个类当中所定义的起飞和降落这两个纯虚函数,实现之后,它就具有了“会飞”这种能力。我们来看一下:如果我们定义了鸟(Bird)这个类,并且Bird去继承Flyable这个类(如下)。

当形成了这种继承关系之后,如果我们要实例化Bird,那么我们就必须要在Bird这个类当中去实现起飞和降落这两个函数(如上面省略号表示的函数体)。大家可以想一想,如果我们在使用的时候,有如下一个函数flyMatch()。

我们看到,flyMatch这个函数所要求传入的指针是“会飞”的,也就是说,任何会飞的对象的指针都可以传入进来。Bird这个类实现了Flyable,即Bird是一个子类。前面我们讲过,当我们用一个子类去继承父类的时候,就形成了一种is-a的关系。当形成了这种is-a的关系之后,我们就可以在flyMatch,也就是飞行比赛当中传入两个指针,这两个指针要求传入的类只要是Flyable的子类就可以了。那么这个时候,我们知道Bird是Flyable的子类,那么在flyMatch中就可以传入Bird类的对象指针。传入进来的对象指针就可以调用Flyable类中所要求必须实现的起飞和降落这两个函数了。这个时候,大家应该隐隐的感觉到,其实Flyable这个类就相当于是一种协议,你如果想要参加飞行比赛,那么你就一定要会飞;那么如果你会飞,你一定实现了起飞和降落这两个函数;那么你实现了这两个函数,那么我们就可以再flyMatch(飞行比赛)中去调用了。同样的道理,如果我们有如下一个类,这个类叫做CanShot(能够射击,即一种具有射击的能力)。

在这个类当中,我们定义了两个纯虚函数:瞄准和装弹。此时,如果我们再定义一个Plane(飞机类),飞机可以进行多继承,其继承了Flyable(会飞的)和CanShot(可以射击的)这两个类,如下所示:

那么这个时候,想要实例化Plane,那么,它就要必须实现Flyable中的起飞(takeoff)和降落(land)以及CanShot中的瞄准(aim)和装弹(reload)。如果我们把这些都实现了,那么,假设我们有如下一个函数fight(战斗)。战斗的时候,我们要求,只需要具有能够射击这种能力就可以了,如下所示:

那么,作为Plane(飞机)这个二类来说,它既是Flyable的子类也是CanShot(射击)这个类的子类。Fight这个函数要求只要是CanShot的子类就可以了。如果我们此时传入Plane这个类的对象指针给fight,其就是满足fight函数参数要求的。那么传入进来的对象指针必定是CanShot这个类的对象指针,呢么它就一定实现了瞄准和装弹这两个函数,实现之后,我们就可以再fight战术中去调用者两个函数了。

对于接口类来说,更为复杂的情况如下所示:

当我们定义一个Plane(飞机)这样的一个类的时候,对于飞机来说,他一定是能够会飞的,所以我们继承Flyable这个类,这样飞机就有了好“会飞”的能力。如果想要去实例化飞机,,那么此时我们就必须要实现起飞和降落这两个函数。而战斗机是可以继承飞机的,同时战斗机还应该具有射击的能力,这个时候它也是一种多继承,如下所示:

这种多继承请大家注意:它的第一个父类(Plane)并不是一个接口类,它的第二个父类(CanShot)则是一个接口类。这种情况下,我们从逻辑上可以理解为:战斗机是继承了飞机的绝大部分属性,同时还具有能够射击这样的功能,那么我们就需要在战斗机中去实现CanShot这个类当中的瞄准和装弹这两个函数。实现完成之后,如果我们有一个函数airBattle(空战),而空战的时候就需要传入两个战斗机的对象指针,如下所示:

因为此时我们传入的是战斗机的对象指针,那么战斗机对象当中一定实现了CanShot中瞄准和装弹这两个函数,同时,也肯定实现了Flyable中的起飞和降落这两个函数,于是我们就可以放心地在airBattle(空战)这个函数中去调用Flyable和CanShot所约定的函数了。

接口类代码实践

题目描述:

/*  ********************************************   */

/*  接口类

         1. Flyable类,成员函数:takeoff(起飞)、land(降落)

         2. Plane类,成员函数:takeoff、land、printCode,数据成员:m_strCode

         3. FighterPlane类,成员函数:构造函数、takeoff、land

         4. 全局函数flyMatch(Flyable *f1, Flyable *f2)

*/

/*  ********************************************   */

程序框架:

 

头文件(Flyable.h

#ifndef FLYABLE_H
#define FLYABLE_H

//Flyable只含有纯虚函数,没有其他的成员函数,也没有任何的数据成员,
//所以不需要.cpp文件,这样的类就称之为接口类
class Flyable 
{
public:
    virtual void takeoff() = 0;
    virtual void land() = 0;
};

#endif

头文件(Plane.h

#ifndef PLANE_H
#define PLANE_H

#include <string>
#include "Flyable.h"
using namespace std;

class Plane:public Flyable
{
public:
    Plane(string code);
    virtual void takeoff();
    virtual void land();
    void printCode();
private:
    string m_strCode;
};

#endif

源程序(Plane.cpp

#include <iostream>
#include "Plane.h"

using namespace std;

Plane::Plane(string code)
{
    m_strCode = code;
}
void Plane::takeoff()
{
    cout << "Plane --> takeoff()" << endl;
}
void Plane::land()
{
    cout << "Plane --> land()" << endl;
}
void Plane::printCode()
{
    cout << m_strCode << endl;
}

头文件(FighterPlane.h

#ifndef FIGHTERPLANE_H
#define FIGHTERPLANE_H

#include "Plane.h"
#include <string>
using namespace std;

class FighterPlane:public Plane
{
public:
    FighterPlane(string code);
    virtual void takeoff();
    virtual void land();
};
#endif

源程序(FighterPlane.cpp

#include <iostream>
#include "FighterPlane.h"

using namespace std;

FighterPlane::FighterPlane(string code):Plane(code)
{
}
void FighterPlane::takeoff()
{
    cout << "FighterPlane --> takeoff()" << endl;
}
void FighterPlane::land()
{
    cout << "FighterPlane --> land()" << endl;
}

主调程序(demo.cpp

#include <iostream>
#include "stdlib.h"
#include <string>
#include "FighterPlane.h"

using namespace std;

void flyMatch(Flyable *f1, Flyable *f2)
{
    f1->takeoff();
    f1->land();
    f2->takeoff();
    f2->land();
}

int main()
{
    Plane p1("001");
    Plane p2("002");
    p1.printCode();
    p2.printCode();
    flyMatch(&p1, &p2);

    system("pause");
    return 0;
}

我们来看一看运行结果(按F5)

从运行结果,我们可以看到,首先打印出来两行”001”和”002”,这个毫无疑问是通过printCode函数打印出来的;接下来打印出来的四行,分别是f1的起飞和降落,f2的起飞和降落,这个也说明了Plane可以正确的作为参数传递给flyMatch。通过打印结果,我们可以看到,对于flyMatch这个函数来说,它相当于限制了传入参数的参数类型,并且可以在函数体中放心地去调用接口类当中所定义的纯虚函数,这个就是接口类最为常见的用法。接下来我们使用FighterPlane这个类来试一试,看看其能不能作为参数传入到flyMatch当中,修改main函数如下:

int main()
{
    FighterPlane p1("001");
    FighterPlane p2("002");
    p1.printCode();
    p2.printCode();
    flyMatch(&p1, &p2);

    system("pause");
    return 0;
}

我们来看一看运行结果:

通过运行效果,我们可以看到,打印飞机编号跟之前是一样的,但后面四行是不一样的。我们可以看到,通过f1和f2调用起飞和降落函数呢,实际上调用的是FighterPlane(战斗机)这个类当中的起飞和降落函数。此外,我们再做一个小小的修改:要求在FighterPlane中不仅要继承Plane这个类,而且还要继承Flyable这个类,同时,不让Plane继承Flyable这个类(注意此时就要去掉之前实现的纯虚函数takeoff和land)。这个时候,我们发现此时是一个多继承:FighterPlane既继承了Plane这个类,也继承了Flyable这个类。在这种情况下,对于FighterPlane来说,它就有了两个父类,这就意味着:FighterPlane既是一个Plane,也是一个Flyable。

修改后的头文件(Plane.h

#ifndef PLANE_H
#define PLANE_H

#include <string>
#include "Flyable.h"
using namespace std;

class Plane
{
public:
    Plane(string code); 
    void printCode(); 
private:
    string m_strCode;
};

#endif

修改后的源程序(Plane.cpp)

#include <iostream>
#include "Plane.h"

using namespace std;

Plane::Plane(string code)
{
    m_strCode = code;
}

void Plane::printCode()
{
    cout << m_strCode << endl;
}

修改后的头文件(FighterPlane.h

#ifndef FIGHTERPLANE_H
#define FIGHTERPLANE_H

#include "Plane.h"
#include <string>
using namespace std;

class FighterPlane:public Plane,public Flyable
{
public:
    FighterPlane(string code);
    virtual void takeoff();
    virtual void land();
};
#endif

main函数不变,如下:

int main()
{
    FighterPlane p1("001");
    FighterPlane p2("002");
    p1.printCode();
    p2.printCode();
    flyMatch(&p1, &p2);

    system("pause");
    return 0;
}

此时我们来看一看运行结果:

从运行结果可以看到跟之前一样,但是意义却不同了。此时的FighterPlane既继承了Plane这个类,也继承了Flyable这个类。这就意味着,如果有另外一个函数,要求传入的是Plane而不是FighterPlane,如下:

void flyMatch(Plane *f1, Plane *f2)
{
    f1->printCode();
    f2->printCode();    
}

而我们在main函数中:

int main()
{
    FighterPlane p1("001");
    FighterPlane p2("002");

    flyMatch(&p1, &p2);

    system("pause");
    return 0;
}

我们实例化的是FighterPlane,但在flyMatch这个函数中,我们仍然传入的是FighterPlane(p1和p2),这样写是合法的,因为flyMatch要求传入的类是FighterPlane的父类,所以这样写是合法的。此时,我们按F5来看一看运行的结果如何:

我们可以看到,打印出来的结果只有两行”001”和”002”。通过这两个实验,我们可以更进一步的体会到接口类给我们带来的好处,以及多继承给编程带来的灵活性。