【C++】名稱空間 namespace
本講基本要求
* 掌握:名稱空間的作用及定義;如何使用名稱空間。 * 瞭解:使用早期的函式庫 重點、難點 ◆名稱空間的作用及定義;如何使用名稱空間。
在學習本書前面各章時,讀者已經多次看到在程式中用了以下語句:
using namespace std;
這就是使用了名稱空間std。在本講中將對它作較詳細的介紹。
一、 為什麼需要名稱空間
名稱空間是ANSIC++引入的可以由使用者命名的作用域,用來處理程式中常見的同名衝突。
在C語言中定義了3個層次的作用域,即檔案(編譯單元)、函式和複合語句。C++又引入了類作用域,類是出現在檔案內的。在不同的作用域中可以定義相同名字的變數,互不於擾,系統能夠區別它們。
//如果沒有名稱空間的話得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, nt a=3; 在檔案B中可以再定義一個變數a ,int a=5; 在分別對檔案A和檔案B進行編譯時不會有問題。但是,如果一個程式包括檔案A和檔案B,那麼在進行連線時,會報告出錯,因為在同一個程式中有兩個同名的變數,認為是對變數的重複定義。可以通過extern宣告同一程式中的兩個檔案中的同名變數是同一個變數。如果在檔案B中有以下宣告: extem int a;
簡而言之::一是拓展使用C的函式,二是使用全域性定義
表示檔案B中的變數a是在其他檔案中已定義的變數。由於有此宣告,在程式編譯和連線後,檔案A的變數a的作用域擴充套件到了檔案B。如果在檔案B中不再對a賦值,則在檔案B中用以下語句輸出的是檔案A中變數a的值: cout<<a; //得到a的值為3
2、程式中就會出現名字衝突。
在簡單的程式設計中,只要人們小心注意,可以爭取不發生錯誤。但是,一個大型的應用軟體,往往不是由一個人獨立完成的,而是由若干人合作完成的,不同的人分別完成不同的部分,最後組合成一個完整的程式。假如不同的人分別定義了類,放在不同的標頭檔案中,在主檔案(包含主函式的檔案)需要用這些類時,就用#include命令列將這些標頭檔案包含進來。由於各標頭檔案是由不同的人設計的,有可能在不同的標頭檔案中用了相同的名字來命名所定義的類或函式。
例如:名字衝突,程式設計師甲在標頭檔案headerl.h中定義了類Student和函式fun。
//例4中的標頭檔案header1(標頭檔案1,沒其檔名為cc8-4-h1.h)
#include <string>
#include <cmath>
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 <iostream>
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 <string>
#include <cmath>
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 <iostream>
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 <string>
#include <cmath>
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 <string>
#include <cmath>
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 <iostream>
#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; }
程式能順利通過編譯,並得到以下執行結果:
101 Wang l9 (物件studl中的資料)
2.82843 (/5+3的值)
102 Li f (物件studg中的資料)
1.41421 (/5-2的值)
解決本題的關鍵是建立了兩個名稱空間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()。
四、 使用名稱空間成員的方法
從上面的介紹可以知道,在引用名稱空間成員時,要用名稱空間名和作用域分辨符對名稱空間成員進行限定,以區別不同的名稱空間中的同名識別符號。即:
名稱空間名::名稱空間成員名
這種方法是有效的,能保證所引用的實體有惟一的名字。但是如果名稱空間名字比較長,尤其在有名稱空間巢狀的情況下,為引用一個實體,需要寫很長的名字。在一個程式中可能要多次引用名稱空間成員,就會感到很不方便。
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宣告的不同名稱空間的成員中不能有同名的成員。例如:
using nsl::Student; //宣告其後出現的Student是名稱空間nsl中的Student
using ns2::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的標頭檔案時,不必用名稱空間。只需在檔案中包含所用的標頭檔案即可。
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<cstdio>
#include<cmath>
using namespace std;
目前所用的大多數C++編譯系統既保留了c的用法,又提供了C++的新方法。下面兩種用法等價,可以任選。
C傳統方法 C++新方法
#include<stdio.h> #include<cstdio>
#include<math.h> #include<cmath>
#include<string.h> #include<cstring>
using namespace std;
可以使用傳統的c方法,但應當提倡使用C++的新方法。