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

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. 解决某些手机RadioGroup中的RadioButton不居中的问题

    问题:RadioButton中使用android:gravity="center"使其图片文字居中,在我的华为荣耀7手机上居中显示了,但在HUAWEI G606-T00却显示在右侧 ...

  2. Mysql 的变量

    变量 MySQL是一门编程语言.所以存在变量.流程控制.函数.存储过程.触发器 MySQL分系统变量,与自定义变量 MySQL的某些功能是通过系统变量来实现的.例如:autocommit 查看系统变量 ...

  3. OpenJudge/Poj 1753 Flip Game

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

  4. Redis和Memcache的区别分析 [转]

    1. Redis中,并不是所有的数据都一直存储在内存中的,这是和Memcached相比一个最大的区别. 2. Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构 ...

  5. width(),innerHTML(),outerHTML()

    HTML代码: <div id="box"> <p>哈哈,随便写点内容</p> <p>删除的实例</p> <p&g ...

  6. 《jQuery、jQuery UI及jQuery Mobile技巧与示例》勘误收集

    此书由程学彬 (http://weibo.com/ironbin)和我合译完成,此篇博客作为勘误收集而用,若译文有误或者有任何疑问,欢迎留下评论,或者给我发邮件(地址:gzooler@gmail.co ...

  7. BFC(Box,Formatting,Context) —— 块级格式化上下文

    Box:CSS布局的基本单位 Formatting context是页面中的一块渲染区域,最常见的是BFC和IFC,CSS3增加了GFC和FFC BFC定义:块级格式化上下文,它是一个独立的渲染区域, ...

  8. Chrome 浏览器各版本下载大全

    随着最近64位版本的 Chrome 浏览器正式版的推出,Chrome 浏览器再次受到广大浏览迷的重点关注,今天我们就整理一下各版本的 Chrome 浏览器 32位及64位的下载地址,方便各位浏览迷选择 ...

  9. Python的传值和传址与copy和deepcopy

    1.传值和传址 传值就是传入一个参数的值,传址就是传入一个参数的地址,也就是内存的地址(相当于指针).他们的区别是如果函数里面对传入的参数重新赋值,函数外的全局变量是否相应改变,用传值传入的参数是不会 ...

  10. 2016030206 - mysql常用命令

    参考地址如下: http://www.cnblogs.com/linjiqin/archive/2013/03/01/2939384.html http://www.cnblogs.com/zhang ...