名稱空間(C++)
在C++中,名稱可以是變數、函式、結構、列舉、類以及類和結構的成員。當隨著專案的增大,名稱相互衝突的可能性也增加。使用多個廠商的類庫時,可能導致名稱衝突。例如,兩個庫可能都定義了名為List、Tree和Node的類,但定義的方式不相容。使用者可能希望使用一個庫的List類,而使用另一個庫的Tree類。這種衝突被稱為名稱空間問題。 C++標準提供了名稱空間工具,以便更好地控制名稱的作用域。
傳統的C++名稱空間:
宣告區域(declaration region):宣告區域是可以在其中進行宣告的區域。 潛在作用域(oitential scope):變數的潛在作用域從宣告點開始,到其生命區域的結尾。 然而,變數並非在其潛在作用域內的任何位置都是可見的。例如,它可能被另一個在巢狀宣告區域中宣告的同名變數隱藏(同名的區域性變數隱藏全域性變數)。
新的名稱空間特性:
C++新增了這樣一個功能,即通過定義一種新的宣告區域來建立命名的名稱空間,這樣做的目的是提供一個宣告名稱的區域。一個名稱空間中的名稱不會與另一個名稱空間的相同名稱發生衝突,同時,允許程式的其他部分使用該名稱空間中宣告的東西。例如:
namespace Jack {
double pail;
void fetch();
int pal;
struct Well { ... };
}
namespace Jill {
double bucket(double n) { ... };
double fetch;
int pal;
struct Hill { ... };
};
名稱空間可以說全域性的,也可以位於另一個名稱空間中,但不能位於程式碼塊中。因此,在預設情況下,在名稱空間中宣告的名稱的連結性為外部的(除非它引用了常量)。
除了使用者定義的名稱空間外,還存在另一個名稱空間——全域性名稱空間(global namespace)。它對應於檔案級宣告區域,因此前面所說的全域性變數現在被描述為位於全域性名稱空間中。
任何名稱空間中的名稱都不會與其他名稱空間中的名稱發生衝突。因此,Jack中的fetch可以與Jill中的fetch共存,Jill中的Hill可以與外部Hill共存。
名稱空間是開放的,即可以把名稱加入到已有的名稱空間中。例如,下面這條語句將名稱goose新增到Jill中已有的名稱列表中:
namespace Jill {
char *goose(const char *);
}
同樣,原來的Jack名稱空間為fetch()函式提供了原型。可以在該檔案後面(或另外一個檔案中)再次使用Jack名稱空間來提供該函式的程式碼:
namespace Jack {
void fetch() {
...
}
}
訪問給定名稱空間中的名稱:通過作用域解析運算子::,使用名稱空間來限定該名稱:
Jack::pail = 12.34;
Jill::Hill mole;
Jack::fetch();
未被裝飾的名稱(如pail)稱為未限定的名稱(unqualified name),包含名稱空間的名稱(如Jack::pail)稱為限定的名稱(qualified name)。
using宣告和using編譯指令:
我們並不希望每次使用名稱時都對它進行限定,因此C++提供了兩種機制(using宣告和using編譯指令)來簡化對名稱空間中名稱的使用。using宣告使特定的識別符號可用,using編譯指令使整個名稱空間可用。 using宣告由被限定的名稱和它前面的關鍵字using組成:
using Jill::fetch;
using宣告將特定的名稱新增到它所屬的宣告區域中。例如main()中的using宣告Jill::fetch將fetch新增到main()定義的宣告區域中。完成該聲明後,便可以使用名稱fetch來代替Jill::fetch。
namespace Jill {
double bucket(double n) { ... };
double fetch;
struct Hill { ... };
};
char fetch;
int main() {
using Jill::fetch;
double fetch; //error!Already have a local fetch
cin >> fetch; //read a value into Jill::fetch
cin >> ::fetch; //read a value into global fetch
...
}
由於using宣告將名稱新增到區域性宣告區域中,因此這個示例避免了將另一個區域性變數也命名為fetch。另外,和其他區域性變數一樣,fetch也將覆蓋同名的全域性變數。 在函式外面使用using宣告時,將把名稱新增到全域性名稱空間中。
using宣告使一個名稱可用,而using編譯指令使所有的名稱都可用。using編譯指令由名稱空間名和它前面的關鍵字using namespace組成,它使名稱空間中的所有名稱都可用,而不需要使用作用域解析運算子:
using namespace Jack;
有關using編譯指令和using宣告,需要記住的一點是,它們增加了名稱衝突的可能性。也就是說,如果有名稱空間Jack和Jill,並在程式碼中使用作用域解析運算子,則不會存在二義性。
Jack::pal = 3;
Jill::pal = 10;
變數Jack::pal與Jill::pal是不同的識別符號,表示不同的記憶體單元。然而,如果使用using宣告,情況將發生變化。事實上,編譯器不允許您同時使用上述兩個using宣告,因為這將導致二義性。
using編譯指令和using宣告之比較:
使用using編譯指令匯入一個名稱空間中所有的名稱與使用多個using宣告是不一樣的,而更像是大量使用作用域解析運算子。使用using宣告時,就好像聲明瞭相應的名稱一樣。如果某個名稱已經在函式中聲明瞭,則不能用using宣告匯入相同的名稱。然而使用using編譯指令時,將進行名稱解析,就像在包含using宣告和名稱空間本身的最小宣告區域中聲明瞭名稱一樣。在下面的示例中,名稱空間為全域性的。如果使用using編譯指令匯入一個已經在函式中宣告的名稱,則區域性名稱將因此名稱空間名,就像隱藏同名的全域性變數一樣。
namespace Jill {
double bucket(double n) { ... };
double fetch;
struct Hill { ... };
};
char fetch;
int main() {
using namespace Jill;
Hill Thrill;
double water = bucket(2);
double fetch; //not an error; hides Jill::fetch
cin >> fetch; //read a value into the local fetch
cin >> ::fetch; //read a value into global fetch
cin >> Jill::fetch; //read a value into Jill::fetch
...
}
int foom() {
Hill top; //error
Jill::Hill crest; //valid
}
在main()中,名稱Jill::fetch被放在區域性名稱空間中,但其作用域不是區域性的,因此不會覆蓋全域性的fetch。然而,區域性宣告的fetch將因此Jill::fetch和全域性fetch。然而,如果使用作用域解析運算子,則後兩個fetch變數都是可用的。
需要指出的另一點是:雖然函式中的using編譯指令將名稱空間的名稱視為在函式之外宣告的,但它不會使得該檔案中的其他函式可以使用這些名稱。
注意:假設名稱空間和宣告區域定義了相同的名稱。如果試圖使用using宣告將名稱空間的名稱匯入該宣告區域,則這兩個名稱會發生衝突,從而出錯。如果使用using編譯指令將該名稱空間的名稱匯入該宣告區域,則區域性版本將隱藏名稱空間版本。
一般來說,使用using宣告比使用using編譯指令更安全,這是由於它只匯入指定的名稱。如果該名稱與區域性名稱發生衝突,編譯器將發出指示。using編譯指令匯入所有名稱,包括可能並不需要的名稱。如果與區域性名稱發生衝突,在區域性名稱將覆蓋名稱空間版本,而編譯器並不會發出警告。另外,名稱空間的開放性意味著名稱空間的名稱可能分散在多個地方,這使得難以準確知道添加了哪些名稱。
名稱空間的其他特性:
可以將名稱空間宣告進行巢狀:
namespace elements {
namespace fire {
int flame;
...
}
float water;
}
這裡,flame指的是elements::fire::flame。 同樣,可以使用下面的using編譯指令使內部的名稱可用:
using namespace elements::fire;
另外,也可以在名稱空間中使用using編譯指令和using宣告:
namespace myth {
using Jill::fetch;
using namespace elements;
using std::cout;
using std::cin;
}
using編譯指令是可傳遞的。如果A op B且B op C則A op C,則說操作op是可傳遞性的。
using namespace myth;
這條編譯指令與下面兩條編譯指令等價:
using namespace myth;
using namespace elements;
可以給名稱空間建立別名:
namespace my_very_favorite_things{ ... };
namespace mvft = my_very_favorite_things;
可以使用這種技術來簡化對巢狀名稱空間的使用。
未命名的名稱空間:
可以通過省略名稱空間的名稱來建立未命名的名稱空間:
namespace {
int ice;
int bandycoot;
}
這就像後面跟著using編譯指令一樣,也就是說,在該名稱空間中宣告的名稱的潛在作用域為:從宣告點到該宣告區域末尾。從這個方向看,它們與全域性變數相似。然而,由於這種名稱空間沒有名稱,因此不能顯性地使用using編譯指令或using宣告來使它在其他位置都可用。具體地說,不能在未命名名稱空間所屬檔案之外的其他檔案中,使用該名稱空間中的名稱。這提供了連結性為內部的靜態變數的替代品。
名稱空間及其前途:
隨著程式設計師逐漸熟悉名稱空間,將出現統一的程式設計理念。
- 使用在已命名的名稱空間中宣告的變數,而不是使用外部全域性變數。
- 使用在已命名的名稱空間中宣告的變數,而不是使用靜態全域性變數。
- 如果開發了一個函式庫或類庫,將其放在一個名稱空間中。事實上,C++當前提倡將標準函式庫放在名稱空間std中,這種做法擴充套件到了來自C語言中的函式。例如,標頭檔案math.h是與C語言相容的,沒有使用名稱空間,但C++標頭檔案cmath應將各種數學庫函式放在名稱空間std中。
- 僅將編譯指令using作為一種將舊程式碼轉換為使用名稱空間的權宜之計。
- 不要在標頭檔案中使用using編譯指令。
- 匯入名稱時,首選使用作用域解析運算子或using宣告的方法。
- 對於using宣告,首選將其作用域設定為區域性而不是全域性。
宣告:以上整理自個人理解和Stephen Prata 著的《C++ Primer Plus》