C++GOW系列之(5):重载虚函数

原文请链接http://www.gotw.ca/gotw/005.htm

虚函数是非常基础的特性,如果你能回答上以下问题,你就能完全了解他们。

问题:

假设你正在浏览公司代码库的边角地带,你碰到了一段如下的代码。编写这段代码的人看上去像在实验C++的这些特性是如何工作的。程序员想要打印的结果是什么?实际结果是什么呢?

1#include <iostream> 2 #include <complex> 3 using namespace std; 4 5 class Base { 6 public: 7 virtual void f( int ) { 8 cout << "Base::f(int)" << endl; 9 } 10 11 virtual void f( double ) { 12 cout << "Base::f(double)" << endl; 13 } 14 15 virtual void g( int i = 10 ) { 16 cout << i << endl; 17 } 18 }; 19 20 class Derived: public Base { 21 public: 22 void f( complex<double> ) { 23 cout << "Derived::f(complex)" << endl; 24 } 25 26 void g( int i = 20 ) { 27 cout << "Derived::g() " << i << endl; 28 } 29 }; 30 31 void main() { 32 Base b; 33 Derived d; 34 Base* pb = new Derived; 35 36 b.f(1.0); 37 d.f(1.0); 38 pb->f(1.0); 39 40 b.g(); 41 d.g(); 42 pb->g(); 43 44 delete pb; 45 }

答案:

首先是一些风格问题:

  • void main() ;

    这不是main函数的合法用法,尽管一些编译器允许这样使用。要使用"int main()"或者"int main(int, char*[])"。然而,要注意到,return语句在main中并不是必须的。如果你不写,编译器会为你自动加上"return 0;"。

  • delete pb;

     这看上去没什么错误,前提是基类的编写者使用了虚拟析构函数。事实上,通过基类指针删除一个没有虚析构函数的实例就像魔鬼一样,你最好期望它能立即奔溃。

        【规则】使基类的析构函数为虚

  •     Derived::f(complex<double>)

     Derived 冰没有重载Base::f,而是隐藏了它。这非常重要,因为在Derived中,Base::f(int)和Base::f(double)在Derived中不可见。(注意,一些知名的编译器对此甚至连警告都没有)

        【规则】当提供了一个与继承而来的函数同名的函数时,确保使用"using"指示符将继承而来的函数引入类作用域中,如果你不想隐藏他们。

  • Derived::g(int i = 10)

     除非你想要愚弄大家一下,否则不要试图改变继承而来的默认参数值。(总的来讲,尽量使用继承而来的默认参数值并不是个坏主意,不过这是它本身的问题)在C++中这是合法的,而且是良好定义的,但是,请不要这样使用。通过下面的代码来展示它是如何迷惑人们的。

        【规则】永远不要修改继承而来的默认参数值。

现在我们清楚掉了主要的代码风格问题,让我们切回主题来看一下结果是否是编写者想要的。

1 void main() { 2 Base b; 3 Derived d; 4 Base* pb = new Derived; 5 6 b.f(1.0);

没问题,调用了Base::f(double)

d.f(1.0);

这调用了Derived::f(complex<double>).为什么呢?回想一下,Derived并没有声明"using Base::f()"。显然,Base::f(int)和Base::f(double)并不会被调用。

这调用了Derived::f(complex<double>),而编译器连警告都不会发出。这是因为"幸运的"在complex<double>有从"double"的隐式转换,所以编译器把它视作"Derived::f(complex<double>(1.0))"。

    隐式转换是因为在当前的草案中,complex<double>的构造器并不是explicit。

pb->f(1.0);

这儿很有趣,它调用的是Base::f(double),尽管pb指向的是Derived类型。这是因为重载决议是在静态类型上完成的,而不是在动态类型上。

b.g();

这打印出了"10",这是因为它仅仅是调用了Base::g(int i = 10).没问题。

d.g();

这打印出了"20",这是因为它仅仅调用了Derived::g(int i = 20).依然没问题。

pb->g();

这打印出了"Derived::g() 10"。这会使你非常吃惊直到你意识到编译器这么做是多么的正确。这里要记住的是,像重载决议一样,默认参数来自于静态类型,而不是动态类型,因此是10.然而,这里恰巧是虚函数,因此调用的又是动态类型的方法。

    【译者观点】关于虚函数中的默认参数,在effective c++中,作者提倡的原则是不要在虚函数中使用默认参数,比此文中的规则更加严格,至于使用那种规则,就要见仁见智了。在译者本人的项目组中,看到的已有代码会使用虚函数的默认参数,毕竟省时省力。现代的代码补全工具会在派生类中根据基类声明自动添加同样的默认参数值。很多时候为了兼容客户代码,即使要重构,也必须继续使用默认参数。因此使用或者不使用并不是简简单单的一条规则,it depends。

如果你理解了最后几个段落(哎呀,原来是这样啊!),那你应该完全理解了这个主题,恭喜。

1 delete pb; 2 }

对于delete语句,无论如何它会崩溃,析构掉部分内存。详见上文关于虚析构函数部分。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )