加载中...
大厂题第1辑——虚函数七题精讲之3:纯虚函数
第1节:C++的“友元”是否会破坏类的封装?
第2节:大厂题第1辑——虚函数七题精讲之1:虚函数的作用
第3节:大厂题第1辑——虚函数七题精讲之2:虚函数的作用机制
第4节:大厂题第1辑——虚函数七题精讲之3:纯虚函数
第5节:大厂题第1辑——虚函数七题精讲之4:override 的作用
第6节:大厂题第1辑——虚函数七题精讲之5:虚析构(virtual destructor)
第7节:大厂题第1辑——虚函数七题精讲之6:构造、析构、虚函数
课文封面
  1. 什么是纯虚函数?
  2. 什么是抽象类?
  3. 纯虚函数可以提供实现吗?有什么意义?

题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()” 的实际定义,以上在代码要么在编译时出错(本例),要么在运行时出错(更复杂的情况下)。出错位置就在注释为 “调用基类的纯虚函数!!!”那行。

在一个复杂的系统里,编译器有时候无法检测出代码调用了一个没有实现的纯虚函数,这就会造成运行期错误,以至程序意外退出。所以,为纯虚函数提供实现(哪怕是空实现,或者输出错误信息),可以有效躲开这种运行期错误——但是,这种做法事实上也隐藏了程序可能的设计错误或实现错误,所以它被视为是一种实用的,但不一定是好的做法。

为纯虚的析构函数提供默认的实现(通常就是空实现),倒是一个被写上书的好做法。具体我们在后续 “虚析构函数”时再做讲解。