列舉一:無範圍的列舉 Enum

列舉 enum 是一種複合型態,其中每一個資料都是代表著常數的變數。

在 C++ 中,列舉有分兩種。一種是無範圍的 unscoped enum,而另一種則是有範圍的 scoped enum。

在近代的 C++ 中,後者通常是用 enum class 來實作,並且也是比較常會看見的。主要原因有兩個:

  1. enum class 相較安全
  2. 在 enum class 中,變數的資料型態並不會自動被轉換成其他型態。

現在聽不懂對不對?沒關係!看完這篇文章你就可以很清楚的知道了!

無範圍 Unscoped Enum

首先,所有的列舉都會從 enum 這個關鍵字起始。

讓我們先來看看以下的範例:

// 定義一個叫 Color 的 enum
// 在這個 enum 中,所有變數都被逗點隔開
// 這些變數也代表在這個 Color 的 enum 裡的資料
enum Color
{
    red,
    green,
    blue
};

int main()
{
    // 定義型態為列舉的變數
    Color apple { red };        // 正確
    Color banana { yellow };    // 錯誤,因為在 Color 裡面我們並沒有定義 yellow

    return 0;
}

在上面的例子中,我們定義了一個叫 Color 的 enum,使用大括弧來作為起始和結束,並且在最後還有一個分號 ;

在這個列舉中,我們又定義了三個符號:redgreenblue。代表了這個 Color 目前含有這三個資料,也代表著 Color 可以有三個不同的值。

在主方程式 main() 中,我們定義了兩個資料型態為 Color 這個列舉的變數 applebanana

實際運行後,我們會發現在運行到 banana 那行時編譯器會報錯。這是因為在 Color 中,我們並沒有 yellow 這個符號。

另外,值得一提的是,習慣上我們會將 enum 型態的第一個字母設為大寫。

不同的列舉代表不同的資料型別

當我們建立一個列舉型態時,編譯器會將他視為一個獨立的資料型態。這可以確保變數只能取得特定的數值範圍,並且可以避免錯誤的資料型態分配。

但是,不同的列舉型態之間是不能互相使用的,因為即使列舉值的名稱相同,編譯器仍然會認為它們是不同的資料型態。

因此,使用列舉型態可以幫助程式設計師更容易地管理程式中的常數,從而提高程式的可讀性和可維護性。

以下我們提供一個範例來解釋獨立的資料型態的意思:

enum Animal
{
    bird,
    cat,
    dog
};

enum Color
{
    red,
    green,
    blue,
};

int main()
{
    Animal my_animal { red }; // 錯誤,因為在 Animal 中,並沒有 red 這個資料
    Color  my_color { dog };  // 錯誤,因為在 Color 中,並沒有 dog 這個資料

    return 0;
}

運行程式後,我們可以看到編譯器會出現錯誤。這是因為在個別的列舉中,我們並沒有定義相關的資料。

Unscoped 無範圍的意思

無範圍的意思是因為列舉值符號與列舉型態定義本身存在於相同的範圍內,而不是一個不同的範圍區域。

舉例來說:

// 這些在 enum 裡的符號都是被定義在我們所謂的 global scope 裡
// 和 Color 定義在同一個空間範圍
enum Color
{
    red,
    green,
    blue
};

enum Animal
{
    bird,
    cat,
    blue   // 錯誤,因為在 Color 裡面我們也定義了擁有同樣名字的符號
};

int main()
{
    Color apple { red };
    return 0;
}

上面的例子中,我們在兩個不同的 enum 裡建立的擁有相同名字的符號 blue。這樣會造成在同一個空間範圍裡,出現兩個相同的符號,使得編譯器無法判別其中的差別。

那我們有辦法避免嗎?其實有的,第一種方法其實也是最簡單的,就是記得把每個符號都設有不同的名字。但是這種方法在大型的程式碼或是多人合作的專案中,依然有非常高的危險性。

第二種方法就是創建獨立的名稱空間,而這也是比較推薦的。

名稱空間並不是我們這章主要討論的內容,因此只會稍微帶過,我們有一章專門講解什麼是 名稱空間

以下我們直接來看使用獨立命名空間的解決辦法:

// 使用關鍵字 namespace 來創建叫做 color 的名稱空間
namespace color
{
    enum Color
    {
        // red, green, blue 三者都在 color 的名稱空間
        red,
        green,
        blue
    };
}

// 使用關鍵字 namespace 來創建叫做 animal 的名稱空間
namespace animal
{
    enum Animal
    {
        // bird, cat, blue 三者都在 animal 的名稱空間
        bird,
        cat,
        blue
    };
}

int main()
{
    color::Color my_color { color::blue };
    animal::Animal my_animal { animal::blue };

    return 0;
}

在上面的例子中,我們透過利用關鍵字 namespace 來創建獨立的命名空間。然後將原本的列舉 ColorAnimal 放進個別的命名空間。

這麼一來,即使有重複名字的符號,他們也是各自存在於不同的空間,編譯器這樣就可以分辨了!

但是值得注意的是,如果給了獨立的命名空間,那麼我們在想要存取他們的值時,就必須透過命名空間來存取。也就是在主函式中的 color::blue 以及 animal::blue

列舉的比較

我們可以利用 ==!= 來比較列舉值。

比如說:

enum Color
{
    red,
    green,
    blue,
};

int main()
{
    Color apple{ red };

    if (apple == red)
        std::cout << "Apple is red!";
    else
        std::cout << "Apple is not red!";

    return 0;
}

雖然我們前面提到不同的列舉代表著不同的資料型態,但是這不代表無範圍的列舉是型態安全的!

有時候編譯器會容許一些奇怪的行為發生。這是什麼意思呢?我們來看看下面這段程式碼:

enum Animal
{
    bird,
    cat
};

enum Color
{
    red,
    green
};

int main()
{
    Animal animal { bird };
    Color color { red };

    if (animal == color) // 編譯器會使用整數型態比較這兩個列舉值,這裡兩個列舉值都是 0
        std::cout << "color and animal are equal\n";
    else
        std::cout << "color and animal are not equal\n";

    return 0;
}

以上的程式碼會被成功執行,並且被編譯器理解為兩個列舉值相同!

這是因為當 animalcolor 進行比較時,編譯器會嘗試確定是否能夠進行比較。由於編譯器無法直接比較 animalcolor,它會嘗試將它們轉換為整數並加以匹配。

最終,編譯器會發現只有當它們都轉換為整數時才能進行比較。由於 animalcolor 都被定義為列舉值,並會被轉換為整數值 0(因為兩個都是在各自列舉的第一個),所以比較結果為兩者相等。

為了要解決這個問題,我們因此有了有範圍的 scoped enum:enum class

寫到這裡文章已經有點長了,讓我們在這裡把列舉 enum 分為上下兩章,在下一章講解什麼是 有範圍的 scoped enum