ヘッダファイル 解答ページ | Programming Place Plus 新C++編

トップページ新C++編ヘッダファイル

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (確認★)

ヘッダファイルに関数の定義を記述すると、どのような問題が起こり得ますか? 実際にプログラムを書いて、その問題が起こることを確認してください。


同じものを表している定義は原則として1つでなければならないという、ODR のルールがあり、関数の定義にもこのルールが適用されます。通常の関数は外部結合であり、例外的なルールも特にないので、いくつかの翻訳単位に繰り返し定義が現れると、ODR に違反してしまいます(本編解説)。

ヘッダファイルに関数の定義を記述した場合、その関数を呼び出そうとする側は、そのヘッダファイルを #include を使って取り込む必要があります(本編解説)。#include は、指定したヘッダファイルに書かれているコードをそっくりそのまま持ってくるだけの機能ですから、複数のソースファイルから #include されると、それぞれのソースファイルに同じコードが取り込まれることになります。したがって、それぞれのソースファイルに、同じ関数の定義がばらまかれることになり、ODR への違反となります。

たとえば次のようにプログラムを作ると、リンクエラーが起こることが確認できます。

// main.cpp
#include "hello.h"

int main()
{
    print_hello();
}
// sub.cpp
#include "hello.h"

void sub()
{
    print_hello();
}
// hello.h
#ifndef HELLO_H_INCLUDED
#define HELLO_H_INCLUDED

#include <iostream>

void print_hello()
{
    std::cout << "Hello.\n";
}

#endif

#include "hello.h" と書いた行が、hello.h のコードで置き換えられるので、main.cpp と sub.cpp のプリプロセス後のコードは次のようになります。

// main.cpp
// hello.h
#ifndef HELLO_H_INCLUDED
#define HELLO_H_INCLUDED

#include <iostream>

void print_hello()
{
    std::cout << "Hello.\n";
}

#endif

int main()
{
    print_hello();
}
// sub.cpp
// hello.h
#ifndef HELLO_H_INCLUDED
#define HELLO_H_INCLUDED

#include <iostream>

void print_hello()
{
    std::cout << "Hello.\n";
}

#endif

void sub()
{
    print_hello();
}

print_hello関数の定義が、main.cpp と sub.cpp のそれぞれにあり、定義の重複となります。

問題2 (確認★)

ヘッダファイルに constexpr変数の定義を記述しても、問題1のようなことは起こりません。なぜか説明してください。


constexpr変数は内部結合なので、ほかの翻訳単位からは見えないからです(本編解説)。#include によって、複数のソースファイルに取り込まれたとしても、それぞれは別の定義として存在できます。

問題3 (確認★)

次のそれぞれについて、ビルドを通すためには、対象の宣言や定義のうち何が必要か説明してください。

  1. void f(); と宣言されている関数f を呼び出す(f();
  2. auto f(); と宣言されている関数f を呼び出す(x = f();
  3. 構造体型 S の変数を定義する(S s {};
  4. 構造体型 S の参照型の仮引数をもつ関数を宣言する(void f(S& s);
  5. 構造体型 S の大きさを取得する(sizeof(S);
  6. scoped enum E を構造体型 S のデータメンバの型に使う(struct S { E e; };


1番は、関数f の宣言がみえていればコンパイルできます。ただし、どこかには関数f の定義が必要で、それが見つからない場合にはリンクエラーになります。

2番は、関数f の定義がみえている必要があります。戻り値型が auto の場合は、定義の内容をみて戻り値型が推論されるからです(「関数から値を返す」のページを参照)。

3番は、構造体型S の定義がみえている必要があります。変数を定義するということは、その大きさに応じたメモリを確保しなければならないということであり、そのためには構造体型S にどんなメンバが含まれているか分からなければならないからです。

4番は、構造体型S の宣言がみえていればコンパイルできます。参照型として構造体型の名前を使っているだけなので、構造体型S の大きさを知る必要もないですし、メンバ名も登場しないため、定義がみえている必要はありません。S という名前が何者であるかは分かっていなければならないので、宣言は必要です。

5番は、構造体型S の定義がみえている必要があります。3番と同じ理由で、sizeof演算子が結果を返すためには、構造体型S のメンバがみえていなければならないからです。

6番は、scoped enum E の宣言がみえていればコンパイルできます。列挙子は登場していないので、ポイントになるのは、3番や5番と同様、scoped enum E の大きさが分かるかどうかです。scoped enum の場合、基底型を明示しなければ、デフォルトで int型になるルールなので(「列挙型」のページを参照)、大きさは int型と同じであることが分かっているため、定義は不要です。E という名前が何者であるかは分かっていなければならないので、宣言は必要です。

問題4 (応用★★★)

ポーカープログラムに次の仕様を加えて、プログラムを完成させてください。


詳細は自由に決めていただいて構いませんが、たとえば次のような感じに実装できます。

// main.cpp
#include <iostream>
#include <sstream>
#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);


int main()
{
    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 << "あなたに配られたカードです。\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 << "入力が正しくありません。\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 << "入力が正しくありません。\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_mark_string(card.mark) << " " << get_card_number_string(card.number) << " を捨てました。\n";

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

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

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

        // ゲームの続行確認
        std::cout << "\n"
                  << "ゲームを続けますか?\n"
                  << "はい: Y  いいえ: N\n";
        std::string input_string {};
        std::getline(std::cin, input_string);
        if (input_string == "Y" || input_string == "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_mark_string(card.mark) << " " << get_card_number_string(card.number) << " が配られました。\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 << ": \n";
        }
        else {
            std::cout << i << ": " << get_mark_string(card.mark) << " " << get_card_number_string(card.number) << "\n";
        }
    }
}

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

    for (cards_type::size_type i = 0; i < cards.size(); ++i) {
        std::cout << i << ": " << get_mark_string(cards.at(i).mark) << " " << get_card_number_string(cards.at(i).number);

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

        std::cout << "\n";
    }
}
// poker_hand.cpp
#include "poker_hand.h"
#include <algorithm>

// ストレートが成立しているか判定する
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)
{
    if (cards.size() != hand_card_num) {
        return PokerHand::no_pair;
    }
    if (std::is_sorted(std::cbegin(cards), std::cend(cards), cards_sort_compare) == false) {
        return PokerHand::no_pair;
    }

    // フラッシュ系
    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 "ノーペア";
    case PokerHand::one_pair:               return "ワンペア";
    case PokerHand::two_pairs:              return "ツーペア";
    case PokerHand::three_cards:            return "スリーカード";
    case PokerHand::straight:               return "ストレート";
    case PokerHand::flush:                  return "フラッシュ";
    case PokerHand::full_house:             return "フルハウス";
    case PokerHand::four_cards:             return "フォーカード";
    case PokerHand::straight_flush:         return "ストレートフラッシュ";
    case PokerHand::royal_straight_flush:   return "ロイヤルストレートフラッシュ";
    default:                                return "";
    }
}
// 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 "A";
    case 2:     return "2";
    case 3:     return "3";
    case 4:     return "4";
    case 5:     return "5";
    case 6:     return "6";
    case 7:     return "7";
    case 8:     return "8";
    case 9:     return "9";
    case 10:    return "10";
    case 11:    return "J";
    case 12:    return "Q";
    case 13:    return "K";
    default:    return "";
    }
}

// カードのマークの文字列表現を返す
std::string get_mark_string(CardMark card_mark)
{
    switch (card_mark) {
    case CardMark::spade:
        return "spade";
    case CardMark::club:
        return "club";
    case CardMark::diamond:
        return "diamond";
    case CardMark::heart:
        return "heart";
    default:
        return "";
    }
}
// card.h
#ifndef CARD_H_INCLUDED
#define CARD_H_INCLUDED

#include <string>
#include <vector>

// カードの数字の型
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: カードのマーク
// 戻り値: カードのマークを文字列で表現したもの
std::string get_mark_string(CardMark card_mark);

#endif

実行すると、最初の手札が、左端に 0~4 の番号が割り当てられた状態で提示されます。その後、捨てたいカードを 0~4 のいずれかを入力することで設定します。1度に指定できるカードは1枚ずつで、同じ番号を選ぶと選択を解除できます。数字を入力せずに Enterキーを押すことで決定され、選択したカードが捨てられて、新しいカードが補充されます。ここで役が判定されて、結果が表示されます。最後に、ゲームを続けるかやめるかを問うメッセージが表示されます。

あなたに配られたカードです。
0: club A
1: diamond 2
2: diamond 3
3: diamond 6
4: diamond 10

左端の番号を入力して、カードを選んでください。
何も入力せず Enterキーを押すと決定します。
0: club A
1: diamond 2
2: diamond 3
3: diamond 6
4: diamond 10
0

左端の番号を入力して、カードを選んでください。
何も入力せず Enterキーを押すと決定します。
0: club A <-- 捨てる
1: diamond 2
2: diamond 3
3: diamond 6
4: diamond 10

club A を捨てました。

heart A が配られました。

0: heart A
1: diamond 2
2: diamond 3
3: diamond 6
4: diamond 10
残念でした。

ゲームを続けますか?
はい: Y  いいえ: N
n


参考リンク



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る