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

“override” 意为 “覆盖”,在C++11新标引入。它最主要的作用就是让程序员可以明确告诉编译器:我要在当前类里“重写”基类的某个函数——请帮我检查:我写得对或不对?

override 在 C++11(即2011年的新标准) 引入。它的目的,主要是为了避免程序员犯低级错误,另外还能让代码有更好的可读性。

下面我们做个对比。

题9-override的作用是什么?

1 不用 override 有什么问题?

没错,override 在 C++ 11 之前(C++98)不存在,在C++11及之后,也非必须——意思是:不用这个关键字,也可以写出功能一致的代码。

假设基类 “Flyable” 定义了一个虚函数,叫 “Fly”,下面是不使用 override,实现在派生类 Bird中提供覆盖实现的 “Fly” 的代码:

class Flyable { public: virtual void Fly() { cout << "我是谁?我为什么在天上?" << endl; }; }; class Bird : public Flyable { public: void Fly() { cout << "我拍着翅膀飞..." << endl; } };

例中,Bird::Fly() 到底有没有覆盖 Flyable::Fly() 的实现?当然有!那这样写有什么问题?
答:没有大问题,只有很重要的“小问题” (毕竟,在C++11之前的一代人,就是这么写)。

1.1 问题1:可读性不好

第1个小问题:在实际项目中,派生类和基类通常不在同一个源文件中,所以阅读 Bird 代码的人,看着它的 Fly() ,无法看出它是一个虚函数,更无法看出它覆盖了基类的实现。

在 C++11 之前,有个半桶水的解决方法:在派生类中,也为 Fly() 函数加上 “virtual”,如:

class Bird : public Flyable { public: virtual void Fly() { cout << "我拍着翅膀飞..." << endl; } };

这个方法确实是“半桶水”:现在能看出 Fly 是一个虚函数了,但是,Fly 到底是在基类(Flyable)中就已经定义了的虚函数,还是在派生类(Bird)中才新定义的一个虚函数?看不出来。

1.2 问题2:查不出低级错误

第2个问题要比第1个更可怕一些:假设我是派生类 Bird 的作者,然后我英语不好、或指法不好、或视力不好,或者就是心情不好,于是在输入派生类的 Fly 函数时,一不小心写成 F1y()……

class Bird : public Flyable { public: virtual void F1y() // 正确应是 Fly { cout << "我拍着翅膀飞..." << endl; } };

然后我就一直用着派生类的 F1y()了,并且我心里还以为它是一个来自基类的那个 Fly() 虚函数——但其实你已经知道了:F1y() 是在一个在派生类中新造出来的函数,它和基类的 Fly () 自然不是 “覆盖” 关系——事实上,它俩现在毫无关系。

除了函数名看错以外,参数列表稍有不同,也会产生相同问题:派生类新创了一个全新的函数。这时候,虽然名函数同名,但其实仍然不存在“覆盖”的关系。比如,我们要求 Fly() 加一个整数入参,用于表示飞行时的最低高度。基类因此为 Fly() 加了一个 long 类型的入参:

class Flyable { public: virtual void Fly(long m) { cout << "我是谁?我为什么在 " << m << " 米的空中?" << endl; }; };

这回,写派生类的人英语棒,指法也棒,视力更棒,但可惜他容易相当然,于是在实现派生类时,long 入参被他写成 int ……

class Bird : public Flyable { virtual void Fly(int m) { cout << "我拍着翅膀在 " << m << " 米高空飞..." << endl; } };

很糟糕:现在 Bird::Fly(int) 和 基类 Flyable::Fly(long)又没有“覆盖”关系了。

覆盖关系有什么用,见本系列前面的课堂

1.3 问题小结

还有很多可能在引诱我们犯此类的低级错误,比如:

  • 基类定义的某个虚函数有两个入参,按次序是 int 和 char *,结果写派生类的实现时,不小写对调了次序,变成 char * 或 int ……
  • 基类定义的某个虚函数入参是 char const*,写派生类的同学比较喜欢用 std::string ……

所有这些,都还是建立在“写基类的同学是个好人,一切错误都是写派生类的同学犯下”这样的前提(事实上,写基类的和写派生类的是同一个人的情况下,也会“人格分裂”犯这种错);更可怕的情况我们现在才提:如果写基类的家伙,是个坏蛋,怎么办?

写基类的家伙怎么个坏法呢?比如,一开始他定义虚函数时,参数用的是 char const* 类型;这回,我们(写派生类的人)没有犯错,我们也用了 char const* ;但是,写基类的人突然有一天,觉得还是用标准库字符串比较好,于是把 char const* 换成 std::string,改完以后,他还不告诉我们……

总而言之,不同谁是“好人”谁是“坏人”,关键是 C++ 在这个问题上,没有办法让我们把人的意图明确地通过语法告诉编译器:我希望这个函数必须和基类的中某个同名函数存在“覆盖/override”关系。

2 解决:使用 override,完美!

override 必须在派生类要覆盖的函数身上使用,而不能在基类从头开始定义一个虚函数时使用,所以,下面基类的代码完全没有变化:

class Flyable { public: virtual void Fly() { cout << "我是谁?我为什么在天上?" << endl; }; };

但是,派生类版本,在函数名字(包括小括号)之后,函数体之前,加了 override,并且,通常这时候,我们也就不再写 virtual :

class Bird : public Flyable { public: void Fly() override // <- 变化在这里:头部少了 virtual,尾部多了 override { cout << "我拍着翅膀飞..." << endl; } };

先不说编译器, 别人看这段代码,也很清晰我们的意图:Fly() 这个函数,它想 “覆盖 / override” 基类的的实现。

这时候,如果我们再把 Fly 写成 F1y:

class Bird : public Flyable { public: virtual void F1y() override // 注意这行 { cout << "我拍着翅膀飞..." << endl; } };

这一次轮到编译器来理解我们的内心:当编译器看到 override 关键字,这回它明确知道 F1y() 是要 “覆盖 / override ” 基类的实现,于是编译器去基类(有必时,将包括基类的基类)的代码中“翻了一翻”,迅速发现问题了:基类没有这个 “F1y”啊?您这是要覆盖什么?

其它情况也类似。