前言
我对抽象基类(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; }
- 可以把抽象基类看作一个模型,而这个模型严格规定了子类的接口设计。毕竟,程序员之间的信任度一直以来都是值得被探讨的话题。
还不快抢沙发