【原创】Performanced C++ 经验规则 第五条:再谈重载、覆盖和隐藏

第五条:再谈重载、覆盖和隐藏

在C++中,无论在类作用域内还是外,两个(或多个)同名的函数,可能且仅可能是以下三种关系:重载(Overload)、覆盖(Override)和隐藏(Hide),因为同名,区分这些关系则是根据参数是否相同、是否带有const成员函数性质、是否有virtual关键字修饰以及是否在同一作用域来判断。在第四条中,我们曾提到了一些关于重载、覆盖的概念,但只是一带而过,也没有提到隐藏,这一篇我们将详细讨论。

1、首先说的是重载,有一个前提必须要弄清楚的是,如果不在类作用域内进行讨论,两个(或多个)同名函数之间的关系只可能是重载或隐藏,这里先说重载。考虑以下事实:

1 int foo(char c){...}
2 void foo(int x){...}

这两个函数之间的关系是重载(overload),即相同函数名但参数不同,并注意返回类型是否相同并不会对重载产生任何影响

也就是说,如果仅仅是返回类型不相同,而函数名和参数都完全相同的两个函数,不能构成重载,编译器会告知"ambiguous"(二义性)等词以表达其不满:

1 //Can't be compiled!
2 
3 int fooo(char c){...}
4 void fooo(char c){...}
5 
6 char c = 'A';
7 fooo(c); // Which one? ambiguous

在第四条中,已经讲述过,重载是编译期绑定的静态行为,不是真正的多态性,那么,编译器是根据什么来进行静态绑定呢?又是如何确定两个(或多个)函数之间的关系是重载呢?

有以下判定依据:

(1)相同的范围:即作用域,这里指在同一个类中,或同一个名字空间,即C++的函数重载不支持跨越作用域进行(读者可再次对比Java在这问题上的神奇处理,既上次Java给我们提供了未卜先知的动态绑定能力后,Java超一流的意识和大局观再次给Java程序员提供了跨类重载的能力,如有兴趣可详细阅读《Thinking in Java》的相关章节,其实对于学好C++来讲,去学一下Java是很有帮助的,它会告诉你,同样或类似的问题,为什么Java要做这样的改进),这也是区别重载和隐藏的最重要依据。

关于“C++不能支持跨类重载”,稍后笔者会给出代码来例证这一点。

(2)函数名字相同(基本前提)

(3)函数参数不同(基本前提,否则在同一作用域内有两个或多个同名同参数的函数,将产生ambiguous,另外注意,对于成员函数,是否是const成员函数,即函数声明之后是否带有const标志, 可理解为“参数不同“),第(2)和第(3)点统称“函数特征标”不同

(4)virtual关键字可有可无不产生影响(因为第(1)点已经指出,这是在同一个类中)

“相同的范围,特征标不同(当然同名是肯定的),发生重载“

 

2、覆盖(override),真正的多态行为,通过虚函数来实现,所以,编译器根据以下依据来进行判定两个(注意只可能是两个,即使在继承链中,也只是最近两个为一组)函数之间的关系是覆盖:

(1)不同的范围:即使用域,两个函数分别位于基类和派生类中

(2)函数名字相同(基本前提)

(3)函数参数也相同(基本前提),第(2)和第(3)点统称“函数特征标”相同

(4)基类函数必须用virtual关键字修饰

“不同的范围,特征标相同,且基类有virtual声明,发生覆盖“

 

3、隐藏(Hide),即:

(1)如果派生类函数与基类函数同名,但参数不同(特征标不同),此时,无论是否有virtual关键字,基类的所有同名函数都将被隐藏,而不会重载,因为不在同一个类中;

(2)如果派生类函数与基类函数同名,且参数也相同(特征标相同),但基类函数没有用virtual关键字声明,则基类的所有同名函数都将被隐藏,而不会覆盖,因为没有声明为虚函数。

“不同的范围,特征标不同(当然同名是肯定的),发生隐藏”,或"不同的范围,特征标相同,但基类没有virtual声明,发生隐藏“

可见有两种产生隐藏的情况,分别对应不能满足重载和覆盖条件的情况。

另外必须要注意的是,在类外讨论时,也可能发生隐藏,如在名字空间中,如下述代码所示:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 void foo(void) { cout << "global foo()" << endl; }
 5 int foo(int x) { cout << "global foo(int)" << endl; return x; }
 6 namespace a
 7 {
 8         void foo(void) { cout << "a::foo()" << endl; }
 9         void callFoo(void) 
10         { foo();
11            // foo(10); Can't be compiled! }
12 }
13 
14 int main(int argc, char** argv)
15 {
16         foo();
17         a::callFoo();
18         return 0;
19 }    

输出结果:

1 global foo()
2 a::foo()

注意,名字空间a中的foo隐藏了其它作用域(这里是全局作用域)中的所有foo名称,foo(10)不能通过编译,因为全局作用域中的int foo(int)版本也已经被a::foo()隐藏了,除非使用::foo(10)显式进行调用。

这也告诉我们,无论何时,都使用完整名称修饰(作用域解析符调用函数,或指针、对象调用成员函数)是一种好的编程习惯

 好了,上面零零散散说了太多理论的东西,我们需要一段实际的代码,来验证上述所有的结论:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Other
 5 {
 6         void* p;
 7 };
 8 
 9 class Base
10 {
11 public:
12         int iBase;
13         Base():iBase(10){}
14         virtual void f(int x = 20){ cout << "Base::f()--" << x << endl; }
15         virtual void g(float f) { cout << "Base::g(float)--" << f << endl; }
16         void g(Other& o) { cout << "Base::g(Other&)" << endl; }
17         void g(Other& o) const { cout << "Base::g(Other&) const" << endl;}
18 };
19 
20 class Derived : public Base
21 {
22 public:
23         int iDerived;
24         Derived():iDerived(100){}
25         void f(int x = 200){ cout << "Derived::f()--" << x << endl; }
26         virtual void g(int x) { cout << "Derived::g(int)--" << x << endl; }
27 };
28 
29 int main(int argc, char** argv)
30 {
31         Base* pBase = NULL;
32         Derived* pDerived = NULL;
33         Base b;
34         Derived d;
35         pBase = &b;
36         pDerived = &d;
37         Base* pBD = &d;
38         const Base* pC = &d;
39         const Base* const pCCP = &d;
40         Base* const pCP = &d;
41 
42         int x = 5;
43         Other o;
44         float f = 3.1415926;
45 
46         b.f();
47         pBase->f();
48         d.f();
49         pDerived->f();
50         pBD->f();
51 
52         b.g(x);
53         b.g(o);
54         d.g(x);
55         d.g(f);
56         // Can't be compiled!
57         // d.g(o);
58 
59         pBD->g(x);
60         pBD->g(f);
61         pC->g(o);
62         pCCP->g(o);
63         pCP->g(o);
64 
65         return 0;
66 }

在笔者Ubuntu 12.04 + gcc 4.6.3运行结果: 

 1 Base::f()--20 //b.f(),通过对象调用,无虚特性,静态绑定
 2 Base::f()--20 //基类指针指向基类对象,虽然是动态绑定,但没有使用到覆盖
 3 Derived::f()--200 //d.f,通过对象调用,无虚特性,静态绑定
 4 Derived::f()--200 //子类指针指向子类对象,虽然是动态绑定,但没有使用到覆盖
 5 Derived::f()--20 //基类指针指向子类对象,动态绑定,子类f()覆盖基类版本。但函数参数默认值,是静态联编行为,pBD的类型是基类指针,所以使用了基类的参数默认值,注意此处!
 6 
 7 Base::g(float)--5 //通过对象调用,int被提升为float
 8 Base::g(Other&) //没什么问题,基类中三个g函数之间的关系是重载
 9 Derived::g(int)--5 //没什么问题
10 Derived::g(int)--3 //注意基类的g(float)已经被隐藏!所以传入的float参数调用的却是子类的g(int)方法!
11 
12 Base::g(float)--5 //注意!pBD是基类指针,虽然它指向了子类对象,但基类中的所有g函数版本它是可见的!所以pBD->g(5)调用到了g(float)!虽然产生了动态联编也发生了隐藏,但子类对象的虚表中,仍可以找到g(float)的地址,即基类版本!
13 Base::g(float)--3.14159 //原理同上
14 
15 //d.g(o)
16 //注意此处!再注意代码中被注释了的一行,d.g(o)不能通过编译,因为d是子类对象,在子类中,基类中定义的三个g函数版本都被隐藏了,编译时不可见!不会重载
17 
18 Base::g(Other&) const //pC是指向const对象的指针,将调用const版本的g函数
19 Base::g(Other&) const //pCCP是指向const对象的const指针,也调用const版本的g函数
20 Base::g(Other&) //pCP是指向非cosnt对象的const指针,由于不指向const对象,调用非const版本的g函数

上述结果,是否和预想的是否又有些出入呢?问题主要集中于结果的第5、12、13和15行。

第5行输出结果证明:当函数参数有默认值,又发生多态行为时,函数参数默认值是静态行为,在编译时就已经确定,将使用基类版本的函数参数默认值而不是子类的

而第12、13、15行输出结果则说明,尽管已经证明我们之前说的隐藏是正确的(因为d.g(o)不可以通过编译,确实发生了隐藏),但却可以利用基类指针指向派生类对象后,来绕开这种限制!也就是说,编译器根据参数匹配函数原型的时候,是在编译时根据指针的类型,或对象的类型来确定,指针类型是基类,那么基类中的g函数版本就是可见的;指针类型是子类,由于发生了隐藏,基类中的g函数版本就是不可见的。而到动态绑定时,基类指针指向了子类对象,在子类对象的虚函数表中,就可以找到基类中g虚函数的地址。

写到这里,不知道读者是否已经明白,这些绕来绕去的关系。在实际代码运用中,可能并不会写出含有这么多“陷阱”的测试代码,我们只要弄清楚重载、覆盖和隐藏的具体特征,并头脑清醒地知道,我现在需要的是哪一种功能(通常也不会需要隐藏),就能写出清析的代码。上面的代码其实是一个糟糕的例子,因为在这个例子中,重载、覆盖、隐藏并存,我们编写代码,就是要尽可能防止这种含混不清的情况发生。

记住一个原则:每一个方法,功能和职责尽可能单一,否则,尝试将它拆分成为多个方法

转载于:https://www.cnblogs.com/ccdev/archive/2012/12/26/2833884.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/448358.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++之纯虚函数和抽象类

纯虚函数和抽象类 1.基本概念 2.案例 #include <iostream> using namespace std;////面向抽象类编程(面向一套预先定义好的接口编程)//解耦合 ....模块的划分class Figure //抽象类 { public://阅读一个统一的界面(接口),让子类使用,让子类必须去实现virtual void get…

解决: -bash: $‘\302\240docker‘: command not found

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 我只是运行 一条很简单的启动容器的命令&#xff0c;多次执行都报错&#xff0c;报错如题&#xff1a; -bash: $\302\240docker: comma…

换挡/挂档

定义 换挡/挂档是指变速器&#xff0c;用于转变发动机曲轴的转矩及转速&#xff0c;以适应汽车在起步、加速、行驶以及克服各种道路阻碍等不同行驶条件下对驱动车轮牵引力及车速不同要求的需要。作用 使汽车能以非常低的稳定车速行驶&#xff0c;而这种低的转速只靠内然…

sql:无法解决 equal to 操作中 Chinese_PRC_CI_AS 和 Chinese_Taiwan_Stroke_CI_AS 之间的排序规则冲突。...

--无法解决 equal to 操作中 "Chinese_PRC_CI_AS" 和 "Chinese_Taiwan_Stroke_CI_AS" 之间的排序规则冲突。 CREATE VIEW View_VipBranchStaffBranchList AS select VipBranchStaff.*,geovindu_branch.B_Name,VipExamCountry.ExamCountryName from VipBran…

【汽车取证篇】GA-T 1998-2022《汽车车载电子数据提取技术规范》(附下载)

【汽车取证篇】GA-T 1998-2022《汽车车载电子数据提取技术规范》&#xff08;附下载&#xff09; GA-T 1998-2022《汽车车载电子数据提取技术规范》标准—【蘇小沐】 总结 公众号回复关键词【汽车取证】自动获取资源合集&#xff0c;如链接失效请留言&#xff0c;便于…

解决: Client does not support authentication protocol requested by server; consider upgrading MySQL

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 在服务器上把 mysql 装好后&#xff0c;运行起来。 2. navicat 死活连接不上&#xff0c;在网上查说是要改数据库账号、密码什么的&…

C++之STL理论基础

1.基本概念 STL&#xff08;Standard Template Library&#xff0c;标准模板库)是惠普实验室开发的一系列软件的统称。虽然主要出现在C中&#xff0c;但在被引入C之前该技术就已经存在了很长的一段时间。 STL的从广义上讲分为三部分&#xff1a;algorithm&#xff08;算法&am…

方向盘

定义 方向盘是汽车、轮船、飞机等的操纵行驶方向的轮状装置。 构成 一般由骨架和发泡组合起来就是最简单的方向盘了&#xff0c;而方向盘上都会有和主驾驶气囊对应的安装卡扣或螺钉孔&#xff0c;其下方一般会有多功能开关模块。作用 方向盘不仅可以控制车辆的方向…

数据库范式俗话

1NF&#xff1a;一个table中的列是不可再分的&#xff08;即列的原子性&#xff09; 2NF&#xff1a;一个table中的行是可以唯一标示的&#xff0c;&#xff08;即table中的行是不可以 重复的&#xff09; 3NF&#xff1a;一个table中的列不依赖于另一个table中的非主键列 4NF&…

STL之string类型

1.String概念 string是STL的字符串类型&#xff0c;通常用来表示字符串。而在使用string之前&#xff0c;字符串通常是用char*表示的。 string和char*的区别&#xff1a; string是一个类, char*是一个指向字符的指针。 string封装了char*&#xff0c;管理这个字符串&#x…

解决maven打包报错:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 一、报错经历&#xff1a; 今天使用eclipse通过maven install打war包的时候&#xff0c;出现了下图所示的错误 二、问题分析&#xff1a…

离合器

离合器的定义 汽车离合器位于发动机和变速箱之间的飞轮壳内&#xff0c;用螺钉将离合器总成固定在飞轮的后平面上&#xff0c;离合器的输出轴就是变速箱的输入轴。在汽车行驶过程中&#xff0c;驾驶员可根据需要踩下或松开离合器踏板&#xff0c;使发动机与变速箱暂时分离和…

Python 删除满足条件的某些行

数据&#xff1a; data 字段&#xff1a;col 要删除的内容是 col False 的行 # 方案一 data1 data[~data[col] False] # ~ 取反# 方案二 保留 data[已采] ! False ind data[col] ! False data2 data.loc[ind,]# 方案三 去掉 data[已采] True ind2 data[col] False…

STL之Vector

1.简介 vector是将元素置于一个动态数组中加以管理的容器。可以随机存取元素&#xff08;支持索引值直接存取&#xff0c;用[]操作符或at()方法&#xff0c;还支持迭代器方式存取&#xff09;。   vector尾部添加或移除元素非常快速。但是在中部或头部插入元素或移除元素比…

解决 : Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 执行 maven install 命令报错如题&#xff1a; Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:comp…

制动踏板

什么是制动踏板 制动踏板就是限制动力的踏板&#xff0c;即脚刹(行车制动器)的踏板&#xff0c;是长时间摩擦导致刹车片过热软化的原因。制动踏板的作用 其主要作用是刹车减速或停车。 制动踏板的工作原理 在机器的高速轴上固定一个轮或盘&#xff0c;在机座上安装…

实现一个用户取过的数据不被其他用户取到

实现一个用户取过的数据不被其他用户取到: 问题&#xff1a; 在用ADO访问数据库时&#xff0c;从一个表中取一定的记录&#xff08;比如20行&#xff09;&#xff0c;取出后在程序中使用&#xff0c;使用完后删除掉记录&#xff08;不用更新或删除记录&#xff09;。在多用户操…

Docker 镜像 重命名

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1. 镜像改名命令格式&#xff1a; # 命令格式&#xff1a;docker tag 镜像id 仓库&#xff1a;标签或&#xff1a;docker tag 旧镜…

STL之deque和其他容器

deque简介 deque是“double-ended queue”的缩写&#xff0c;和vector一样都是STL的容器&#xff0c;deque是双端数组&#xff0c;而vector是单端的。 deque在接口上和vector非常相似&#xff0c;在许多操作的地方可以直接替换。 deque可以随机存取元素&#xff08;支持索引…

Java蓝桥杯02——第二题集锦:生日蜡烛、星期一、方格计数、猴子分香蕉

第二题 生日蜡烛(结果填空) 某君从某年开始每年都举办一次生日party&#xff0c;并且每次都要吹熄与年龄相同根数的蜡烛。 现在算起来&#xff0c;他一共吹熄了236根蜡烛。 请问&#xff0c;他从多少岁开始过生日party的&#xff1f; 请填写他开始过生日party的年龄数。 注意&a…