一些大型軟件往往由多人共同開發,會使用到大量的變量和函數,不可避免容易出現變量或者函數名的命令沖突。即使所有人代碼測試通過,但將它們結合到一起時,也極有可能出現命名沖突。
命名空間(namespace)為防止名字沖突提供了更加可控的機制。命名空間分割了全局命名空間,其中每個命名空間都是一個作用域。
1. 命名空間定義
C++ 使用 namespace 關鍵字來定義一個命名空間,隨后是命名空間的名字。語法格式為:
namespace name
{
// 類、變量(及其初始化操作)、函數(及其定義)、模板和其他命名空間
}
如
namespace cpp_primer
{
class Sales_data{};
class Query{};
void func(){};
int x = 100;
}
和其他名字一樣,命名空間的名字也必須在定義它的作用域內保持唯一。
命名空間既可以定義在全局作用域內,也可以定義在其他命名空間中,但不能定義在函數或類的內部。
命名空間作用域后面無須分號
1.1 每個命名空間都是一個作用域
定義在命名空間中的名字可以被該命名空間內的其他成員直接訪問,也可以被這些成員的內嵌作用域的任何單位訪問,該命名空間之外訪問必須指明所用的屬於哪個命名空間。
例如又定義了一個額外的命名空間,該空間與 cpp_primer 命名空間部分變量名字相同,那么使用里面的成員時,需要指定是那一個命名空間:
namespace Addison_cpp_primer
{
class Sales_data{};
class Query{};
void func(){};
int x = 100;
}
cpp_primer::Query q = cpp_primer::Query();
Addison_cpp_primer::Query q = Addison_cpp_primer::Query();
1.2 命名空間可以不連續
命名空間可以定義在幾個不同的部分。
namespace nsp
{
//
}
上述代碼可能定義了一個新的命名空間,也可能為已存在的命名空間添加一些新成員。
命名空間定義可以不連續的特性使得我們可以將幾個獨立的接口和實現文件組成一個命名空間。
定義多個類型不相關的命名空間應該使用單獨的文件分別表示每個類型。
通過接口和實現分離機制,將 cpp_primer 庫定義在幾個不同文件中。
// Sales_data.h
// #include 應該出現在打開命名空間的操作之前
#include <string>
namespace cpp_primer
{
class Sales_data{};
Sales_data operator+(const Sales_data&,const Sales_data&);
// Sales_data 類的其他接口聲明
}
// Sales_data.cc
#include "Sales_data.h"
namespace cpp_primer
{
// 定義
}
如果需要使用定義的Sales_data 庫,必須包含頭文件。
#include "Sales_data.h"
int main()
{
using cpp_primer::Sales_data;
Sales_data trans1,trans2;
// ...
return 0;
}
通常情況下,我們不把 #include
放在命名空間內部。如果這樣做了,意味着把頭文件中所有的名字定義成該命名空間的成員。
1.3 定義命名空間成員
假定作用域中存在合適的聲明語句,則命名空間中的代碼可以使用直接同一命名空間定義的其他成員
#include "Sales_data.h"
namespace cpp_primer
{
std::istream& operator>>(std::istream& in,Sales_data& s){/* */}
}
也可以在命名空間外部定義該命名空間的成員。命名空間對於名字的聲明必須在作用域,同時該名字的定義需要明確指出其所屬的命名空間。
cpp_primer::Sales_data cpp_primer::operator+(const Sales_data& lhs, const &Sales_data& rhs)
{
Sales_data ret(lhs);
// ...
}
1.4 模板特例化
模板特例化必須定義在原始模板所屬的命名空間中。和其他命名空間名字類似,只要我們在命名空間中聲明了特例化,就能在命名空間外部定義它了。
// 必須將末班特例化聲明成 std 的成員
namespace std
{
template <> struct hash<Sales_data>;
}
// 在 std 中添加了模板特例化的聲明后,就可以在命名空間 std 的外部定義它了
template <> struct std::hash<Sales_data>
{
size_t operator()(const Sales_data& s) const
{
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
}
1.5 全局命名空間
全局作用域(即所有類、函數及命名空間之外)定義的名字是定義在全局命名空間中,全局命名空間以隱式的方式聲明,並且在所有程序中都存在。
全局作用域中定義的名字被隱式地添加到全局命名空間中。
作用域運算符同樣可以用於全局作用域的成員,因為全局作用域是隱式的,所以它並沒有名字:
::member_name
1.6 嵌套的命名空間
嵌套的命名空間是指定義在其他命名空間中的命名空間
namespace cpp_primer
{
namespace QueryLib
{
// ...
}
namespace Bookstore
{
// ...
}
}
內層命名空間聲明的名字將隱藏外層命名空間聲明的同名成員。在嵌套的命名空間中定義的名字只在內層命名空間中有效,外層命名空間要想訪問它必須添加限定符。
cpp_primer::QueryLib::Query
1.7 內聯命名空間
C++11 引入了一種新的嵌套命名空間,稱為內聯命名空間。
和普通的嵌套命名空間不同,內聯命名空間中的名字可以被外層命名空間直接使用。
定義內聯命名空間的方式是在關鍵字 namespace 前添加關鍵字 inline:
inline namespace FifthEd
{
// ...
}
namespace FifthEd // 隱式內聯,第二次定義,內聯關鍵字 inline 可加可不加
{
// ...
}
假設命名空間 cpp_primer 同時使用 FifthEd 和 FourthEd 兩個命名空間
namespace cpp_primer
{
#include "FifthEd.h" // 內聯
#include "FourthEd.h" // 非內聯
}
那么 cpp_primer 命名空間可以不通過限定符可以直接訪問 FifthEd 命名空間的成員,但需要完整的命名空間名才能訪問 FourthEd 的成員。
1.8 未命名的命名空間
未命名的命名空間是指關鍵字 namespace 后緊跟着花括號的一系列聲明語句。
未命名的命名空間中定義的變量擁有靜態生命周期:它們在第一次使用前創建,並且直到程序結束時才銷毀。
未命名空間僅在特定的文件內有效,其作用范圍不會跨越多個不同的文件,它可以在特定文件內不連續。
在文件中進行靜態聲明的做法已經被 C++ 標准取消了,現在的做法是使用未命名的命名空間。
2. 使用命名空間成員
像 namespace_name::member_name 這樣使用命名空間的成員顯然非常繁瑣,可以使用其他更簡便的方法使用命名空間的成員。
2.1 命名空間的別名
命名空間的別名聲明以關鍵字 namespace 開始。
namespace primer = cpp_primer;
- 不能在命名空間還沒有定義之前就聲明別名。
- 一個命名空間可以有好幾個同義詞或別名,所有別名都與命名空間原來的名字等價。
2.2 using聲明:扼要概述
一條 using 聲明語句一次只引入命名空間的一個成員。
它的有效范圍:從 using 聲明的地方開始,一直到 using 聲明所在的作用域結束為止。在此過程中,外層作用域的同名實體將被隱藏。
一條 using 聲明語句可以出現在全局作用域、局部作用域、命名空間作用域以及類作用域中。在類的作用域中,這樣的聲明語句只能指向基類成員。
2.3 using 指示
using 指示以關鍵字 using 開始,后面是關鍵字 namespace 以及命名空間的名字。
using 指示可以出現在全局作用域、局部作用域和命名空間作用域中,但不能出現在類的作用域中。
2.4 using 指示與作用域
using 聲明和 using 指示在作用域上的區別直接決定了它們工作方式的不同。對於 using 聲明來說,只是簡單地令名字在局部作用域內有效。相反,using 指示是令整個命名空間的所有內容變得有效。
命名空間中通常會含有一些不能出現在局部作用域的定義,因此,using 指示一般被看作是出現在最近的外層作用域中。
讓我們看一個例子
namespace blip
{
int i = 16, j = 15, k = 23;
}
int j = 0;
void maip()
{
// using 指示,blip 中的名字被“添加”到全局作用域中
using namespace blip; // 如果使用 j,則將在::j 和 blip::j 之間產生沖突
++i; // blip::i
++j; // 二義性錯誤:是全局 j 還是 blip::j?
++::j; // 全局j
++blip::j; // blip::j
int k = 100; // 隱藏 blip::k
++k; // 當前局部 k
}
2.5 頭文件與 using 聲明或指示
頭文件如果在其頂層作用域中含有 using 指示或 using 聲明,則會將名字注入到所有包含了該頭文件的文件中。
通常情況下,頭文件應該只負責定義接口部分的名字,而不定義實現部分的名字。因此,頭文件最多只能在它的函數或命名空間內使用 using 指示或 using 聲明。
3. 類、命名空間與作用域
對命名空間內部名字的查找遵循常規的查找規則:即由內向外依次查找每個外層作用域,外層作用域也可能是一個或多個嵌套的命名空間,直到最外層的全局命名空間查找過程終止。只有位於開放的塊中且在使用點之前聲明的名字才會被考慮。
對於位於命名空間中的類來說,常規的查找規則仍然適用:當成員函數使用某個名字時,首先在該成員中進行查找,然后在類中查找(包括基類),接着在外層作用域中查找,這時一個或幾個外層作用域可能就是命名空間:
namespace A
{
int i;
int k;
class C1
{
public:
C1():i(0),j(0){} // 正確,初始化 C1::i 和 C1::j
int f1(){ return k;} // 正確,返回 A::k
int f2(){ return h; } // 錯誤,h未定義
int f3();
private:
int i;
int j;
};
int h = i;
}
// 成員 f3 定義在 C1 和命名空間 A 的外部
int A::C1::f3()
{
return h; // 正確,返回 A::h f3 的定義位於 A::h 的定義之后,所以是合法的
}
限定符A::C1::f3
指出了查找類作用域和命名空間作用域的相反次序。首先查找函數 f3 的作用域,然后查找外層類 C1 的作用域,最后檢查命名空間 A 的作用域以及包含着 f3 定義的作用域。
3.1 實參相關的查找與類類型形參
考慮下面這個簡單的程序:
std::string s;
std::cin >> s;
// 等價於
operator>>(std::cin, s);
operator>> 函數定義在標准庫 string 中,string 又定義在命名空間 std 中。但是我們不用 std::限定符和 using 聲明就可以調用 operator>>。
當給函數傳遞一個類類型的對象時,除了在常規的作用域查找外還會查找實參類所屬的命名空間。這一例外對於傳遞類的引用或指針的調用同樣有效。
此例中,編譯器發現 operator>> 調用時,首先在當前作用域尋找合適函數,接着查找輸出語句的外層作用域。隨后,因為>>表達式的形參是類類型,所以編譯器還會查找 cin 和 s 的類所屬的命名空間。也就是說,對於這個調用來說,編譯器會查找定義了 istream 和 string 的命名空間 std。當在 std 中查找時,編譯器找到了 string 的輸出運算符函數。
如果沒有這個規則,那么此例中需要專門提供 using 聲明。
using std::operator>>;
// std::operator>>(std::cin,s);
3.2 查找與 std::move 和 std::forward
通常如果在程序中定義了一個標准庫中已有的名字,則會出現以下兩種情況的一種:
- 根據一般的重載規則確定調用應該執行的函數版本;
- 程序根本不執行函數的標准庫版本。
標准庫的 move 和 forward 函數,都是模板函數,在標准庫定義中它們都接受一個右值引用的函數參數,而右值引用可以匹配任何類型(c++ primer 611頁)。因此如果我們定義了一個接受單一形參的 move 函數,不管形參是什么類型,都將與標准庫版本的沖突。
因此通常書寫 std::move 而非 move,這樣就能明確知道想用的是標准庫版本。
3.3 友元聲明與實參相關的查找
友元的聲明僅僅指定了訪問的權限,不是一個通常意義上的函數聲明。當類聲明了一個友元時,該友元聲明並沒有使得友元本身可見,因此必須在友元聲明之外再專門對函數進行一次聲明。
一個另外的未聲明的類或函數第一次出現在友元聲明中,通常認為它是最近的外層空間的成員。
namespace A
{
class C
{
// 兩個友元,在友元聲明之外沒有其他聲明
// 這些函數隱式地稱為命名空間 A 的成員
friend void f2(); // 除非另有聲明,否則不會被找到
friend void f(const C&); // 根據實參相關的查找規則可以被找到
};
}
此時 f 和 f2 都是命名空間 A 的成員。即使 f 不存在其他聲明,也能通過實參相關的查找規則調用 f:
int main()
{
A::C cobj;
f(cobj); // 正確,通過接受類類型實參找到
f2(); // 錯誤,A::f2 沒有聲明
}
4. 重載與命名空間
4.1 與實參相關的查找與重載
對於接受類類型實參的函數來說,其名字查找將在實參類所屬的命名空間中進行,這條規則對於如何確定候選函數集同樣有影響。
namespace NS
{
class Quote{/* */};
void display(const Quete&) {/* */}
}
class Bulk_item : public NS::Quote{ /* */ };
int main()
{
Bulk_item book1;
display(book1); // 其候選函數不僅在調用語句所在作用域查找,還會在 Bulk_item 及其基類 Quote 所屬的命名空間中查找。
return 0;
}
4.2 重載與 using 聲明/指示
using 聲明語句聲明的是一個名字,而非一個特定的函數,using 聲明函數會將該函數名所有的版本都引入到當前域中
- 如果 using 聲明出現在局部作用域中,則引入的名字會隱藏外層作用域的相關聲明。
- 如果 using 聲明所在的作用域已有無法再重載的函數,則 using 聲明將引發錯誤。
using 指示將命名空間提升到外層作用域中。
與 using 聲明不同的是,如果引入一個與已有函數形參列表完全相同(無法重載)的函數不會報錯,調用時指明要調用的是哪個版本即可。