你或许不了解的C++函数调用(1)
这篇博客名字起得可能太自大了,搞得自己像C++大牛一样,其实并非如此。C++有很多隐藏在语法之下的特性,使得用户可以在不是特别了解的情况下简单使用,这是非常好的一件事情。但是有时我们可能会突然间发现一个很有意思的现象,然后去查资料,最终学到了C++的一个特性。所以很可能每个人理解的C++都有很大不同,我只是从自己的角度去跟大家分享而已。
C++的函数调用相比于C的函数调用要复杂很多,这主要是由于函数重载、类、命名空间等特性造成的。
根据Stephan T. Lavavej的介绍,C++编译器在解析一次函数调用的时候,要按照顺序做以下事情(根据具体情况,有些步骤可能会跳过的):
1) 名字查找(Name Lookup)
2) 模板参数类型推导(Template Argument Deduction)
3) 重载决议(Overload Resolution)
4) 访问控制(Access Control)
5) 动态绑定(Dynamic Binding)
本篇博客主要跟大家分享下自己对Name lookup的理解。
对于编译器来说,完成一次函数调用之前,必须能够先找到这个函数。在C中这个问题很简单,就是函数调用点向上找函数声明,如果能找到就匹配,如果找不到就报错。在C++中有函数重载(Function Overload)和名字空间(Namespace)的概念,使得这个问题变得有些复杂,但非常有意思。
一、从一段程序讲起
首先,问大家个问题,在C++程序中,我们经常这样写:
#include <iostream> int main()
{
std::cout << "Hello, Core C++!" << std::endl;
}
请问:上面main函数中的语句使用了重载操作符<<,如果用普通函数调用的语法该怎么写?
显然,这个语句一共有两次operator<<函数调用。那么这两个operator<<函数调用是一样的函数吗?如果不是,区别在哪里?
OK,告诉大家答案吧,上面的代码等价于这样写:
#include <iostream> int main()
{
operator<<(std::cout, "Hello, Core C++!");
std::cout.operator<<(std::endl);
}
大家看出来了吧?第一次operator<<调用的是一个全局函数,而第二次调用的是一个成员函数。
如果再深入一些,std::endl到底是个什么东西?直觉上这就是用来换行的,可能就是一个\n。而事实上,std::endl是一个函数。为什么呢?我们先看看VC中std::endl的代码:
template<class _Elem,
class _Traits> inline
basic_ostream<_Elem, _Traits>&
__CLRCALL_OR_CDECL endl(basic_ostream<_Elem, _Traits>& _Ostr)
{ // insert newline and flush stream
_Ostr.put(_Ostr.widen('\n'));
_Ostr.flush();
return (_Ostr);
}
std::endl是一个全局函数,接受一个basic_ostream参数_Ostr。函数内部做了两件事情:一、调用_Ostr的put(const char*)成员函数,输出\n;二、调用_Ostr的flush()函数。其中第二步保证了ostream立即刷新,这也就是std::cout<<”\n”和std::cout<<std::endl的区别。也就只有std::endl是个函数才能完成这样的操作。
还是最开始的例子,如果写成这样:
#include <iostream> int main()
{
cout << "Hello, Core C++!" << endl;
}
编译器会提示“undeclared identifier”,因为我们没有指定任何namespace,编译器默认到全局命名空间中查找,相当于::cout << "Hello, Core C++!" << ::endl;,而程序中并没有提供的cout和endl,因此找不到。这个大家应该都比较熟悉了。
再问大家一个问题:
operator<<(std::cout, "Hello, Core C++!");
为什么这个语句不写成:
std::operator<<(std::cout, "Hello, Core C++!");
也能通过编译呢?毕竟operator<<是在std名字空间里,全局名字空间里面并没有,为什么没有报错呢?
二、Name Lookup的主要机制
这就要从C++标准中对于名字查找的描述说起了。C++中有三种主要名字查找机制:
a) 隐式名字查找(Unqualified name lookup);
b) 基于参数的名字查找(Argument-dependent name lookup,ADL);
c) 显式名字查找(Qualified name lookup)。
显然,如果变量和函数之前不写任何名字空间,就是隐式名字查找,此时编译器只会从当前命名空间和全局命名空间中查找;如果写了名字空间,就是显式名字查找,编译器会忠实地按照指定的命名空间去查找。
最有意思的是基于参数的名字查找,简称ADL,也叫Koenig Lookup,这种名字查找方式是C++大牛Andrew Koenig发明的。具体来说,对于一个函数调用,如果没有显式地写函数的名字空间,编译器会根据函数的参数所在的名字空间里面去查找这个函数。最新的C++标准加强了这个规则,叫Pure ADL,也就是只到参数所在的名字空间里去查找,而不到其它名字空间里查找,这样的好处是防止找到其它名字空间里具有相同签名的函数,导致非常隐蔽的bug。
这就可以理解为什么
operator<<(std::cout, "Hello, Core C++!");
可以正常编译了,因为函数中有std::cout这个参数,所以编译器就会到std名字空间里去查找operator<<这个函数。
这个特点非常重要,否则C++中的操作符重载根本无法做到像现在如此简洁。可以想象下,如果每次都要去指定操作符的命名空间,语法该有多丑!仅仅通过ADL,就可以看出Andrew Koenig对于C++的贡献。
注意:
std::cout.operator<<(std::endl);
这个语句不能省略最前面的std::,这是因为C++中类本身也形成了一个名字空间(就是类名),也就是说std::cout.operator<<这个函数的名字空间是std:ostream,而不是std,而std::endl在std名字空间中,ADL是不会向下去查找嵌套的名字空间的的,只会在当前名字空间里去查找。因此最前面的std::不能省略。
三、名字空间污染
对已一开始的例子,可能很多人更喜欢写成:
#include <iostream>
using namespace std; int main()
{
cout << "Hello, Core C++!" << endl;
}
这样下面使用任何STL里面的类和算法的时候,都不用加上std::前缀了,这样是方便,但是也是会带来问题的。using namespace std;这个语句将std里面所有的东西(类、算法、对象等等)都引入到我当前的名字空间中,其中很多东西我是暂时使用不到的。如果我自己在当前名字空间中定义了一些和std中同名的东西的话,就会导致一些意想不到的问题:
#include <iostream>
using namespace std; class Polluted {
public:
Polluted& operator<<(const char*)
{
return *this;
}
};
int main()
{
Polluted cout;
cout << "Hello, Core C++!\n";
}
上面这个程序,看上去会输入Hello, Core C++!,实际上却什么都没做。因为cout已经不是std::cout了,而是Polluted的一个对象,这个对象恰巧也有一个operator<<(const char*)函数。因为名字空间查找和普通变量的作用域一样,局部名字空间会覆盖全局名字空间和引入的名字空间,所以编译器虽然两个cout都找到,但根据局部优先于全局的规则,选用了main函数中定义的cout,而不是std::cout。
这样的危害在于当程序规模比较大的时候,这样的问题会变得很隐蔽,甚至测试都不一定能测试到,但是却会引发非常奇怪的问题,给调试带来非常大的麻烦。所以using namespace std;尽量少用,最多使用using std::cout,这样就只引入std中的cout,其它东西都没有引入,出问题的概率小些,但问题依旧存在,所以如果可能的话,尽量将std::都加上,保证不出问题。
四、using在STL中的使用
2005年,C++对STL进行了扩充,就是所谓的TR1(Technical Report 1),里面加入了很多实用的库,如shard_ptr、function、bind、regular exprestion等等,它们都位于std::tr1名字空间下。到了C++11,TR1中的很多库得到了升级,正式成为std名字空间中的一员。但是之前很多代码已经用了std::tr1,为了确保已有的代码不被破坏,并且不要重复定义相同的东西。STL采取这样的方式:将原来std::tr1中的定义移到std中,然后在std::tr1中使用using指令将库引入到std::tr1中。如VC中有这样的代码:
namespace tr1 { // TR1 additions
using _STD allocate_shared;
using _STD bad_weak_ptr;
using _STD const_pointer_cast;
using _STD dynamic_pointer_cast;
using _STD enable_shared_from_this;
using _STD get_deleter;
using _STD make_shared;
using _STD shared_ptr;
using _STD static_pointer_cast;
using _STD swap;
using _STD weak_ptr;
} // namespace tr1
这样就达到了兼顾新标准和已有代码的目标。
五、名字空间别名
如果我们有一个很深的名字空间,比如A::B::C::D::E,并且经常会用到这里面的类和函数,我们不希望每次都敲这么长的前缀,当然也不希望通过using namespace A::B::C::D::E来污染名字空间,C++提供了名字空间别名的方式来简化使用。比如,我们可以通过
namespace ABCDE = A::B::C::D::E;
产生名字空间别名ABCDE,ABCDE::ClassT就等价于A::B::C::D::E::ClassT。
C++11中,这种方式的别名得到了扩展,不仅仅用于名字空间,可以用于任何别名:
using ABCDE = A::B::C::D::E;
using ABCDE_ClassT = ABCDE::ClassT;
这样的语法基本上可以替代typedef了,而且语法更简洁。
OK,关于Name lookup相关的就想到这么多,以后有新的了解再跟大家分享!
你或许不了解的C++函数调用(1)的更多相关文章
- Apache Spark源码走读之3 -- Task运行期之函数调用关系分析
欢迎转载,转载请注明出处,徽沪一郎. 概要 本篇主要阐述在TaskRunner中执行的task其业务逻辑是如何被调用到的,另外试图讲清楚运行着的task其输入的数据从哪获取,处理的结果返回到哪里,如何 ...
- JavaScript中七种函数调用方式及对应 this 的含义
this 在 JavaScript 开发中占有相当重要的地位,不过很多人对this这个东西都感觉到琢磨不透.要真正理解JavaScript的函数机制,就非常有必要搞清楚this到底是怎么回事. 函数调 ...
- How Javascript works (Javascript工作原理) (一) 引擎,运行时,函数调用栈
个人总结:该系列文章对JS底层的工作原理进行了介绍. 这篇文章讲了 运行时:js其实是和AJAX.DOM.Settimeout等WebAPI独立分离开的 调用栈:JavaScript的堆内存管理 和 ...
- C++如何解析函数调用
C语言是一个简单的语言.用户针对每一个函数,只能设置一个唯一的函数签名.但是C++而言,就给了我们很多的灵活性: 你可以将多个函数设置为相同的名字(overloading) 你可以使用内置操作符重载( ...
- 站在风口,你或许就是那年薪20w+的程序猿
最近面试了一些人,也在群上跟一些群友聊起,发现现在的互联网真是热,一些工作才两三年的期望的薪资都是十几K的起,这真是让我们这些早几年就成为程序猿的情何以堪!正所谓是站在风口上,猪也能飞起来!我在这里就 ...
- idapython实现动态函数调用批量注释
部门小伙伴遇到一个样本需要对动态函数调用就行批量注释还原的问题,通过idapython可以大大的减少工作量,其实这一问题也是很多样本分析中最耗时间的一块,下面来看看如何解决这个问题(好吧这才是今年最后 ...
- [汇编与C语言关系]1.函数调用
对于以下程序: int bar(int c, int d) { int e = c + d; return e; } int foo(int a, int b) { return bar(a, b); ...
- javascript函数调用的各种方法!!
在JavaScript中一共有下面4种调用方式: (1) 基本函数调用 (2)方法调用 (3)构造器调用 (4)通过call()和apply()进行调用 1. 基本函数调用 普通函数调用模式,如: J ...
- jsContext全局函数调用与对象函数调用、evaluateScript
evaluateScript:兼具js加载(生成具体的上下文)(函数与通用变量的加载),与函数执行的功能: 函数调用的方式有两种: 1)获取函数(对象),然后执行调用: [context[@" ...
随机推荐
- MySql 分页
MySql 分页 由于最近项目需要,于是就简单写了个分页查询.总体而言MySql 分页机制较为简单.数据库方面只需要使用limit即可实现分页.前后台交互就直接用session传了值. 下面就写写具体 ...
- android: 通过内容提供器读取系统联系人
读取系统联系人 由于我们之前一直使用的都是模拟器,电话簿里面并没有联系人存在,所以现在需要自 己手动添加几个,以便稍后进行读取.打开电话簿程序,界面如图 7.1 所示. 图 7.1 可以看到,目前 ...
- file_get_contents高級用法
首先解決file_get_contents的超時問題,在超時返回錯誤後就象js中的settimeout那樣進行一次嘗試,錯誤超過3次或者5次後就確認為無法連線伺服器而徹底放棄.這裡就簡單介紹兩種解決方 ...
- JVM中的Stack和Frame
JVM执行Java程序时需要装载各种数据,比如类型信息(Class).类型实例(Instance).常量数据(Constant).本地变量等.不同的数据存放在不同的内存区中,这些数据内存区称作“运行时 ...
- C#中扩展StringBuilder支持链式方法
本篇体验扩展StringBuilder使之支持链式方法. 这里有一个根据键值集合生成select元素的方法. private static string BuilderSelectBox(IDicti ...
- FTP客户端上传下载Demo实现
1.第一次感觉MS也有这么难用的MFC类: 2.CFtpFileFind类只能实例化一个,多个实例同时查找会出错(因此下载时不能递归),采用队列存储目录再依次下载: 3.本程序支持文件夹嵌套上传下载: ...
- Java 多线程(1)-Thread和Runnable
一提到Java多线程,首先想到的是Thread继承和Runnable的接口实现 Thread继承 public class MyThread extends Thread { public void ...
- H-Basis/SG/SH GI Relighting
小试了一把预计算全局光照,作为PRT的上级应用.完全自行实现,使用SG/SH.H-Basis基波对GI光场进行频域压缩,存在3D纹理中,用于2跳间接光照实时显示.其中坑点不少,尤其是在HDR环境下使用 ...
- LogViewer - 方便的日志查看工具
一个完整的程序日志记录功能是必不可少的,通过日志我们可以了解程序运行详情.错误信息等,以便更好的发现及解决问题. 日志可以记录到数据库.日志服务器.文件等地方,本文主要介绍文件日志. 文件日志通常是一 ...
- [转]Sublime Text3注册码(可用)
补充:2016.05 最近经过测试,3个注册码在新版3103的sublime上已经不可用了. 现补充两枚新版的license key: —– BEGIN LICENSE —– Michael Barn ...