题6-什么是纯虚函数?
C++中有两种继承:接口继承和实现继承。二者的第一个区别是:基类都干了些什么?
- 如果基类啥实事不干,只是提出了:目标、要求,即提出了 “希望有”的东西,而不真正实现,那么继承这个“不干实事”的基类的,就叫接口继承。
- 反过来,如果基类好歹干了点事,提供了某些功能的实现(哪怕是个半成品),那么继承它,就叫实现继承。
放在现实生活中,“接口继承”大概类似于“继承遗志”,而“实现继承”相当于“继承遗产”。
- 前者:一个父亲,自己没有读大学,但希望儿女们实现读清华北大的目标;一个父亲,自己一分存款没有,但希望自己女儿非富即贵,这样的父亲(基类)的儿女(派生类),自然只能“继承遗志”。
- 后者:一个拥有两家上市企业,五十套一线城市房产,银行存款15个亿……他的儿女将来可以继承遗产。
如果某个基类,在某个功能上,一点都没有实现,但又希望派生类能实现,这个功能就可以使用一个只声明规格(只提要求),不提供任何实现的成员函数来表达,这个成员函数就叫 “纯虚函数 / pure virtual function”。
比如,有个“父亲”类,在发财这件事上的实现度完全是零(幸好不是负数),而在学识这件事上,实现了部分高中文化,那么这个类的设计就是:
class Father
{
public:
// 财富
virtual void Fortune() = 0; // 纯虚函数
// 学识
virtual void Learning()
{
cout << "清华池高中肄业";
}
};
成员函数 “Fortune()” 被标记上“virtual”,同时还标记为 “ = 0 ”,在C++当中,这就是表明它是一个“纯虚函数”。语义是:本基类完全不知道如何提供 “财富()” 的实现,派生类们,你们好好努力,一定要好好实现啊!
请大家对比一下 Fortune 和 Learning。
题7-什么是抽象类?
一个类(结构也一样,下同),只要拥有一个纯虚函数,这个类就是 “抽象类 (Abstract class)”。抽象类没办法创建出真实的对象,比如:
Father f; // 失败,因为 Father::Fortune() 是纯虚函数
如果一个类所有成员函数都是纯虚函数,那么这个类我们通常也称为“接口 / interface”。比如:
class Target
{
public:
virtual void Fly() = 0; // 会飞
virtual void Cry() = 0; // 会哭
virtual void Say() = 0; // 会说
};
因此,所谓的“接口”,其实就是自己一点实事不干,却给未来的后代(派生类)们提了一堆要求的家伙……
以抽象类为基类的派生类怎么办呢?可以一个一个虚函数都实现,也可以挑选某些虚函数加以实现,另外的一些暂不实现,留给派生类的派生类(一代一代继承遗志啊!)。实现在不爽,也可以一个都不实现,然后还再添加几个新的纯虚函数……
因此,关于抽象类的更准确的定义是:一个类,不管是自己加的,还是是继承而来,只要拥有一个纯虚函数,就是抽象类。
题8-纯虚函数可以提供实现吗?有什么意义?
会问这个问题的考官,八成有些 “刁”……
没错,一个虚函数确实可以在被声明为 “ = 0 ” 的同时,又提供了具体实现,比如:
class Father
{
public:
virtual void Fortune() = 0 ;
};
void Father::Fortune() // 正确,可行
{
cout << "孩子,其实咱们家有前清留下的两亿银票";
}
注意,实现必须在类外,不能在声明纯虚成员函数时直接加上定义——我也不知道为什么,大概是那样写实现有点过份“造假”——刚说 “ = 0”,马上就有了实现?
class Father
{
public:
virtual void Fortune() = 0 // 错误,不可行
{
cout << "孩子,其实咱们家有前清留下的两亿银票" ;
}
};
为一个纯虚类提供实现,有什么意义和作用吗?
答:几乎没有意义,为纯虚函数提供定义,改不了当前类被视为抽象类的局面。当前类仍然无法实例化。不过,一个有定义(实现)的纯虚函数确实可以被调用。比如:
class Father
{
public:
virtual void Fortune() = 0 ;
};
void Father::Fortune() // 正确,可行
{
cout << "孩子,其实咱们家有前清留下的两亿银票";
}
class Son : public Father
{
void Fortune() override
{
Father::Fortune(); // 调用基类的纯虚函数!!!
cout << "\n真的吗?\n" << endl;
}
};
如果没有提供纯虚函数 “Father::Fortune()” 的实际定义,以上在代码要么在编译时出错(本例),要么在运行时出错(更复杂的情况下)。出错位置就在注释为 “调用基类的纯虚函数!!!”那行。
在一个复杂的系统里,编译器有时候无法检测出代码调用了一个没有实现的纯虚函数,这就会造成运行期错误,以至程序意外退出。所以,为纯虚函数提供实现(哪怕是空实现,或者输出错误信息),可以有效躲开这种运行期错误——但是,这种做法事实上也隐藏了程序可能的设计错误或实现错误,所以它被视为是一种实用的,但不一定是好的做法。
为纯虚的析构函数提供默认的实现(通常就是空实现),倒是一个被写上书的好做法。具体我们在后续 “虚析构函数”时再做讲解。