这篇博客名字起得可能太自大了,搞得本身像C++大牛同样,其实并不是如此。C++有不少隐藏在语法之下的特性,使得用户能够在不是特别了解的状况下简单使用,这是很是好的一件事情。可是有时咱们可能会忽然间发现一个颇有意思的现象,而后去查资料,最终学到了C++的一个特性。因此极可能每一个人理解的C++都有很大不一样,我只是从本身的角度去跟你们分享而已。html
C++的函数调用相比于C的函数调用要复杂不少,这主要是因为函数重载、类、命名空间等特性形成的。ios
根据Stephan T. Lavavej的介绍,C++编译器在解析一次函数调用的时候,要按照顺序作如下事情(根据具体状况,有些步骤可能会跳过的):算法
1) 名字查找(Name Lookup)ide
2) 模板参数类型推导(Template Argument Deduction)函数
3) 重载决议(Overload Resolution)测试
4) 访问控制(Access Control)this
5) 动态绑定(Dynamic Binding)spa
本篇博客主要跟你们分享下本身对Name lookup的理解。.net
对于编译器来讲,完成一次函数调用以前,必须可以先找到这个函数。在C中这个问题很简单,就是函数调用点向上找函数声明,若是能找到就匹配,若是找不到就报错。在C++中有函数重载(Function Overload)和名字空间(Namespace)的概念,使得这个问题变得有些复杂,但很是有意思。调试
#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。函数内部作了两件事情:1、调用_Ostr的put(const char*)成员函数,输出\n;2、调用_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名字空间里,全局名字空间里面并无,为何没有报错呢?
这就要从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::都加上,保证不出问题。
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相关的就想到这么多,之后有新的了解再跟你们分享!