虚函数是一个很基本的特性,但是它们偶尔会隐藏在很微妙的地方,然后等着你。如果你能回答下面的问题,那么你已经完全了解了它,你不太能浪费太多时间去调试类似下面的问题。

Problem

JG Question

1. override和final这两个关键字都有什么作用?为什么他们有用?

Guru Qusetion

2. 在你浏览公司的代码的时候,你看到了一个未知程序员写的下面的代码片段。这个程序员好像看起来是在练习一些C++特性,想看下它们是怎么工作的。

(a)怎么做能改进下面代码的正确性或风格?

(b)这个程序员可能期待程序打印什么,但实际上是怎么一个情况?

class base {
public:
virtual void f( int );
virtual void f( double );
virtual void g( int i = );
}; void base::f( int ) {
cout << "base::f(int)" << endl;
} void base::f( double ) {
cout << "base::f(double)" << endl;
} void base::g( int i ) {
cout << i << endl;
} class derived: public base {
public:
void f( complex<double> );
void g( int i = );
}; void derived::f( complex<double> ) {
cout << "derived::f(complex)" << endl;
} void derived::g( int i ) {
cout << "derived::g() " << i << endl;
} int main() {
base b;
derived d;
base* pb = new derived; b.f(1.0);
d.f(1.0);
pb->f(1.0); b.g();
d.g();
pb->g(); delete pb;
}

Stop and thinking…..

Solution

1. override和final这两个关键字都有什么作用?为什么他们有用?

这些关键字的功能是对虚函数的重写有了明确的控制。在声明中使用override的意图是重写基类的虚函数。而final则是让基类的虚函数在子类中变得不再具有可重写性,或一个类不再允许有子类。

它们有用是因为它们让程序员在编译期对函数的声明有了更明确意图。如果你在声明中写了override而在基类中找不到匹配的虚函数,或你声明为final而在派生类中试图隐式或显式地重写函数,那么在编译期就会有错误。

两者之中,到目前为止,更常见的使用的是override,使用final很少见。

2. (a)怎么做能改进下面代码的正确性或风格?
     首先,看一看一些风格问题,这里有个具体的错误:
1.代码中显示地使用new、delete和*

避免使用原始指针和显示使用new和delete,除了在你试图写一写底层数据结构的内部实现时。

{
base* pb = new derived; ... delete pb;
}

使用make_unique和unique_ptr<base>来替代new和base*

{
auto pb = unique_ptr<base>{ make_unique<derived>() }; ... } // automatic delete here

Guideline: 不要显示地使用new和delete和拥有*指针。除了封装在一些底层数据结构的内部实现时。

但是,delete带来了另外一个不相干的问题,如何分配和管理对象的生命期,也就是:

2.基类的析构函数应该是virtual或protected

class base {
public:
virtual void f( int );
virtual void f( double );
virtual void g( int i = );
};

这看起来是无伤大雅的事,但是在base类中既没有使得析构函数为virtual也没有是protected。事实上,通过指向没有virtual析构函数的基类的指针来删除对象是一件邪恶的事。因为派生类的成员不会被销毁,并且delete操作符会以不正确的对象大小被调用。

Guideline: 使基类的析构函数是public且virtual,或protected且non-virtual

下面的其中一项会适用于一个多态类型:
      · 要不允许通过指向基类的指针来析构,此时析构函数必须是public并且最好是virtual
      · 或者不通过,此时析构函数必须是protected(private是不被允许的,因为派生类析构函数必须能够调用基类析构函数)且是non-virtual(当派生类析构函数调用基类析构函数,不论声明是否为virtual,它确实是non-virtual的)

插曲
对于接下来的问题,有必要区分一下下面三个术语:
    · 重载(overload)函数f的意思是在相同作用域中提供另外一个有着相同名字但不同参数类型的函数。当调用f时,编译器基于具体的类型尝试去匹配最适合的一个
    · 重写(override)虚函数f的意思是在派生类中,提供另外一个有着相同名字和参数类型的函数。
    · 隐藏(hide)函数f在一个存在的封闭作用域中(基类、外部类或名字空间)的意思是在内部作用域中(派生类、嵌套类或名字空间)提供一个相同名字的函数,此时将会隐藏外部作用域中的相同名字的函数。

3.derived::f既不是重写也不是重载

void derived::f( complex<double> )

派生类derived没有重载base::f函数,而是隐藏了它。这个区别很重要,因为这意味着base::f(int)和base::f(double)在派生类derived的作用域中是不可见的。

如果写derived的程序员确实是想要隐藏基类中同名的函数f,那么这是正确的。但是一般来说,隐藏可能是一时的疏忽,正确的做法是将它的名字带入派生类的作用域中,比如在derived类中这样写:using base::f。

Guideline: 当提供一个非重写的同名函数作为继承而来的函数时,如果你不想隐藏它们,确保在作用域中对继承而来的函数使用using-声明。

4.derived::g重写了base::g,但是声明中却没有“override”

void g( int i =  )  /* override */

这个函数重写了基类的函数,因此它应该显示地写上override。这样就记录了它的意图,且如果你试图去重写一个不是虚的函数或者将函数签名误写了的话,此时编译器会提醒你。

Guideline: 当有意去重写虚函数时,应该总是写上override。

5.derived::g重写了base::g但改变了默认参数。

void g( int i =  )

     改变默认参数是很明确的用户不友好行为。除非你真的是想去迷惑使用它的人,否则的话不要改变你重写函数的默认参数。这个在C++是合法的,且结果也是定义良好的,但是不要那么做。下面我们会看到它如何让人感到困惑。

Guideline: 绝不要改变重写函数的默认参数

或者可以进一步:

Guideline: 一般情况下载虚函数中避免有默认参数

最后,公有的虚函数是好的,当这个类是一个纯抽象基类(abstract base class --ABC),只指定虚接口而不实现它们,就像C#或java中的interface那样。

Guideline: 倾向于一个类只有公有的虚函数,或非公有的虚函数()除了特别的析构函数


                   一个纯抽象基类应该只有公有的虚函数。

但是当一个类既有虚函数又有它们的实现,考虑使用Non-Virtual Interface(NVI),这样区分公有接口和虚接口。对于任何其他基类,倾向于使公有成员函数为non-virtual且虚成员函数为非公有。前者应该有默认参数并且是依据后者来实现。这很清晰地将公有接口从派生接口中分离了出来,让它们遵循它们自然的格式来适应不同的用户,且避免了一个函数有两个责任来做两份事。其他的好处是,使用NVI经常可以在一些重要方面阐明你的类的设计。比如对于用户重要的默认参数来说,它应该属于公有接口而不是虚接口。

2. (b)这个程序员可能期待程序打印什么,但实际上是怎么一个情况?

现在我们已经把这些问题解决了,来看看主函数中是否确实是做了这个程序员想要做的事:

int main() {
base b;
derived d;
base* pb = new derived; b.f(1.0);

没问题,首先调用base::f(double),和想象中的那样。

d.f(1.0);

这会调用derived::f(complex<double>),为什么?,记住这里的derived类没有使用using base::f来将基类的f函数带入这个作用域,因此很明确,base::f(int)和base::f(double)不会被调用。它们没有出现在和derived::f(complex<double>)一样的作用域中。

这个程序员可能是想要这调用base::f(double),但在这种情况下将不会,甚至会是编译错误,因为幸运(?)的是complex<double>提供了一个double的隐式转换,因此编译器将这个调用解释成derived::f( complex<double>(1.0) ).

pb->f(1.0);

有意思的是,尽管base* pb是指向derived对象,但这会调用base::f(double),因为函数重载解析是在静态类型上(base)完成的,而不是动态类型(derived)。base指针,base接口。基于同样原因,调用pb->f(complex<double>(1.0));将不会被编译,因为此时在基类中找不到匹配的函数。

b.g();

打印出10,因为这只是简单地调用base::g(int),默认的参数为10,毫不费力。

d.g();

打印出derived::g() 20,因为这只是调用derived::g(int),默认参数是20,同样毫不费力。

pb->g();

打印出derived::g() 10.

你可能会奇怪,这究竟发生了什么?这个结果可能会让你的脑子短路一下下子,然后你意识到这就是编译器要做的事!要记住的事是,重载,默认的参数是取自于对象的静态类型(base),因此这里是10。然而,这个函数是虚函数,所以具体调用的函数是基于对象的动态类型(derived)。再一次,这可以通过避免在虚函数中使用默认参数来避免。或者使用NVI来完全避免公有虚函数。

delete pb;

最后,值得注意的是,这应该是不必要的,因为你应该使用unique_ptr,它会为你做最后的清理工作,同时base应该有个virtual析构函数,这样通过任意指向base的指针都能正确地析构。

原文地址:http://herbsutter.com/2013/05/22/gotw-5-solution-overriding-virtual-functions/

[译]GotW #5:Overriding Virtual Functions的更多相关文章

  1. [C++] OOP - Virtual Functions and Abstract Base Classes

    Ordinarily, if we do not use a function, we do not need to supply a definition of the function. Howe ...

  2. [CareerCup] 13.3 Virtual Functions 虚函数

    13.3 How do virtual functions work in C++? 这道题问我们虚函数在C++中的工作原理.虚函数的工作机制主要依赖于虚表格vtable,即Virtual Table ...

  3. Standard C++ Programming: Virtual Functions and Inlining

    原文链接:http://www.drdobbs.com/cpp/standard-c-programming-virtual-functions/184403747 By Josée Lajoie a ...

  4. [译]GotW #4 Class Mechanics

    你对写一个类的细节有多在行?这条款不仅注重公然的错误,更多的是一种专业的风格.了解这些原则将会帮助你设计易于使用和易于管理的类. JG Question 1. 什么使得接口“容易正确使用,错误使用却很 ...

  5. [译]GotW #6b Const-Correctness, Part 2

         const和mutable对于书写安全代码来说是个很有利的工具,坚持使用它们. Problem Guru Question 在下面代码中,在只要合适的情况下,对const进行增加和删除(包括 ...

  6. [译]GotW #89 Smart Pointers

    There's a lot to love about standard smart pointers in general, and unique_ptr in particular. Proble ...

  7. [译]GotW #6a: Const-Correctness, Part 1

    const 和 mutable在C++存在已经很多年了,对于如今的这两个关键字你了解多少? Problem JG Question 1. 什么是“共享变量”? Guru Question 2. con ...

  8. [译]GotW #3: Using the Standard Library (or, Temporaries Revisited)

    高效的代码重用是良好的软件工程中重要的一部分.为了演示如何更好地通过使用标准库算法而不是手工编写,我们再次考虑先前的问题.演示通过简单利用标准库中已有的算法来避免的一些问题. Problem JG Q ...

  9. [译]GotW #2: Temporary Objects

        不必要的和(或)临时的变量经常是罪魁祸首,它让你在程序性能方面的努力功亏一篑.如何才能识别出它们然后避免它们呢? Problem JG Question: 1. 什么是临时变量? Guru Q ...

随机推荐

  1. Extjs搜索域使用

    要在使用的panel在预先加载搜索域类requires : ["Ext.ux.form.SearchField"],

  2. java取随机数

    一, 指定的特定几个数据集合里按“随机顺序”全部取出 一碰到随机, 可能第一个想到的是用Math.Random() 来处理, 其实java本身提供了现成的类 通过 “打乱顺序”来处理“随机”问题 方法 ...

  3. 12天学好C语言——记录我的C语言学习之路(Day 8)

    12天学好C语言--记录我的C语言学习之路 Day 8: 从今天开始,我们获得了C语言中很有力的一个工具,那就是函数.函数的魅力不仅于此,一个程序到最后都是由众多函数组成的,我们一定要用好函数,用熟练 ...

  4. WPF嵌入百度地图完整实现

    无论是做App还是web开发,很多都会用到地图功能,一般都会调用第三方的API实现地图功能!而正如国内的地图API提供方,基本上对Android.IOS和web开发提供了很完整的一套API,但是对于桌 ...

  5. Google 编码风格

    一.Google JavaScript编码风格 简体中文版 Google JavaScript Style Guide 二.Google HTML/CSS代码风格指南 简体中文版 三.Google C ...

  6. springmvc学习(一)helloworld实例

    今天介绍的是springmvc的学习,越来越多的企业开始选择springmvc+mybatis来构建系统架构,在电商热门的今天,springmvc+mybatis已成为电商项目架构的很好搭配.Spri ...

  7. File Operation using SHFileOperation

    SHFILEOPSTRUCT Original link: http://winapi.freetechsecrets.com/win32/WIN32SHFILEOPSTRUCT.htm Refere ...

  8. Poj 1017 / OpenJudge 1017 Packets/装箱问题

    1.链接地址: http://poj.org/problem?id=1017 http://bailian.openjudge.cn/practice/1017 2.题目: 总时间限制: 1000ms ...

  9. Codeforces Round #345 (Div. 1) A. Watchmen 模拟加点

    Watchmen 题意:有n (1 ≤ n ≤ 200 000) 个点,问有多少个点的开平方距离与横纵坐标的绝对值之差的和相等: 即 = |xi - xj| + |yi - yj|.(|xi|, |y ...

  10. MySQL 5.7 启用查询日志

    MySQL版本:5.7 新版本的 my.ini 文件改动了,导致原先启用查询日志的方法不再适用 新版本的启用方法如下: 1. 修改 C:\ProgramData\MySQL\MySQL Server ...