Learning Objectives
- 說明命名空間解決的問題:不同程式庫裡的同名函式與變數
- 定義命名空間,用範圍解析運算子 :: 指定名稱屬於哪一個命名空間
- 使用巢狀命名空間與 C++17 的 namespace a::b 簡寫
- 分辨 using 宣告與 using 指令,說明為什麼標頭檔裡不要寫 using namespace std
- 用 ::name 指定全域命名空間裡的名稱
名稱衝突
程式越寫越大,或用了別人寫的程式庫,就很容易出現同名的東西:你的程式有一個 play() 播放音樂,
另一個程式庫也有一個 play() 播放影片。兩個同名、參數也相同的函式放在一起,編譯器無法分辨,會直接報錯。
命名空間(namespace)替名稱加上一個「姓氏」,把它們分在不同的區域:
namespace music {
void play() { ... }
}
namespace movie {
void play() { ... }
}
music::play(); // 音樂的 play
movie::play(); // 電影的 play
:: 叫做範圍解析運算子(scope resolution operator),music::play 讀作「music 裡的 play」。
你其實從第一課就在用命名空間了:cout、string、vector 都放在標準函式庫的命名空間 std 裡,完整的名字是 std::cout、std::string。
程式開頭的 using namespace std; 讓你可以省略 std::,本課會說明它到底做了什麼,以及什麼時候不該用。
用命名空間區分同名的變數與函式
這個範例沒有寫 using namespace std;,所以輸出都寫成 std::cout。右邊最上方是全域變數區,兩個 volume 顯示成 music::volume 和 movie::volume,程式還沒進入 main 就先初始化好了。函式框架的標題只顯示 play,看程式碼的高亮行就能分辨是哪一個 play。
定義與巢狀命名空間
定義命名空間的寫法:
namespace 名稱 {
// 變數、函式、類別……
} // 結尾的大括號後面不用分號
幾個規則:
- 命名空間可以分很多次寫:在不同地方、甚至不同檔案寫兩次
namespace music { ... },裡面的東西會合在同一個命名空間。 - 命名空間可以巢狀:
namespace city { namespace library { ... } },裡面的名稱從外面要寫city::library::名稱。 C++17 起可以簡寫成namespace city::library { ... }。 - 在命名空間裡面使用名稱時,先找自己這一層,找不到再往外層找,所以
city::library裡可以直接用city的名稱。 - 名字太長時可以取別名:
namespace lib = city::library;,之後寫lib::info()。(本課的直譯器不支援別名,在自己的電腦上可以使用。)
巢狀命名空間
city 裡面有 library 和 gym 兩個命名空間,各有一個 openHour。第 16 行用 C++17 的簡寫把 gym 加進 city。注意第 11 行:info 在 city::library 裡,只寫 name 也能找到外層的 city::name。
想一想::: 與巢狀
下面的程式會印出什麼?
namespace a { int x = 1; }
namespace b { int x = 2; }
int x = 3;
int main() {
int x = 4;
cout << a::x << b::x << ::x << x;
}
下面的程式會印出什麼?
namespace outer {
int v = 1;
namespace inner {
int v = 2;
int get() { return v; }
}
int get() { return v; }
}
int main() {
cout << outer::inner::get() << outer::get();
}
程式沒有寫 using namespace std;,也沒有任何 using 宣告。下列哪一行可以正確印出 1?
using 宣告與 using 指令
每次都寫全名很長,using 有兩種寫法可以省略:
| 寫法 | 名稱 | 效果 |
|---|---|---|
using std::cout; |
using 宣告(using-declaration) | 只把一個名稱帶進目前的範圍 |
using namespace std; |
using 指令(using-directive) | 讓命名空間裡的所有名稱都可以不寫前綴 |
兩種都可以寫在檔案最上面(整個檔案有效),也可以寫在函式裡(只在那個函式裡有效)。
using 宣告比較精確:用到什麼就帶進什麼,不會意外引進其他名稱。using 指令最方便,但會把一大堆名稱一次攤開, 如果兩個命名空間都被攤開、又剛好有同名的東西,只寫名稱時編譯器就不知道你指的是哪一個,會報錯說這個名稱有歧義(ambiguous),這時仍然要寫全名。
using 宣告與 using 指令
第 3、4 行用 using 宣告帶進 cout 和 endl,所以整個檔案都不用寫 std::。useDeclaration 只帶進 circleArea 一個名稱;useDirective 用 using 指令帶進整個 geometry。
std 與 using namespace std
標準函式庫的所有名稱都在 std 裡,而且數量非常多:除了 cout、string,還有 count、find、max、min、swap、distance……
寫了 using namespace std;,這些名稱全都攤開在你的程式裡。如果你自己也定義了同名的東西,就可能出問題:
#include <iostream>
#include <algorithm> // 裡面有 std::count
using namespace std;
int count = 0; // 自己的全域變數也叫 count
int main() {
count++; // g++ 報錯:count 有歧義(是 ::count 還是 std::count?)
}
小程式、練習題寫 using namespace std; 很方便,本課程的範例也這樣寫。但有兩個場合要避免:
- 標頭檔(.h)裡不要寫
using namespace std;:標頭檔會被#include到別人的程式裡,等於替所有引用它的人都攤開了std, 他們完全不知情,卻可能因此遇到名稱衝突。標頭檔裡一律寫全名std::string、std::vector。 - 大型專案通常規定寫全名,或只用
using std::cout;這種 using 宣告,一眼就看得出每個名稱來自哪裡。
全域命名空間:不屬於任何命名空間的名稱(例如寫在所有函式外面的全域變數)屬於全域命名空間。
被區域變數或其他名稱遮住時,可以用前面不加名稱的 ::name 指定全域的那一個。
區域、命名空間與全域的同名變數
三個變數都叫 level:全域的 1、game 裡的 10、show 裡的區域變數 100。看第 11~13 行分別找到哪一個。
想一想:using 的使用
下面的程式用 g++ 編譯會怎樣?
#include <iostream>
#include <algorithm>
using namespace std;
int count = 0;
int main() {
count++;
cout << count;
}
為什麼不建議在標頭檔(.h)裡寫 using namespace std;?
下列哪一行只把 std::string 一個名稱帶進目前的範圍?
動手寫:兩個 area 函式
定義兩個命名空間 circle 和 square,各有一個 double area(double) 函式:圓的面積是 3.14159 * r * r,正方形的面積是 side * side。main 已經寫好,執行後的輸出要和預期相同。
重點回顧
- 命名空間替名稱分區,讓不同程式庫裡的同名函式、變數可以共存
namespace 名稱 { ... }定義命名空間,可以分次寫、可以巢狀;C++17 可寫成namespace a::b { ... }::是範圍解析運算子:a::x是 a 裡的 x,::x是全域命名空間的 x- 在命名空間裡找名稱時,先找自己這一層,再往外層找
using std::cout;(using 宣告)只帶進一個名稱;using namespace std;(using 指令)帶進全部名稱,可能造成名稱衝突- 標準函式庫都在
std裡;標頭檔裡不要寫using namespace std;,一律寫全名