先頭へ戻る

UTF-8 | Programming Place Plus 新C++編

Programming Place Plus トップページ新C++編

先頭へ戻る

このページの概要

このページでは、ポーカープログラムの締めくくりとして、トランプのマークを絵文字で表現できるようにしてみます。そのために、UTF-8 という文字エンコーディング形式を使います。Windows および Visual Studio を使った環境ではいくつか理解しなければならないことがあるので、その辺りの話を中心にして、UTF-8 そのものについては(まじめにやると難しいので)あまり深入りせずに進めます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



Unicode

ポーカープログラムの締めくくりとして、トランプのマークを「♠♣♦♥」のような絵文字で出力できるようにしてみます。

実際の見た目は使っているフォントによって異なります。また、フォントによっては表示できないかもしれません。

トランプで使うこれらのマークは、ASCII や Shift_JIS (CP932) の文字集合には含まれていません(「マルチバイト文字」のページを参照)。そこで、Unicode と呼ばれる文字コード体系を使うようにします。Unicode は、エンコーディング形式と文字集合をそれぞれ定めた規格になっています。Unicode のエンコーディング形式にはいくつか種類があり、このページのテーマである UTF-8 はそのうちの1つです。このほか UTF-16UTF-32 が有名どころです。

Unicode の文字集合には世界中のさまざまな文字が含まれており、たびたび改訂されて新しい文字が増え続けています。この中にトランプのマークも含まれています。Unicode の文字集合内の文字を区別するために、文字集合内で固有の整数表現があり、コードポイント (code point) と呼ばれています。Unicode のコードポイントによる表現と、UTF-8 などのエンコーディング形式による表現は異なるものです。

UTF-8 は1つの文字を 8ビット(1バイト)単位の大きさで表現します。最低で 8ビット(1バイト)、最大で 32ビット(4バイト)です。ASCII で表現できる文字ならば、UTF-8 で表現しても ASCII で表現しても、8ビットに収まる同じ整数になります。

UTF-8 には BOM(バイト順マーク) (Byte Order Mark) という概念があります。BOM は、ファイルの先頭に付加される、ある決まった並びの 3バイトのマークのことです。BOM があることによって、以降のデータが UTF-8 で表現されることをソフトウェアが認識しやすくなります。ただ、BOM は必須ではないため、BOM を認識しないソフトウェアもあり、その場合は BOM があると正常に読めないことがあります(BOM の部分もデータの一部であるように扱ってしまう)。そのため、UTF-8 を使うなら、BOM の有無に関する違いがあることを理解しておかなければなりません。BOM がある UTF-8 を「BOM付きの UTF-8」などと表現し、BOM がない UTF-8 を「BOMなしの UTF-8」とか「UTF-8N」などと表現します。

UTF-8 の BOM は 16進数表記で 0xEF 0xBB 0xBF です。

ソースファイルのエンコーディング形式

C++ のプログラムで UTF-8 を扱うのなら、ソースファイル上に UTF-8 の文字を書けなければ困る場合が多いでしょう。そこでまず、ソースファイル自体のエンコーディング形式を UTF-8 にします。大抵のテキストエディタには、エンコーディング形式を指定する機能があります。

【上級】現在のエンコーディング形式で表記できない文字を、ユニバーサル文字名という仕組みを使って表記する方法もあります。\uNNNN\UNNNNNNNNN という表記方法で、N のところに ISO/IEC 10646(Unicode とほぼ同じもの)のコードポイントを記述すると、該当する文字に置き換えられます。1

Visual Studio では、ソースファイルのエンコーディング形式を次の順序で判断します2

  1. ソースファイルに明確なマークがあれば、それに従う
  2. コンパイラオプションによる指定があれば、それに従う
  3. 現在のコードページの設定に従う

特に何もせずソースファイルを作成していたら、3番のところまで進んで、CP932(Shift_JIS)になっているでしょう。

Windows を除くほとんどの現代の環境では、UTF-8 というエンコーディング形式を使っているため、Windows も UTF-8 がデフォルトになるように変わる可能性があります。今のところのデフォルト設定は CP932 ですが、UTF-8 に変更することは可能になっています。Windows 10 の設定画面より、【時刻と言語】>【言語】>【管理用の言語の設定】と進み、【管理】タブの【システムロケールの変更】を選択。【ベータ: ワールドワイド言語サポートで Unicode UTF-8 を使用】にチェックを入れると、UTF-8 に変更できます。ただしあくまでベータ版扱いの機能ですし、Windows上で動作するすべてのプログラムに影響を与える設定なので、過去に作られたプログラムが誤動作する可能性もあります。

新C++編としては、確実に UTF-8 と判断させるために、ソースファイルのエンコーディング形式を UTF-8 (BOM付き) にしておくことにします。BOM は、上記の順序のうちの1番のところにある「明確なマーク」に該当します。

BOM があって都合が悪い環境では、UTF-8N を使ってください。

Visual Studio で、ソースファイルを UTF-8 (BOM付き) で保存するには、設定を変えておくか、適当なテキストエディタを使って UTF-8 (BOM付き) にしておきます。Visual Studio での設定方法については、「Visual Studio編>ソースファイルのエンコーディング形式」を参照してください。

文字列リテラル、文字リテラルのエンコーディング形式

'あ' とか "あいうえお" のように記述した文字リテラル、文字列リテラルのエンコーディング形式は処理系定義です。Visual Studio の場合は、実行文字集合 (execution character set) に従うことになっています3が、これはソースファイルのエンコーディング形式を決めるときと同じ手順で判断されるので、おそらく CP932 になるでしょう。

リテラルはソースファイル内に記述するものなので、ソースファイルのエンコーディング形式と同じであるようにも思えますが、実際には独立して考えることになります。Visual Studio では、ソースファイルを UTF-8 (BOM付き) にしても、文字リテラルや文字列リテラルが UTF-8 になるわけではありません。

文字リテラルや文字列リテラルの直前に、以下の表に挙げるプリフィックス(接頭辞) (prefix) を付加することによって、エンコーディング形式を強制します。

プリフィックス エンコーディング形式
u8 UTF-8 (※C++14 では文字リテラルのみ) char (※C++20 から char8_t)
L 処理系定義のワイド文字エンコーディング wchar_t
u 処理系定義(多くの場合 UTF-16) char16_t
U 処理系定義(多くの場合 UTF-32) char32_t
なし 処理系定義 char

「型」は、1文字を表現するために用いる型です。

【C++20】uプリフィックスのエンコーディング形式は UTF-16、Uプリフィックスのエンコーディング形式は UTF-32 であることが規定されました。4

【C++20】u8プリフィックスを付加した場合の型は char8_t に変更されました。また、char8_t型による文字列を扱う std::u8string も追加されています。5

プリフィックスとは、先頭に付け足すことで何かしらの意味を持つ表記のことです。たとえば、u8プリフィックスは u8"あいうえお" のように用い、こう記述すると、UTF-8 による “あいうえお” であると認識されます。C++14 では、文字リテラルに u8プリフィックスを付加することはできません。

【C++17】文字リテラルに対して u8プリフィックスを付加することが可能になりました。6

今のところ、使うのは u8 だけです。L、u、U もさまざまな文字を取り扱う方法として選択肢に上げられるものではありますが、型が異なっており、文字の扱い方そのものに違いがあるので、当面は取り扱わないことにします。

今後、新C++編では原則として、文字列リテラルに u8プリフィックスを付加し、UTF-8 で取り扱うことにします。


u8プリフィックスを付加しても文字の型は char型のままです(C++20 で変更されましたが、以下の話題には違いはありません)。char型の大きさは 1バイトですが、UTF-8 では 1文字を 1~4バイトで表現するため、1つの char型の変数や std::string の 1つの要素に、UTF-8 の文字が入る保証はありません。この辺りの事情は、「マルチバイト文字」のページで Shift_JIS を例に取り上げたことと同様です。


次のプログラムは、UTF-8 で記述した文字列リテラルの内容を test.txt に書き出しています。標準出力に出力しないのは、Windows環境での事情があるためです。

#include <fstream>

int main()
{
     std::ofstream ofs(u8"test.txt");
     ofs << u8"あいうえお\n";
}

実行結果(test.txt の内容。UTF-8 で表示):

あいうえお

実行結果(test.txt の内容。CP932 で表示):

縺ゅ>縺�縺医♀

test.txt を UTF-8 で開くと正しく表示されますが、CP932 で開くと滅茶苦茶な表示になります。エンコーディング形式の違いによって、正常に内容を表示したり、読んだりできなくなる現象を文字化け (Mojibake) と呼びます。

コマンドプロンプトでの表示と入力

文字列を test.txt に出力して、テキストエディタでエンコーディング形式を指定して確認する方法を採りましたが、当然、結果を画面(標準出力)で確認したい場合もあります。しかし、Windows ではこれがなかなか厄介です。

u8プリフィックスを付加した文字列リテラルを、標準出力へ出力するようにして、Visual Studio 上で実行してみます。

#include <iostream>

int main()
{
    std::cout << u8"あいうえお\n";
}

実行結果(コマンドプロンプト上の表示):

縺ゅ>縺・∴縺

結果がコマンドプロンプト上で表示されますが、文字化けを起こしてしまいます。このような結果になるのは、コマンドプロンプトが CP932 を使っているためです。

これも結局は Windows の設定によるもので、「ソースファイルのエンコーディング形式」の項のコラムで取り上げた方法で UTF-8 に変更できますが、正式な機能とはなっていません。

コマンドプロンプト上で UTF-8 の文字を正常に表示させるには、設定を切り替える必要があります。コマンドプロンプトを開くたびに、設定を変更するコマンドを入力する方法もありますが、その方法では Visual Studio からの実行には対応できませんし、知識のないユーザーに毎回その対応をさせることも難しいでしょう。

コマンドプロンプト上でのコマンド入力による方法は、「Windows編>コマンドプロンプト - コードページ(言語)を切り替える」のページで取り上げています。

そこで、ソースコードの方で対応する方法があります。SetConsoleOutputCP関数7を使うと、コマンドプロンプトで表示するときに使うエンコーディング形式を指定できます。たとえば、SetConsoleOutputCP(CP_UTF8) のように呼び出すと UTF-8 になります。

SetConsoleOutputCP関数は Windows が独自に提供しているものであって、C++ の標準規格として定義されているものではありません。Windows以外ではコンパイルを通せないので注意してください。この関数を使うためには、<Windows.h> という、Windows が用意しているヘッダファイルをインクルードする必要があります。Windows でしか使えないコードは #ifdef _WIN32#endif で囲んでおけば、ほかのコンパイラでのコンパイルを妨げずに済みます。_WIN32 は、Visual Studio が独自で定義する事前定義マクロの一種です8

#include <iostream>
#ifdef _WIN32
#include <Windows.h>
#endif

int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);
#endif

    std::cout << u8"あいうえお\n";
}

実行結果(コマンドプロンプト上の表示):

あいうえお

入力の方はより深刻で、コマンドプロンプトから UTF-8 の文字列を入力して、標準入力として受け取ることは基本的にできないと思ってよさそうです9。SetConsoleOutputCP関数に対応して、入力側の設定を行う SetConsoleCP関数10が存在しますが、SetConsoleCP(CP_UTF8) としてみても、変化は起こるものの、正常な入力は得られません。

回避策として、テキストファイルに UTF-8 で文字データを書き込み、リダイレクトによって入力する方法があります(「Windows編>リダイレクト」を参照)。

ASCII で表現できる文字ならば UTF-8 のつもりで扱えるので、新C++編では、標準入力からの入力は、ASCII で表現できる文字に限定するようにします。

外部ファイルのエンコーディング形式

次に、外部のファイルのエンコーディング形式です。つまり、読み取ろうとしているファイルの中身が、どんなエンコーディング形式で書かれているのか。また、ファイルに書き込むとき、どんなエンコーディング形式で書き込むのかということです。

新C++編としては UTF-8 (BOMなし) に統一するという方針を採ります。ここでは BOM は付けません。BOM が付いていると、読み込み時に先頭の 3バイトを取り除く余計な処理が必要になるためです。

UTF-8 のテキストファイルは、これまでどおり std::ifstream や std::ofstream を使って普通に読み書きできます。

次のプログラムは、test.txt に UTF-8 で書き込んでいます。

#include <fstream>

int main()
{
    std::ofstream ofs(u8"test.txt");
    ofs << u8"あいうえお\n";
}

実行結果 (test.txt の内容。UTF-8 で表示):

あいうえお

test.txt の内容は UTF-8 で正しく読める状態になっています。

今度は test.txt を読み込んでみます。

#include <fstream>
#include <iostream>
#include <string>
#ifdef _WIN32
#include <Windows.h>
#endif

int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);  // コマンドプロンプトでの表示に UTF-8 を使う
#endif

    std::ifstream ifs(u8"test.txt");
    std::string s {};
    ifs >> s;
    std::cout << s << u8"\n";
}

実行結果 (UTF-8 で表示):

あいうえお

読み込んだ文字列をそのまま標準出力へ出力しているだけです。結果は UTF-8 で正しく読めるようになっています。

ポーカープログラムの修正

それでは、トランプのマークを絵文字で表示できるように、ポーカープログラムを修正してみましょう。

現在のポーカープログラムは、「プリプロセス」のページの練習問題にあります。トランプのマークの文字列表現を、get_mark_string関数が返していますが、マクロの定義に応じて、アルファベット表現で返すか、カタカナ表記で返すかを切り替えられるようになっています。そこで、3つ目の選択肢として、「絵文字で返す」を追加する方針にしてみます。

切り替えに使うマクロは card.h で定義していますが、新たに CARD_MARK_STRING_EMOJI マクロを追加します。次のように修正して、3つのパターンから選べるようにしておきます。

#define CARD_MARK_STRING_EMOJI      0   // 絵文字
#define CARD_MARK_STRING_KANA       1   // カタカナ
#define CARD_MARK_STRING_ALPHABET   2   // アルファベット
#define CARD_MARK_STRING            CARD_MARK_STRING_EMOJI  // カードマークの文字列表現の方法

CARD_MARK_STRINGマクロの置換結果を、CARD_MARK_STRING_*** のいずれかに書き換えてビルドすることで、コードを切り替えます。

すべてのソースファイル、ヘッダファイルのエンコーディング形式を UTF-8 (BOM付き) に変更し、文字列リテラルには u8プリフィックスを付加しておきます。Windows環境では、コマンドプロンプトへ正常に出力できるように、SetConsoleOutputCP関数の呼び出しも必要です。

最終的に次のようになります。

// main.cpp
#include <iostream>
#include <sstream>
#ifdef _WIN32
#include <Windows.h>
#endif
#include "card.h"
#include "poker_hand.h"

constexpr CardNumber nothing_card_number = -1;  // カードがないことを表すダミー値

// 手札を配る
static void hand_out_cards(cards_type& deck, cards_type& hand_cards);

// カードを補充する
static void reload_cards(cards_type& deck, cards_type& hand_cards);

// カードの情報を出力する
static void print_cards(const cards_type& cards);

// 捨てるカードを選ぶときの出力
static void print_choose_discard(const cards_type& cards, const std::vector<bool>& is_discard);

// カードの文字列表現を返す
static std::string get_card_string(const Card& card);


int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);  // コマンドプロンプトでの表示に UTF-8 を使う
#endif

    while (true) {

        // 山札を準備
        cards_type deck(trump_card_num);
        init_trump(deck);
        shuffle_cards(deck);

        // 最初の手札を配る
        cards_type player_cards {};
        hand_out_cards(deck, player_cards);

        // ユーザーにカードを提示
        std::cout << u8"あなたに配られたカードです。\n";
        print_cards(player_cards);

        // 捨てるカードを選ばせる
        std::vector<bool> is_discard(hand_card_num);
        while (true) {
            print_choose_discard(player_cards, is_discard);

            std::string input_string {};
            std::getline(std::cin, input_string);
            if (!std::cin) {
                std::cout << u8"入力が正しくありません。\n";
            }
            else if (input_string.empty()) {
                break;
            }
            else {
                int index {input_string[0] - '0'};  // 数字から整数に変換
                if (0 <= index && index < hand_card_num) {
                    is_discard[index] = !is_discard[index];
                }
                else {
                    std::cout << u8"入力が正しくありません。\n";
                }
            }
        }

        // 選ばれたカードを捨てる
        for (std::vector<bool>::size_type i = 0; i < is_discard.size(); ++i) {
            if (is_discard.at(i)) {
                auto& card = player_cards.at(i);
                std::cout << get_card_string(card) << u8" を捨てました。\n";

                // 何もないことをあらわすダミー値を入れる
                card.number = nothing_card_number;
            }
        }
        std::cout << u8"\n";

        // 新しいカードを補充する
        reload_cards(deck, player_cards);
        sort_cards(player_cards);
        std::cout << u8"\n";
        print_cards(player_cards);

        // 役判定
        auto poker_hand = judge_poker_hand(player_cards);
        if (poker_hand == PokerHand::no_pair) {
            std::cout << u8"残念でした。\n";
        }
        else {
            std::cout << get_poker_hand_string(poker_hand) << u8"ができました。\n";
        }

        // ゲームの続行確認
        std::cout << u8"\n"
                  << u8"ゲームを続けますか?\n"
                  << u8"はい: Y  いいえ: N\n";
        std::string input_string {};
        std::getline(std::cin, input_string);
        if (input_string == u8"Y" || input_string == u8"y") {
            continue;
        }
        else {
            break;
        }
    }
}


// 手札を配る
static void hand_out_cards(cards_type& deck, cards_type& hand_cards)
{
    while (hand_cards.size() < hand_card_num) {
        hand_cards.push_back(deck.back());
        deck.pop_back();
    }
    sort_cards(hand_cards);
}

// カードを補充する
static void reload_cards(cards_type& deck, cards_type& hand_cards)
{
    // 何もないことをあらわすダミー値が入っているところに、新しいカードを補充する
    for (cards_type::size_type i = 0; i < hand_cards.size(); ++i) {
        if (hand_cards.at(i).number == nothing_card_number) {
            auto& card = deck.back();

            std::cout << get_card_string(card) << u8" が配られました。\n";
            hand_cards.at(i) = card;
            deck.pop_back();
        }
    }
}

// カードの情報を出力する
static void print_cards(const cards_type& cards)
{
    for (cards_type::size_type i = 0; i < cards.size(); ++i) {
        auto& card {cards.at(i)};

        if (card.number == nothing_card_number) {
            std::cout << i << u8": \n";
        }
        else {
            std::cout << i << u8": " << get_card_string(card) << u8"\n";
        }
    }
}

// 捨てるカードを選ぶときの出力
static void print_choose_discard(const cards_type& cards, const std::vector<bool>& is_discard)
{
    std::cout << u8"\n"
              << u8"左端の番号を入力して、カードを選んでください。\n"
              << u8"何も入力せず Enterキーを押すと決定します。\n";

    for (cards_type::size_type i = 0; i < cards.size(); ++i) {
        std::cout << i << u8": " << get_card_string(cards.at(i));

        // 捨てるカードに含まれているか
        if (is_discard[i]) {
            std::cout << u8" <-- 捨てる";
        }

        std::cout << u8"\n";
    }
}

// カードの文字列表現を返す
static std::string get_card_string(const Card& card)
{
#if CARD_MARK_STRING == CARD_MARK_STRING_KANA
    return get_mark_string(card.mark) + u8"の" + get_card_number_string(card.number);
#else
    return get_mark_string(card.mark) + u8" " + get_card_number_string(card.number);
#endif
}
// poker_hand.cpp
#include "poker_hand.h"
#include <algorithm>
#include <cassert>

// ストレートが成立しているか判定する
static bool is_straight(const cards_type& cards)
{
    // [!] 1 を巻き込むストレートは、1・10・11・12・13 か 1・2・3・4・5 のいずれかしか認めないルールもあるが、
    //     ここでは、とにかく連続していればいいことにしている。

    // 13 から 1 に戻ってくるケースを先に判定
    if (cards[0].number == 1 && cards[4].number == 13) {
        return cards[1].number ==  2 && cards[2].number ==  3 && cards[3].number ==  4
            || cards[1].number ==  2 && cards[2].number ==  3 && cards[3].number == 12
            || cards[1].number ==  2 && cards[2].number == 11 && cards[3].number == 12
            || cards[1].number == 10 && cards[2].number == 11 && cards[3].number == 12
            ;
    }

    return cards[0].number + 1 == cards[1].number
        && cards[1].number + 1 == cards[2].number
        && cards[2].number + 1 == cards[3].number
        && cards[3].number + 1 == cards[4].number
        ;
}

// 役を判定する
PokerHand judge_poker_hand(const cards_type& cards)
{
    assert(cards.size() == hand_card_num);
    assert(std::is_sorted(std::cbegin(cards), std::cend(cards), cards_sort_compare));

    // フラッシュ系
    if (cards[0].mark == cards[1].mark
     && cards[0].mark == cards[2].mark
     && cards[0].mark == cards[3].mark
     && cards[0].mark == cards[4].mark
    ) {
        // ロイヤルストレートフラッシュ
        if (cards[0].number == 1
         && cards[1].number == 10
         && cards[2].number == 11
         && cards[3].number == 12
         && cards[4].number == 13
        ) {
            return PokerHand::royal_straight_flush;
        }

        // ストレートフラッシュ
        if (is_straight(cards)) {
            return PokerHand::straight_flush;
        }
        
        // フラッシュ
        return PokerHand::flush;
    }

    // ストレート
    if (is_straight(cards)) {
        return PokerHand::straight;
    }

    // 数字ごとのカードの枚数を数える
    std::vector<int> number_count(each_mark_card_num + 1);
    for (auto& card : cards) {
        number_count.at(card.number) += 1;
    }

    // 一番枚数が多い数字
    int number_count_max {*std::max_element(std::cbegin(number_count), std::cend(number_count))};

    // フォーカード
    if (number_count_max == 4) {
        return PokerHand::four_cards;
    }

    // フルハウス or スリーカード
    if (number_count_max == 3) {

        // 2枚ある番号が存在すればフルハウス
        if (std::find(std::cbegin(number_count), std::cend(number_count), 2) != std::cend(number_count)) {
            return PokerHand::full_house;
        }

        // スリーカード
        return PokerHand::three_cards;
    }

    // ツーペア or ワンペア
    if (number_count_max == 2) {

        // 2枚ある番号が2つあればツーペア
        if (std::count(std::cbegin(number_count), std::cend(number_count), 2) == 2) {
            return PokerHand::two_pairs;
        }

        // ワンペア
        return PokerHand::one_pair;
    }

    // ノーペア
    return PokerHand::no_pair;
}

// 役の文字列表現を返す
std::string get_poker_hand_string(PokerHand poker_hand)
{
    switch (poker_hand) {
    case PokerHand::no_pair:                return u8"ノーペア";
    case PokerHand::one_pair:               return u8"ワンペア";
    case PokerHand::two_pairs:              return u8"ツーペア";
    case PokerHand::three_cards:            return u8"スリーカード";
    case PokerHand::straight:               return u8"ストレート";
    case PokerHand::flush:                  return u8"フラッシュ";
    case PokerHand::full_house:             return u8"フルハウス";
    case PokerHand::four_cards:             return u8"フォーカード";
    case PokerHand::straight_flush:         return u8"ストレートフラッシュ";
    case PokerHand::royal_straight_flush:   return u8"ロイヤルストレートフラッシュ";
    default:                                return u8"";
    }
}
// poker_hand.h
#ifndef POKER_HAND_H_INCLUDED
#define POKER_HAND_H_INCLUDED

#include <string>
#include "card.h"

constexpr auto hand_card_num = 5;               // 手札の枚数

// 役
enum class PokerHand : signed char {
    no_pair,
    one_pair,
    two_pairs,
    three_cards,
    straight,
    flush,
    full_house,
    four_cards,
    straight_flush,
    royal_straight_flush,
};

// 役を判定する
// cards: 手札 (要素数は 5。card.h の cards_sort_compare() が定義する順序で整列されていなければならない)
// 戻り値: 役
PokerHand judge_poker_hand(const cards_type& cards);

// 役の文字列表現を返す
// poker_hand: 役
// 戻り値: 役の名前
std::string get_poker_hand_string(PokerHand poker_hand);

#endif
// card.cpp
#include "card.h"
#include <algorithm>
#include <random>

// トランプを初期化する
void init_trump(cards_type& cards)
{
    for (auto i = 0; i < each_mark_card_num; ++i) {
        auto number = static_cast<CardNumber>(i + 1);
        cards.at(i + each_mark_card_num * 0).number = number;
        cards.at(i + each_mark_card_num * 0).mark = CardMark::spade;
        cards.at(i + each_mark_card_num * 1).number = number;
        cards.at(i + each_mark_card_num * 1).mark = CardMark::club;
        cards.at(i + each_mark_card_num * 2).number = number;
        cards.at(i + each_mark_card_num * 2).mark = CardMark::diamond;
        cards.at(i + each_mark_card_num * 3).number = number;
        cards.at(i + each_mark_card_num * 3).mark = CardMark::heart;
    }
}

// カードをシャッフルする
void shuffle_cards(cards_type& cards)
{
    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());
    std::shuffle(std::begin(cards), std::end(cards), rand_engine);
}

// カードを整列する
void sort_cards(cards_type& cards)
{
    std::sort(std::begin(cards), std::end(cards), cards_sort_compare);
}

// カードの整列に使う比較関数
bool cards_sort_compare(const Card& a, const Card& b)
{
    if (a.number == b.number) {
        return a.mark < b.mark;
    }
    return a.number < b.number;
}

// カードの数字の文字列表現を返す
std::string get_card_number_string(CardNumber number)
{
    switch (number) {
    case 1:     return u8"A";
    case 2:     return u8"2";
    case 3:     return u8"3";
    case 4:     return u8"4";
    case 5:     return u8"5";
    case 6:     return u8"6";
    case 7:     return u8"7";
    case 8:     return u8"8";
    case 9:     return u8"9";
    case 10:    return u8"10";
    case 11:    return u8"J";
    case 12:    return u8"Q";
    case 13:    return u8"K";
    default:    return u8"";
    }
}

// カードのマークの文字列表現を返す
std::string get_mark_string(CardMark card_mark)
{
#if CARD_MARK_STRING == CARD_MARK_STRING_EMOJI
    switch (card_mark) {
    case CardMark::spade:
        return u8"♠";
    case CardMark::club:
        return u8"♣";
    case CardMark::diamond:
        return u8"♦";
    case CardMark::heart:
        return u8"♥";
    default:
        return u8"";
    }
#elif CARD_MARK_STRING == CARD_MARK_STRING_KANA
    switch (card_mark) {
    case CardMark::spade:
        return u8"スペード";
    case CardMark::club:
        return u8"クラブ";
    case CardMark::diamond:
        return u8"ダイヤ";
    case CardMark::heart:
        return u8"ハート";
    default:
        return u8"";
    }
#elif CARD_MARK_STRING == CARD_MARK_STRING_ALPHABET
    switch (card_mark) {
    case CardMark::spade:
        return u8"spade";
    case CardMark::club:
        return u8"club";
    case CardMark::diamond:
        return u8"diamond";
    case CardMark::heart:
        return u8"heart";
    default:
        return u8"";
    }
#else
    #error "invalid CARD_MARK_STRING."
#endif
}
// card.h
#ifndef CARD_H_INCLUDED
#define CARD_H_INCLUDED

#include <string>
#include <vector>

#define CARD_MARK_STRING_EMOJI      0   // 絵文字
#define CARD_MARK_STRING_KANA       1   // カタカナ
#define CARD_MARK_STRING_ALPHABET   2   // アルファベット
#define CARD_MARK_STRING            CARD_MARK_STRING_EMOJI  // カードマークの文字列表現の方法

// カードの数字の型
using CardNumber = signed char;

// カードのマークの型
enum class CardMark : signed char {
    spade,
    club,
    diamond,
    heart,
};

// カード型
struct Card {
    CardNumber number;  // 数字
    CardMark mark;      // マーク
};

// カードの集まりを表す型
using cards_type = std::vector<Card>;

constexpr auto card_mark_num = 4;                                    // マークの総数
constexpr auto each_mark_card_num = 13;                              // 各マークのカードの枚数
constexpr auto trump_card_num = each_mark_card_num * card_mark_num;  // トランプに含まれるカードの枚数


// トランプを初期化する
// cards: カードの集まり
void init_trump(cards_type& cards);

// カードをシャッフルする
// cards: カードの集まり
void shuffle_cards(cards_type& cards);

// カードを整列する
// cards: カードの集まり
void sort_cards(cards_type& cards);

// カードの整列に使う比較関数
// a: 対象のカード
// b: 対象のカード
// 戻り値: a と b が適切な順番で並んでいたら true、並んでいなければ false
bool cards_sort_compare(const Card& a, const Card& b);

// カードの数字の文字列表現を返す
// たとえば、3 は "3"、1 は "A"、13 は "K" と表現される。
//
// number: カードの数字
// 戻り値: カードの数字を文字列で表現したもの
std::string get_card_number_string(CardNumber number);

// カードのマークの文字列表現を返す
// 具体的な表現方法は、CARD_MARK_STRING マクロの定義によって異なる。
// 
// card_mark: カードのマーク
// 戻り値: カードのマークを文字列で表現したもの
std::string get_mark_string(CardMark card_mark);

#endif

実行結果は次のようになります。

あなたに配られたカードです。
0: ♦ A
1: ♦ 3
2: ♦ 8
3: ♦ 9
4: ♠ K

(以下略)

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

ASCII で正常に読めるように作られたテキストファイルを、UTF-8 (BOM付き) で保存しなおしたら、ファイルの大きさは増えますか?

解答・解説

問題2 (応用★★)

UTF-8 (BOM付き) のテキストファイルを用意して、そのファイルの内容を読み取ると、どうなるか確認してみてください。また、BOM を取り除いた部分だけを読み取るにはどうすれば良いでしょうか?

解答・解説

問題3 (調査★★)

UTF-8 で、トランプのマークを1つ表現するには、何バイト必要でしょうか?

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー