名稱空間 Namespace

在上一章 外部連結 Internal Linkage 中我們介紹了外部連結的概念。這個概念讓我們學到了有些參數、或是預設函數都可以被其他文件看到。

在這章中,我們要來介紹新的概念,名稱空間 Namespace,包括我們為什麼需要它、要如何使用等等。

什麼是名稱空間

遇到的問題

在了解什麼是名稱空間 Namespace 之前,我們先來了解一下我們遇到什麼樣的問題。

前一章我們提到,函數預設會有外部連結,因此一份檔案可以看到其他檔案內的函數。

那這樣的話,如果同時有兩個相同名稱的函數存在於不同的檔案,程式碼要如何知道要取用的是哪一個函數呢?

我們直接來看例子

fileA.cpp

int add(int x, int y)
{
    return x + y;
}

fileB.cpp

int add(int x, int y)
{
    return x + y + 1;
}

main.cpp

int add(int x, int y);    // 前向聲明 forward declaration

int main()
{
    add(3, 5);    // 是哪一個 add?
    return 0;
}

在上面的例子中,我們應該就可以很清楚的了解到我們遇到了什麼樣的問題。

如果在兩個不同的文件中,都有相同名稱的 add(int x, int y) 函數,那麼在 main.cpp 中,程式碼需要使用哪一個檔案的呢?

如果我們分開編譯兩個檔案,那麼個別都可以編譯成功,因為程式碼只會看到一個 add(int x, int y) 函數,但如果放在同一個專案一起編譯就不會成功。

我們可以試圖編譯這樣的專案,我們會發現編譯器給出類似這樣的錯誤:

fileB.cpp:2: multiple definition of `add(int, int)'; fileA.cpp:2: first defined here

如何解決

這樣的問題要怎麼解決呢?沒錯!就是我們今天要介紹的名稱空間 Namespace!

名稱空間讓這些擁有相同名字的變數或是函數可以存在於不同的名稱之下,這樣一來編譯器就可以區分了!

我們可以想像小明和小王手上都有蘋果,如果你說:「我要那個蘋果」,沒有人會知道你在說哪一顆蘋果。

但如果你說:「我要小明的蘋果」,那大家都會知道你要的是哪顆蘋果!即使有兩個一模一樣的蘋果。

名稱空間就是在程式碼中創立小明和小王,讓編譯器能夠清楚知道我們到底是要哪顆蘋果!

創造名稱空間

好啦那我們知道名稱空間的意義了,現在我們要來學習到底要如何創造一個名稱空間。

創造一個名稱空間非常簡單,就是利用關鍵字 namespace 和以下的規則就好啦!

namespace  // 你想要的名稱
{
    // 裡面可以放各種物件、變數、或是函數
    int a;
    float b;
    bool check();
    ...
}

我們一樣拿前一節的例子,並套用上面的規則重新改寫一下

fileA.cpp

namespace FileA
{
    int add(int x, int y)
    {
        return x + y;
    }
}

fileB.cpp

namespace FileB
{
    int add(int x, int y)
    {
        return x + y + 1;
    }
}

在上面的例子中,我們創造了兩個名稱空間 FileAFileB,這個名稱是隨意的,你可以創造任意你喜歡的名稱。

接著,我們將函數 add(int x, int y) 放到相對應的名稱空間。這樣就完成啦!這樣一來,我們就可以讓編譯器知道他在呼叫時,是呼叫屬於誰的函數了!

使用名稱空間

名稱空間創造好之後,我們要如何使用呢?

這邊我們要使用一個叫做 scope resolution operator :: 的東西,中文叫做範圍解析運算子。我們必須透過這個運算子來取得名稱空間裡的內容物。

一樣我們使用上面的例子,並在 main.cpp 中取用函數

main.cpp

int main()
{
    std::cout << FileA::add(3, 5) << std::endl;    // 取用 FileA 空間中的函數
    std::cout << FileB::add(3, 5) << std::endl;    // 取用 FileB 空間中的函數
}

我們可以看到輸出結果會是

8
9

程式碼的確知道我們想要使用的函數了!

其他應用

相同檔案

不同的名稱空間也可以存在於同一個檔案中,比如說

int add(int x, int y)
{
    return x + y;
}

namespace Arthur
{
    int add(int x, int y)
    {
        return x + y + 1;
    }
}

namespace Taco
{
    int add(int x, int y)
    {
        return x + y + 2;
    }
}

int main()
{
    add(3, 5);          // 呼叫第一個 add()
    ::add(3, 5);        // 呼叫第一個 add()
    Arthur::add(3, 5);  // 呼叫 Arthur 裡面的 add()
    Taco::add(3, 5);    // 呼叫 Taco 裡面的 add()

    return 0;
}

這邊我們可以看到一個特殊的用法

::add(3, 5)

範圍解析運算子的前面並沒有一個名稱,這樣也是可以的喔!但這樣的效果就跟一般的呼叫沒什麼兩樣!

巢狀名稱空間

迴圈 那章中,我們提到巢狀迴圈的概念。相同的概念也可以應用在名稱空間中喔,英文叫做 nested namespace!

namespace Arthur
{
    namespace Taco
    {
        int add(int x, int y)
        {
            return x + y;
        }
    }
}

int main()
{
    Arthur::Taco::add(3, 5);
    return 0;
}

只是當然我們就需要使用到兩個運算子來取得內容物。

這樣的結構在普通的小專案中可能不是那麼常見,但在大型的專案中,其實是非常常見的喔!

std 名稱空間

我們了解名稱空間後,你有沒有發現,為什麼我們之前在使用 cout 時前面要加一個 std:: ?

沒錯!這裡的 std 也是一個名稱空間,是 standard 的縮寫,只不過是由 C++ 官方創造的!

其實,只要是使用 C++ 資料庫中的東西,我們前面都需要加上 std::,包括 std::vectorstd::stringstd::map 等等。

現在知道為什麼了吧!因為這樣做的話 C++ 就可以確保它所創造的並不會那麼輕易的和使用者所創造的相衝突!

總結

這章我們了解了什麼是名稱空間,包括它為什麼存在、它長怎麼樣、它的應用等等。

基本上,名稱空間的存在就是為了防止名稱的重複,非常簡單。

下一章我們要來了解更為重要的概念:局部變數和全局變數。