抽象基类(ABC)

C++·语法 2019-02-04 4681 字 2476 浏览 点赞

前言

我对抽象基类(Abstract Base Class,ABC)的理解在两个点上。第一点是抽象,即高度抽象;第二点是基类,对抽象基类来说,它的存在只是为了作为基类(区别于具体类),不可以被实例化

高度抽象一词听起来本身就很“抽象”,不妨让我用“总结”这个词进行通俗解释。比方这里有两类人,一类只是聋人,另一类只是盲人。现在我们需要对这两类人抽象,也就是需要总结两类人的相同点+不同点,然后付诸于代码。为方便起见,这里就列举三个行为:散步为相同点,看得见与看不见为不同点,听得见与听不见为不同点——这样一个过程,就是在“抽象”。(好吧,这般解释可能还是抽象)

如何实现ABC

在C++中,virtual关键字使得函数有虚函数非虚函数之分,显然,virtual的存在让一个函数成为虚函数。而虚函数中,又分纯虚函数和非纯虚函数:

virtual void hear() = 0;  // 纯虚函数

virtual void walk();  // 非纯虚函数(常规虚函数)

在一个类中,如果至少有一个纯虚函数,那么认为这个类是抽象基类。

class DeafPerson {
public:
    // ...
    virtual void walk() = 0;
    // ...
};
// ...

如此一来,DeafPerson就是一个抽象基类了。

为什么需要ABC

我们都知道,无论盲人还是聋人,他们都是人,他们都可以散步,但是不一定有视觉或听觉的能力。我们当然可以做以下设计:

class DeafPerson {
public:
    string name;  // 名字
    void walk() { cout << "is able to walk" << endl; }
    void hear() { cout << "is not able to hear" << endl; }
    void see() { cout << "is able to see" << endl; }
};

class BlindPerson {
public:
    string name;
    void walk() { cout << "is able to walk" << endl; }
    void hear() { cout << "is able to hear" << endl; }
    void see() { cout << "is not able to see" << endl; }
};

显然,由于我们只找了一个相同行为,所以只重复了walk()函数,但倘若在我们的需求里有必要用到多个相同行为,比如吃东西、聊天、穿衣……如果仍用以上方式,那对每个类都不得不定义一个eat()函数、chat()函数、wear()函数……为了偷懒,我们很快想到再抽象一个Person类出来,用继承的方式实现代码复用,像下面那样:

class Person {
public:
    string name;
    Person(string name):name(name) {}
    /* 共有的能力 */
    void walk() { cout << name << " is able to walk" << endl; }
    void  eat() { cout << name << " is able to eat" << endl; }
    void  wear() { cout << name << " is able to wear" << endl; }
    /* 可能需要重写的部分 */
    void hear() { cout << name << " is able to hear" << endl; }
    void see() { cout << name << " is able to see" << endl; }
};

class DeafPerson: public Person {
public:
    DeafPerson(string name): Person(name) {}  /* 使用父类的构造函数 */
    void hear() { cout  << name << " is not able to hear" << endl; }
};

class BlindPerson: public Person {
public:
    BlindPerson(string name): Person(name) {};
    void see() { cout << name << " is not able to see" << endl; }
};

int main() {
    DeafPerson deafPerson("lily");
    deafPerson.walk();
    deafPerson.hear();

    BlindPerson blindPerson("boby");
    blindPerson.eat();
    blindPerson.see();
}

// 输出:
lily is able to walk
lily is not able to hear
boby is able to eat
boby is not able to see

可这样做的缺点是,也许设计Person类和DeafPerson类是两个人,两人之间却不曾约定,那么设计DeafPerson时存在忘记重写hear()方法的可能。所以现在得有个规定,如果子类中没有重写父类中的某个方法,就不允许子类实例化,这样一来,设计DeafPerson的人就不得不重写必需方法了。

Person类设计如下:

class Person {
public:
    string name;
    Person(string name):name(name) {}
    void walk() { cout << name << " is able to walk" << endl; }
    void  eat() { cout << name << " is able to eat" << endl; }
    void  wear() { cout << name << " is able to wear" << endl; }
    
    /* 必需重写的部分 */
    virtual void hear() = 0;  /* 必需重写的函数设计为 纯虚函数 */
    virtual void see() = 0;  /* 必需重写的函数设计为 纯虚函数 */
};

倘若DeafPerson继承Person后却不重写hear()方法和see()方法,对它实例化,编译器就会报错:

D:\MyCode\CC++\ABS_Study\main.cpp(37): error C2259: “DeafPerson”: 不能实例化抽象类
D:\MyCode\CC++\ABS_Study\main.cpp(37): note: 由于下列成员:
D:\MyCode\CC++\ABS_Study\main.cpp(37): note: “void Person::hear(void)”: 是抽象的
D:\MyCode\CC++\ABS_Study\main.cpp(16): note: 参见“Person::hear”的声明
D:\MyCode\CC++\ABS_Study\main.cpp(37): note: “void Person::see(void)”: 是抽象的
D:\MyCode\CC++\ABS_Study\main.cpp(17): note: 参见“Person::see”的声明

道理很简单,抽象基类不可以实例化,而子类继承父类之后没有重写父类中的纯虚函数,对于子类来说,它也拥有了这些纯虚函数,所以子类也是抽象基类,因此也不能实例化。按规定,要在子类中用常规虚函数实现这些接口,像这样:

class DeafPerson: public Person {
public:
    DeafPerson(string name): Person(name) {}
    virtual void hear() { cout  << name << " is not able to hear" << endl; }
    virtual void see() { cout << name << " is able to see" << endl; }
};

使用抽象基类的另一好处是,因为子类们继承自同一父类,用父类类型的指针或引用进行管理可以简化代码。

Person *deaf, *blind;  /* 基类类型指针 */

deaf = new DeafPerson("lily");
deaf->walk();
deaf->hear();

blind = new BlindPerson("boby");
blind->eat();
blind->see();
void print_name(Person& person)  /* 基类引用 */
{
    cout << person.name << endl;
}

int main() 
{
    DeafPerson deafPerson("lily");
    BlindPerson blindPerson("boby");

    print_name(deafPerson);
    print_name(blindPerson);
}

// 输出:
lily
boby

其他

  • 抽象基类的纯虚函数仍然可以定义(尽管我暂时不知道这样做的意义):
void Person::hear() { cout  << name << " is able to hear" << endl; }
void Person::see() { cout << name << " is able to see" << endl; }
  • 可以把抽象基类看作一个模型,而这个模型严格规定了子类的接口设计。毕竟,程序员之间的信任度一直以来都是值得被探讨的话题。


本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论