天天看點

C++命名空間 namespace的作用和使用解析

一、 為什麼需要命名空間(問題提出)

    命名空間是ANSIC++引入的可以由使用者命名的作用域,用來處理程式中 常見的同名沖突。

   在 C語言中定義了3個層次的作用域,即檔案(編譯單元)、函數和複合語句。C++又引入了類作用域,類是出現在檔案内的。在不同的作用域中可以定義相同名字的變量,互不于擾,系統能夠差別它們。

    1、全局變量的作用域是整個程式,在同一作用域中不應有兩個或多個同名的實體(enuty),包括變量、函數和類等。

例:如果在檔案中定義了兩個類,在這兩個類中可以有同名的函數。在引用時,為了差別,應該加上類名作為限定:

   class A     //聲明A類

    { public:

       void funl();//聲明A類中的funl函數

    private:

        int i; };

   void A::funl() //定義A類中的funl函數

    {…………}

    class B //聲明B類

    { public:

        void funl(); //B類中也有funl函數

        void fun2(); };

    void B::funl() //定義B類中的funl函數

    { …………}

這樣不會發生混淆。

    在 檔案中可以定義全局變量(global variable),它的作用域是整個程式。如果在檔案A中定義了一個變量a        int a=3;

在檔案B中可以再定義一個變量a       int a=5;

在分别對檔案A和檔案B進行編譯時不會有問題。但是,如果一個程式包括檔案A和檔案B,那麼在進行連接配接時,會報告出錯,因為在同一個程式中有兩個同名的變量,認為是對變量的重複定義。

   可 以通過extern聲明同一程式中的兩個檔案中的同名變量是同一個變量。如果在檔案B中有以下聲明:

      extem int a;

   表示檔案B中的變量a是在其他檔案中已定義的變量。由于有此聲明,在程式編譯和連接配接後,檔案A的變量a的作用域擴充到了檔案B。如果在檔案B中不再對a指派,則在檔案B中用以下語句輸出的是檔案A中變量a的值: cout<<a; //得到a的值為3

   2、程式中就會出現名字沖突。

   在簡單的程式設計中,隻要人們小心注意,可以争取不發生錯誤。但是,一個大型的應用軟體,往往不是由一個人獨立完成的,而是由若幹人合作完成的,不同的人分别完成不同的部分,最後組合成一個完整的程式。假如不同的人分别定義了類,放在不同的頭檔案中,在主檔案(包含主函數的檔案)需要用這些類時,就用#include指令行将這些頭檔案包含進來。由于各頭檔案是由不同的人設計的,有可能在不同的頭檔案中用了相同的名字來命名所定義的類或函數。

例4 名字沖突

程式員甲在頭檔案headerl.h中定義了類 Student和函數fun。

// 例4中的頭檔案header1(頭檔案1,沒其檔案名為cc8-4-h1.h)

#include

#include

using namespace std;

class Student //聲明Student類

   { public:

     Student(int n,string nam,int a)

       { num=n;name=nam;age=a;}

     void get_data();

   private:

     int num;

     string name;

     int age; };

void Student::get_data() //成員函數定義

{ cout<<num<<" "<<name<<" "<<age<<endl; }

double fun(double a,double b)//定義全局函數(即外部函數)

{ return sqrt(a+b);}

在 main函數所在的檔案中包含頭檔案headerl.h:

#include

using namespace std;

#include "header1.h" //注意要用雙引号,因為檔案一般是放在用使用者目錄中的

int main()

{   Student stud1(101,"Wang",18); //定義類對象studl

   stud1.get_data();

   cout<<fun(5,3)<<endl;

   return 0; }

程式 能正常運作,輸出為

   101 Wang 18

   2.82843

   如果程式員乙寫了頭檔案header2.h,在其中除了定義其他類以外,還定義了類Student和函數fun,但其内容與頭檔案headerl.h中的 Student和函數fun有所不同。

// 例4中的頭檔案header2

#include

#include

using namespace std;

class Student //聲明Student類

{ public:

     Student(int n,string nam,char s) //參數與headerl中的student不同

     { num=n;name=nam;sex=s;}

     void get_data();

     private:

     int num;

     string name;

     char sex; };//此項與headerl不同

void Student::get_data() //成員函數定義

{ cout<<num<<" "<<name<<" "<<sex<<endl; }

double fun(double a,double b) //定義全局函數

   { return sqrt(a-b);} //傳回值與headerl中的fun函數不同

//頭檔案2中可能還有其他内容

   假如主程式員在其程式中要用到headerl.h中的Student和函數fun,因而在程式中包含了頭檔案headerl.h,同時要用到頭檔案 header2.h中的一些内容(但對header2.h中包含與headerl.h中的Student類和fun函數同名而内容不同的類和函數并不知情,因為在一個頭檔案中往往包含許多不同的資訊,而使用者往往隻關心自己所需要的部分,而不注意其他内容),因而在程式中又包含了頭檔案 header2.h。如果主檔案(包含主函數的檔案)如下:

#include

using namespace std;

#include "header1.h"//包含頭檔案l

#include "header2.h"//包含頭檔案2

int main()

{ Student stud1(101,"Wang",18);

stud1.get_data();

cout<<fun(5,3)<<endl;

return 0; }

   這時程式編譯就會出錯。因為在預編譯後,頭檔案中的内容取代了對應的#include指令行,這樣就在同一個程式檔案中出現了兩個Student類和兩個 fun函數,顯然是重複定義,這就是名字沖突,即在同一個作用域中有兩個或多個同名的實體。

   3、全局命名空間污染(global namespace pollution)。

   在程式中還往往需要引用一些庫(包括C++編譯系統提供的庫、由軟體開發商提供的庫或者使用者自己開發的庫),為此需要包含有關的頭檔案。如果在這些庫中包含有與程式的全局實體同名的實體,或者不同的庫中有相同的實體名,則在編譯時就會出現名字沖突。

   為了避免這類問題的出現,人們提出了許多方法,例如:将實體的名字寫得長—些(包含十幾個或幾十個字母和字元);把名字起得特殊一些,包括一些特殊的字元;由編譯系統提供的内部全局辨別符都用下劃線作為字首,如_complex(),以避免與使用者命名的實體同名;由軟體開發商提供的實體的名字用特定的字元作為字首。但是這樣的效果并不理想,而且增加了閱讀程式的難度,可讀性降低了。

   C 語言和早期的C++語言沒有提供有效的機制來解決這個問題,沒有使庫的提供者能夠建立自己的命名空間的工具。人們希望ANSI C++标準能夠解決這個問題,提供—種機制、一種工具,使由庫的設計者命名的全局辨別符能夠和程式的全局實體名以及其他庫的全局辨別符差別開來。

二、 什麼是命名空間(解 決方案)

   命名空間:實際上就是一個由程式設計者命名的記憶體區域,程式設計者可以根據需要指定一些有名字的空間域,把一些全局實體分别放在各個命名空間中,進而與其他全局實體分隔開來。

如: namespace ns1 //指定命名中間nsl

      { int a;

      double b; }

   namespace 是定義命名空間所必須寫的關鍵字,nsl 是使用者自己指定的命名空間的名字(可 以用任意的合法辨別符,這裡用ns1是因為ns是namespace的縮寫,含義請楚),在花括号内是聲明塊,在其中聲明的實體稱為命名空間成員(namespace member)。現在命名空間成員包括變量a和b,注意a和b仍然是全局變量,僅僅是把它們隐藏在指定的命名空間中而已。如果在程式中要使用變量a和b,必須加上命名空間名和作用域分辨符“::”,如nsl::a,nsl::b。這種用法稱為命名空間限定(qualified),這些名字(如nsl::a)稱為被限定名 (qualified name)。C++中命名空間的作用類似于作業系統中的目錄和檔案的關系,由于檔案很多,不便管理,而且容易重名,于是人們設立若幹子目錄,把檔案分别放到不同的子目錄中,不同子目錄中的檔案可以同名。調用檔案時應指出檔案路徑。

   命名空間的作用:是建立一些互相分隔的作用域,把一些全局實體分隔開來。以免産生老點名叫李相國時,3個人都站起來應答,這就是名字沖突,因為他們無法辨識老師想叫的是哪一個李相國,同名者無法互相區分。為了避免同名混淆,學校把3個同名的學生分在3個班。這樣,在小班點名叫李相國時,隻會有一個人應答。也就是說,在該班的範圍(即班作用域)内名字是惟一的。如果在全校集合時校長點名,需要在全校範圍内找這個學生,就需要考慮作用域問題。如果校長叫李相國,全校學生中又會有3人一齊喊“到”,因為在同一作用域中存在3個同名學生。為了在全校範圍内區分這3名學生,校長必須在名字前加上班号,如高三甲班的李相國,或高三乙班的李相國,即加上班名限定。這樣就不緻産生混淆。

   可以根據需要設定許多個命名空間,每個命名空間名代表一個不同的命名空間域,不同的命名空間不能同名。這樣,可以把不同的庫中的實體放到不同的命名空間中,或者說,用不同的命名空間把不同的實體隐蔽起來。過去我們用的全局變量可以了解為全局命名空間,獨立于所有有名的命名空間之外,它是不需要用 namespace聲明的,實際上是由系統隐式聲明的,存在于每個程式之中。

在聲明一個命名空間時,花括号内不僅可以包括變量,而且還可以包括以下類型:

·變量(可以帶有初始化);

·常量;

·數(可以是定義或聲明);

·結構體;

·類;

·模闆;

·命名空間(在一個命名空間中又定義一個命名空間,即嵌套的命名空間)。

例如

namespace nsl

   { const int RATE=0.08; //常量

   doublepay;       //變量

   doubletax()       //函數

      {return a*RATE;}

   namespacens2       //嵌套的命名空間

      {int age;}

   }

如果想輸出命名空間nsl中成員的資料,可以采用下面的方法:

cout<<nsl::RATE<<endl;

cout<<nsl::pay<<endl;

cout<<nsl::tax()<<endl;

cout<<nsl::ns2::age<<endl; //需要指定外層的和内層的命名中間名

   可以看到命名空間的聲明方法和使用方法與類差不多。但它們之間有一點差别:在聲明類時在右花括号的後面有一分号,而在定義命名空間時,花括号的後面沒有分 号。

三、 使用命名空間解決名字沖突(使用指南)

有了以上的基礎後,就可以利用命名空間來解決名字沖突問題。現在,對例4程式進行修改,使之能正确運作。

例5 利用命名空間來解決例4程式名字沖突問題。

修改兩個頭檔案,把在頭檔案中聲明的類分别放在兩個不同的命名空間中。

//例8.5中的頭檔案1,檔案名為header1.h

using namespace std;

#include

#include

namespace ns1 //聲明命名空間ns1

{ class Student //在命名空間nsl内聲明Student類

{ public:

Student(int n,string nam,int a)

{ num=n;name=nam;age=a;}

void get_data();

private:

int num;

string name;

int age; };

void Student::get_data() //定義成員函數

   { cout<<num<<" "<<name<<" "<<age<<endl; }

double fun(double a,double b) //在命名空間n引内定義fun函數

{ return sqrt(a+b);}

}

//例 8.5中的頭檔案2,檔案名為header2.h

#include

#include

namespace ns2 //聲明命名空間ns2

{ class Student

{ public:

Student(int n,string nam,char s)

{ num=n;name=nam;sex=s;}

void get_data();

private:

int num;

string name;

char sex; };

void Student::get_data()

{ cout<<num<<" "<<name<<" "<<sex<<endl; }

double fun(double a,double b)

{ return sqrt(a-b);}

}

//main file

#include

#include "header1.h" //包含頭檔案l

#include "header2.h" //包含頭檔案2

int main()

{ ns1::Student stud1(101,"Wang",18);//用命名空間nsl中聲明的Student類定義studt

stud1.get_data(); //不要寫成ns1::studl.get_data();

cout<<Ns1::fun(5,3)<<endl; //調用命名空間ns1中的fun函數

ns2::Student stud2(102,"Li",'f'); //用命名空間ns2中聲明的 Student類定義stud2

stud2.get_data();

cout<<ns2::fun(5,3)<<endl; //調用命名空間nsl,中的fun函數

return 0; }

解決本題的關鍵是建立了兩個命名空間nsl和ns2,将原來在兩個頭檔案中聲叫的類分别放在命名空間nsl和ns2中。注意:在頭檔案中,不要把#include指令放在命名空間中,在上一小節的叙述中可以知道,命名空間中的内容不包括指令行,否則編譯會出錯。

分析例4程式出錯的原因是:在兩個頭檔案中有相同的類名Student和相同的函數名fun,在把它們包含在主檔案中時,就産生名字沖突,存在重複定義。編譯系統無法辨識用哪一個頭檔案中的Student來定義對象studl。現在兩個Student和fun分别放在不同的命名空間中,各自有其作用域,互不相幹。由于作用域不相同,不會産:生名字沖突。正如同在兩個不同的類中可以有同名的變量和函數而不會産生沖突一樣。

在定義對象時用ns1::Student(命名空間nsl中的Student)來定義studl,用ns2::Student(命名空間ns2中的 Student)來定義stud2。顯然,nsl::Student和ns2::Student是兩個不同的類,不會産生混淆。同樣,在調用fun函數時也需要用命名空間名ns]或ns2加以限定。ns1::fun()和ns2::fun()是兩個不同的函數。注意:對象studl是用 nsl::Student定義的,但對象studl并不在命名空間nsl中。studl的作用域為main函數範圍内。在調用對象studl的成員函數 get_data時,應寫成studl.get_data(),而不應寫成nsl::studl.get_data()。

程式 能順利通過編譯,并得到以下運作結果:

101 Wang l9 (對象studl中的資料)

2.82843 (/5+3的值)

102 Li f (對象studg中的資料)

1.41421 (/5-2的值)

四、 使用命名空間成員的方法

從上面的介紹可以知道,在引用命名空間成員時,要用命名空間名和作用域分辨符對命名空間成員進行限定,以差別不同的命名空間中的同名辨別符。即:

命名空間名::命名空間成員名

這種方法是有效的,能保證所引用的實體有惟一的名字。但是如果命名空間名字比較長,尤其在有命名空間嵌套的情況下,為引用一個實體,需要寫很長的名字。在一個程式中可能要多次引用命名空間成員,就會感到很不友善。

1 、使用命名空間别名

可以為命名空間起一個别名(namespace alias),用來代替較長的命名空間名。如

namespace Television //聲明命名空間,名為Television

{ ... }

可以用一個較短而易記的别名代替它。如:

namespace TV=Television; //别名TV與原名Television等價

也可以說,别名TV指向原名Television,在原來出現Television的位置都可以無條件地用TV來代替。

2、使用using命名空間成員名

using後面的命名空間成員名必須是由命名空間限定的名字。例如:

using nsl::Student;

以上語句聲明:在本作用域(using語句所在的作用域)中會用到命名空間ns1中的成員Student,在本作用域中如果使用該命名空間成員時,不必再用命名空間限定。例如在用上面的using聲明後,在其後程式中出現的Student就是隐含地指nsl::Student。

using聲明的有效範圍是從using語句開始到using所在的作用域結束。如果在以上的using語句之後有以下語句:

Student studl(101,"Wang",18); //此處的Student相當于ns1::Student

上面的語句相當于

nsl::Student studl(101,"Wang",18);

又如

using nsl::fun; //聲明其後出現的fun是屬于命名空間nsl中的fun

cout<<fun(5,3)<<endl;//此處處的fun函數相當于nsl::fun(5,3)

顯然,這可以避免在每一次引用命名空間成員時都用命名空間限定,使得引用命名空間成員變得友善易用。

但是要注意:在同一作用域中用using聲明的不同命名空間的成員中不能有同名的成員。例如:

usmgnsl::Student; //聲明其後出現的Student是命名空間nsl中的Student

usmgns2::Student; //聲 明其後出現的Student是命名空間ns2小的Student

Student stud1; //請問此處的Student是哪個命名中間中的Student?

産生了二義性,編譯出錯。

3、使用using namespace命名空間名

用上面介紹的using命名空間成員名,一次隻能聲明一個命名空間成員,如果在一個命名空間中定義了10個實體,就需要使用10次using命名空間成員名。能否在程式中用一個語句就能一次聲明一個命名空間中的全部成員呢?

C++提供了using namespace語句來實作這一目的。using namespace語句的一般格式為

using namespace 命名空間名;

例如

using nanlespace nsl;

聲明了在本作用域中要用到命名空間nsl中的成員,在使用該命名空間的任何成員時都不必用命名空間限定。如果在作了上面的聲明後有以下語句:

Student studl(101,”Wang”,18); //Student隐含指命名中間nsl中的Student

cout<<fun(5,3)<<endl; //這裡的fun函數是命名中間 nsl中的fun函數

在用usmgnamespace聲明的作用域中,命名空間nsl的成員就好像在全局域聲明的一樣。是以可以不必用命名空間限定。顯然這樣的處理對寫程式比較友善。但是如果同時用usingnamespace聲明多個命名空間時,往往容易出錯。例5中的main函數如果用下面程式段代替,就會出錯。

int main()

{ using namespace nsl;//聲明nsl中的成員在本作用域中可用

using namespace ns2;//聲明ns2中的成員在本作用域中可用

Student studl(101,”Wang",18);

studl.8ct_data();

cout<<fun(5,3)<<endl;

Student stud2(102,"Li",'r');

stud2.get_data();

coutt<<fun(5,3)<<endl;

return O; }

因為在同一作用域中同時引入了兩個命名空間nsl和ns2,其中有同名的類和函數。在出現Student時,無法判定是哪個命名空間中的 Student,出現二義性,編譯出錯。是以隻有在使用命名空間數量很少,以及確定這些命名空間中沒有同名成員時才用using namespace語句。

五、 無名的命名空間

以上介紹的是有名字的命名空間,C++還允許使用沒有名字的命名空間,如在檔案A中聲明了以下的無名命名空間:

namespace //命名空間沒有名字

{ void fun( ) //定 義命名空間成員

{ cout<<"OK."<<endl;}

}

由于命名空間沒有名字,在其他檔案中顯然無法引用,它隻在本檔案的作用域内有效。無名命名空間的成員fun函數的作用域為檔案A(确切地說,是從聲明無名命名空間的位置開始到檔案A結束)。在檔案A中使用無名命名空間的成員,不必(也無法)用命名空間名限定。

如果 在檔案A中有以下語句:

fun();

則執行無名命名空間中的成員fun函數,輸出”OK.”。

在本程式中的其他檔案中也無法使用該fun函數,也就是把fun函數的作用域限制在本檔案範圍中。可以聯想到:在C浯言中可以用static聲明一個函數,其作用也是使該函數的作用域限于本檔案。C++保留了用static聲明函數的用法,同時提供了用無名命名空間來實作這一功能。随着越來越多的C++ 編譯系統實作了ANSI C++建議的命名空間的機制,相信使用無名命名空間成員的方法将會取代以前習慣用的對全局變量的靜态聲明。

六、标準命名空間std

為了解決C++标準庫中的辨別符與程式中的全局辨別符之間以及不同庫中的辨別符之間的同名沖突,應該将不同庫的辨別符在不同的命名空間中定義(或聲明)。标準C++庫的所有的辨別符都是在一個名為std的命名空間中定義的,或者說标準頭檔案(如iostream)中函數、類、對象和類模闆是在命名空間 std中定義的。std是standard(标準)的縮寫,表示這是存放标準庫的有關内容的命名空間,含義請楚,不必死記。

這樣,在程式中用到C++标準庫時,需要使用std作為限定。如

std::cout<<"OK."<<endl; //聲明cout是在 命名空間std中定義的流對象

在有的C++書中可以看到這樣的用法。但是在每個cout,cm以及其他在std中定義的辨別符前面都用命名空間std作為限定,顯然是很不友善的。在大多數的C++程式中常用usmgnamespace語句對命名空間std進行聲明,這樣可以不必對每個命名空間成員一進行處理,在檔案的開頭加入以下 using namespace聲明:

using namespace std;

這樣,在std中定義和聲明的所有辨別符在本檔案中都可以作為全局量來使用。但是應當絕對保證在程式中不出現與命名空間std的成員同名的辨別符,例如在程式中不能再定義一個名為cout的對象。由于在命名空間std中定義的實體實在太多,有時程式設計人員也弄不請哪些辨別符已在命名空間std中定義過,為減少出錯機會,有的專業人員喜歡用若幹個"using命名空間成員”聲明來代替“using namespace命名空間”聲明,如

using Std::string;

using Std::cout;

using Std::cin;

等。為了減少在每一個程式中都要重複書寫以亡的using聲明,程式開發者往往把編寫應用程式時經常會用到的命名空間std成員的usmg聲明組成一個頭檔案,然後在程式中包含此頭檔案即可。

如果閱讀了多種介紹C++的書,可能會發現有的書的程式中有using namespace語句,有的則沒有。有的讀者會提出:究竟應該有還是應該沒有?應當說:用标準的C++程式設計,是應該對命名空間std的成員進行聲明或限定的(可以采取前面介紹過的任一種方法)。但是目前所用的C++庫大多是幾年前開發的,當時并沒有命名空間,庫中的有關内容也沒有放在std命名空間中,因而在程式中不必對std進行聲明。

七、 使用早期的函數庫

C語言程式中各種功能基本上都是由函數來實作的,在C語言的發展過程中建立了功能豐富的函數庫,C++從C語言繼承了這份寶貴的财富。在C++程式中可以使用C語言的函數庫。

如果要用函數庫中的函數,就必須在程式檔案中包含有關的頭檔案,在不同的頭檔案中,包含了不同的函數的聲明。

在C++中使用這些 頭檔案有兩種方法。

1、用C語言的傳統方法

頭檔案名包括字尾.h,如stdio.h,math.h等。由于C語言沒有命名空間,頭檔案并不存放在命名空間中,是以在C++程式檔案中如果用到帶字尾.h的頭檔案時,不必用命名空間。隻需在檔案中包含所用的頭檔案即可。如

#include

2、用C++的新方法

  C++标準要求系統提供的頭檔案不包括字尾.h,例如iostream、string。為了表示與C 語言的頭檔案有聯系又有差別,C++所用的頭檔案名是在C語言的相應的頭檔案名(但不包括字尾.h)之前加一字母c。例如,C語言中有關輸入與輸出的頭檔案名為stdio.h在C++中相應的頭檔案名為cstdio。C語言中的頭檔案math.h,在C++中相應的頭文什名為cmath。C語言中的頭檔案 string.h在C++中相應的頭檔案名為cstring。注意在C++中,頭檔案cstnng和頭檔案strmg不是同一個檔案。前者提供C語言中對字元串處理的有關函數(如strcmp,ctrcpy)的聲明,後者提供C++中對字元串處理的新功能。

此外,由于這些函數都是在命名空間std中聲明的,是以在程式中要對命名空間std作聲明。如:

#include

#include

using namespace std;

目前所用的大多數C++編譯系統既保留了c的用法,又提供丁C++的新方法。下面兩種用法等價,可以任選。

C傳 統方法 C++新方法

#include #include

#include #include

#include #include

using namespace std;

可以使用傳統的c方法,但應當提倡使用C++的新方法

繼續閱讀