抽象类

在面向对象编程中,有时将类在功能上分为两种,一种叫“具体类”,另一种叫“抽象类”。所谓“具体类”是指在程序运行当中会使用这种类的若干个实例,即在某个地方、某些时刻会创建这些类型的对象。反之,“抽象类”就是不会创建任何一个实例的类。为了演示这种分类,我们将之前的例子完善一下:

#include <iostream>
struct Animal {
    virtual ~Animal() { } // 令 Animal 为多态类型
};
struct Cat : public Animal {
    void meow() const {
        std::cout << "meow~" << std::endl;
    }
};
struct Dog : public Animal {
    void bark() const {
        std::cout << "woof!" << std::endl;
    }
};

void tryBark(const Animal* a) {
    if (typeid(*a) == typeid(Dog)) {
        const Dog* dog{static_cast<const Dog*>(a)};
        dog->bark();
    }
}

int main() {
    Animal* pet{nullptr};
    std::cout << "If you want a cat, press 1." << std::endl;
    int n;
    std::cin >> n;
    if (n == 1) {
        pet = new Cat{};
    } else {
        pet = new Dog{};
    }
    tryBark(pet);
    delete pet;
}

我删去了不必要的 getName,并令 Animal 的析构函数为虚使得其为多态类型,从而 tryBark 能够正常运行。这个程序中,如果你输入 1,则 tryBark 不做任何事;否则输出 woof!

上面的程序创建了三个类 Animal CatDog,但在实际使用中我们只可能创建 CatDog 类的对象,而不可能仅创建一个 Animal 类型的对象。因此,按照上面的划分,CatDog 是“具体类”,相对地 Animal 就是“抽象类”。

C++ 提供了在语法层面的抽象类(Abstract class)。所谓“语法层面”就是指,如果尝试创建一个抽象类的实例,C++ 就会给出编译错误。这种语法设计可以减少编码时的错误,并有助于程序结构清晰。那么如何创建语法层面的抽象类呢?首先需要引入一个新概念——纯虚函数。

纯虚函数

纯虚函数(Pure virtual function)是一种带有特殊标记的虚函数。所谓特殊标记就是:

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

它与普通的虚函数多了一个被称为纯说明符的小尾巴 = 0。换句话说,只要给一个虚函数加上这个 = 0 就使得它变成了纯虚函数。不过由于语法限制,纯说明符和函数体无法同时出现,故纯虚函数不允许类内定义

而语法层面的抽象类就是指含有或继承了纯虚函数的类。因此,如果一个类带有纯虚函数,那么它成为抽象类,从而不能定义该类的任何对象。

下面我们将上文例子中的 Animal 定义为抽象类。我只需要将添加一个纯虚函数 act 即可。

#include <iostream>
struct Animal {
    virtual void act() const = 0; // 纯虚函数,使得 Animal 为抽象类
};

struct Cat : public Animal {
    void act() const { // 必须定义 Cat::act 来覆盖纯虚函数(见下文)
        std::cout << "It says: ";
        meow();
    }
    void meow() const {
        std::cout << "meow~" << std::endl;
    }
};

struct Dog : public Animal {
    void act() const { // 必须定义
        std::cout << "It says: ";
        bark();
    }
    void bark() const {
        std::cout << "woof!" << std::endl;
    }
};

int main() {
    // Animal a;                // 编译错误
    // Animal* p{new Animal{}}; // 编译错误
    Animal* pet{new Cat{}};     // OK

    // [...]
    // 调用虚函数的最终覆盖
    pet->act();
    delete pet;
}

我在 Animal 中添加了纯虚函数声明 Animal::act。这时,Animal 类成为抽象类,这个类的对象就不可能被构造出来。但由于继承的存在,Animal::act 会被继承到 CatDog 中去,而根据抽象类的概念,CatDog 就都变成抽象类了。这不好。所以每个具体的派生类都需要重写一个非虚的 act 成员来保证自己不是抽象的。

这也就是抽象类和纯虚函数的实际用途的体现。一般地,抽象类经常作为一个一般性概念而存在;它会包含若干个具体的派生类作为这个一般性感念的具体解释。而抽象类的纯虚函数则作为一个约束,要求其派生类必须实现这些函数的定义。

纯虚函数不能有类内定义,不过如果一个纯虚函数始终不会被调用,那么就无需给出它的定义。在刚才的例子中就从来没有调用过 Animal::act,故不用给出定义。但这不意味着纯虚函数不能有定义。纯虚函数可以在类外定义:下面给出了定义并调用 Animal::act 的版本:

#include <iostream>
struct Animal {
    virtual void act() const = 0; // 纯虚函数,不能类内定义,但……
};

// ……可以类外定义
void Animal::act() const {
    std::cout << "It says: ";
}

struct Cat : public Animal {
    void act() const { // 覆盖纯虚函数
        Animal::act(); // 调用基类的纯虚函数
        meow();
    }
    void meow() const {
        std::cout << "meow~" << std::endl;
    }
};

struct Dog : public Animal {
    void act() const { // 覆盖纯虚函数
        Animal::act();
        bark();
    }
    void bark() const {
        std::cout << "woof!" << std::endl;
    }
};

int main() {
    Animal* pet{nullptr};
    pet = new Cat{};

    // [...]
    // 调用虚函数的最终覆盖
    pet->act();
    delete pet;
}
最近更新:
代码未运行