你或許不瞭解的C++函數調用(1)

這篇博客名字起得可能太自大了,搞得本身像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)的概念,使得這個問題變得有些複雜,但很是有意思。調試

1、從一段程序講起

 

首先,問你們個問題,在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。函數內部作了兩件事情: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名字空間裏,全局名字空間裏面並無,爲何沒有報錯呢?

2、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::不能省略。

3、名字空間污染

對已一開始的例子,可能不少人更喜歡寫成:

#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::都加上,保證不出問題。

4、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

這樣就達到了兼顧新標準和已有代碼的目標。

5、名字空間別名

若是咱們有一個很深的名字空間,好比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相關的就想到這麼多,之後有新的瞭解再跟你們分享!

相關文章
相關標籤/搜索