ヘッダファイル | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、ヘッダファイルを取り上げます。ヘッダファイルはこれまでのページでも、標準ライブラリで用意されているものを使うというかたちでは登場していますが、ここでは自分でヘッダファイルを作って活用する方法をみていきます。前のページで、複数のソースファイルを使ったプログラムを作れるようになりましたが、ヘッダファイルを導入することで、より良いかたちに改善できます。

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



ヘッダファイル

前のページで、ポーカープログラムを複数のソースファイルに分割しました。練習問題での対応も含めると、現在3つのソースファイルに分かれています。

しかし現状のプログラムはとにかくコードの重複が酷いので、これをどうにか解決しなければなりません。それがこのページの課題です。

今起きているコードの重複の原因は、ほかのソースファイルで定義されている内部結合の名前にアクセスできないため、仕方なく、同じことを何度も書いているからです。この問題を解決するために重要な役割を果たすのが、ヘッダファイル (header file) です。単にヘッダ (header) とも呼ばれます。

ヘッダファイルは、ソースファイルと同じように C++ のソースコードを記述するファイルです。文法ルールに違いがあるわけではなく、最終的にはどこかのソースファイルに取り込まれるという、役割上の違いがあるだけです。

ヘッダファイルを取り込むには #include を用います。これまで何度も登場した #include <iostream> などという記述がそれです。iostream はヘッダの名前です。iostream のように、標準で用意されているヘッダのことを、標準ヘッダ (standard header) と呼びます。

ヘッダファイルを自分で用意すれば、#include の記述を使ってソースファイルに取り込めます。複数のソースファイルがそれぞれ #include で取り込むことで、ソースコード上で起きていた重複をなくせます。また、関数の定義と宣言のように、名前や型の指定などが完璧に一致しなければならないとき、ヘッダファイルに宣言を置くことで、利用者が宣言を記述しなくて済むようになり、間違いを起こす可能性を減らす効果があります。

決まりはありませんが、ヘッダファイルには、.h や .hpp といった拡張子を付けることが多いです。どちらもよく使われているので、どちらを選んでもいいです。標準ヘッダは特殊で、拡張子がない名前で統一されています。

ヘッダファイルはC言語にもある仕組みですが、C言語では .h を使う習慣があります。C++ のヘッダファイルであることを明確にしたければ .hpp を使います。

Visual Studio のプロジェクトにヘッダファイルを追加する方法については「Visual Studio編>ヘッダファイルを追加する」を参照してください。

#include

ヘッダファイルを取り込む操作をインクルード (include) と呼びます。インクルードは、指定したヘッダファイルの中身のコードによって、#include を記述した行を置き換えます。

#include <iostream>  // この行は、iostream の中身に置き換わる

#include がヘッダファイルの中身で置き換えられるタイミングは、ビルドの過程の最初のほうで、プリプロセス(前処理) (preprocess) と呼ばれます。また、プリプロセスを行うソフトウェアをプリプロセッサ (preprocessor) と呼びます。

プリプロセスは、コンパイルが始まるよりも前に行われています。そのため、コンパイルは #include による置き換えが行われたあとのソースファイルに対して行われています。置き換え後のソースファイルは、翻訳単位 (translation unit) と呼ばれます。

Visual Studio には、プリプロセスを終えたあとのコードがどうなっているかを確認する方法があります。「Visual Studio編>プリプロセス後のコードを確認する」のページを参照してください。

コンパイルは翻訳単位に対して行われているのであって、ヘッダファイル自体をコンパイルしていないことは理解しておきましょう。どのソースファイルからもインクルードされていないヘッダファイルは、ビルドの対象に含まれていません。

また、「分割コンパイル」のページでも触れたとおり、通常、変更のないソースファイルはビルドの対象に含まれませんが、これはインクルードしているヘッダファイルの変更についても考慮されます。そのため、多くのソースファイルからインクルードされているヘッダファイルを変更すると、一斉に多くのソースファイルがコンパイルされることになります。大規模なプログラムでは、開発効率の面で、インクルードの数を減らすことも考えなければならない可能性があります。


#include には2つの記法があります。

#include <ヘッダ名>
#include "ヘッダ名"

いずれの記法も、処理系定義の方法でヘッダファイルを探すということになっていますが1一般的に、<> で囲むほうは標準ヘッダに使い、"" で囲むほうは自分たちで用意したヘッダファイルに対して使うという使い分けをします。

<>"" の内側をどのように記述するかも処理系定義です。基本的にはヘッダファイルの名前を記述するだけですが、ヘッダファイルが置かれている場所によっては、絶対パスや相対パスによる表記が必要になるかもしれません。

絶対パスによる指定は、そのプログラムを作成しているプログラマーの環境でのパスに過ぎないため、ほかの人の環境でコンパイルできないソースファイルになる可能性があります。

相対パスの場合、普通、#include を記述したソースファイル自身の場所が基準になります。また、大抵のコンパイラでは、ヘッダファイルをどこから探し出すかをあらわすパス情報(インクルードパス (include path))を指示する方法があります。

Visual Studio のインクルードパスに関する解説が「Visual Studio編>インクルードパス」のページにあります。

なお、#include の行は、コンパイラではなくプリプロセッサが解釈する部分であるため、C++ のほかの箇所とは少し文法ルールが異なる点があって、行末以外で改行することは許されません。また、<>"" の内側に余計な空白文字を入れるといったことも避けたほうがいいです。

ヘッダファイルの書き方

ヘッダファイルには基本的に何でも書けますが、一般的にヘッダファイルに記述するもの(または、するべきもの)と、しないほうがいいものがあります。

ヘッダファイルに記述するもの

ヘッダファイルに記述するものとして、以下があります。

ほかのソースファイルから呼び出す関数の宣言はヘッダファイルに記述し、定義をソースファイルに記述します。宣言には、externキーワードを付加できます(「分割コンパイル」のページを参照)。

反対に、あるソースファイルの中でしか使わない局所的な関数の場合は、ヘッダファイルに宣言を置かないようにします。たとえば、poker_hand.cpp の is_straight関数の宣言をヘッダファイルに置く必要はないでしょう(ストレートだけを判定できたところでおそらく価値がありません)。

ヘッダファイルに宣言を置かない関数は、宣言や定義に static キーワードを付加すると良いです。こうすると、その関数は内部結合になります。

static bool is_straight(const cards_type& cards);

static bool is_straight(const cards_type& cards)
{
    // 実装
}

【上級】内部結合にする方法として、無名名前空間に入れる方法があります(「スコープと名前空間」のページを参照)。

グローバル変数 (global variable) は、今のところ深入りしませんが、簡単にいえば、関数の外側で定義する変数のことです。ソースファイルに定義を1つ置き、ヘッダファイルに宣言を置くことで、複数のソースファイルからアクセスできる変数が実現できます。ただしこの方法は、プログラムの規模が大きくなるにつれて、把握が困難になっていくため、原則として使用を控えるべきものです。


同じものを表している定義は原則として1つでなければならないというルールがあり、ODR(単一定義規則) (One Definition Rule) と呼ばれています。1つの翻訳単位の中には、変数、関数、クラス(構造体)、列挙型の定義は1つでなければなりません2

【上級】そのほか、テンプレートの定義も1つでなければなりません。

翻訳単位が異なる場合、内部結合であればほかの翻訳単位から見えていないので、同じ定義があっても問題ありませんが、それらは別の存在ということになります。

翻訳単位が異なる場合で、外部結合の場合には、同じ定義が複数あることになり、素直にルールに当てはめれば、ODR に違反していることになります。しかし、構造体型や列挙型の定義のように、複数のソースファイルで共有したい外部結合のものたちもあるため、ODR の例外規則が適用されます3。正確に説明すると難しいので簡単にいうと、いくつかの種類にかぎっては(クラス、構造体、列挙型など)、定義のソースコードが一字一句の違いもなく同じであれば重複を許すということです(半角スペースが1つ増えただけでも、同じとはいえなくなります)定義のソースコードが一字一句の違いもないことを確実にするためには、ヘッダファイルに定義を書いて、インクルードで取り込むようにします。前のページでやった、自力でもう1回同じことを書くというやり方は避けるべきです。

変数や関数には例外規則が適用されないので、ヘッダファイルに定義を書くべきでないということになります。constexpr変数は内部結合なので問題ありません。

【上級】例外規則が認められるものにはほかに、外部結合のインライン関数、クラステンプレート、外部結合の関数テンプレート、クラステンプレートの静的データメンバ、クラステンプレートのメンバ関数、具体的な型を完全に指定していないテンプレートの特殊化があります4

ヘッダファイルに記述すべきもの

ヘッダファイルに記述すべきものとして、以下があります。

インクルードガード (include guard) とは、このヘッダファイルを2回以上繰り返しインクルードしたことで、1つの翻訳単位内に同じ定義が繰り返し現れてしまい、ODR に違反することを防ぐ備えのことです。

インクルードガードは次のように、ヘッダファイルのほかのコードを取り囲むかたちで記述します。

#ifndef XYZ_H_INCLUDED
#define XYZ_H_INCLUDED

// ほかのコード

#endif

詳細な解説はここではしませんが、#ifndef#define のうしろに同じ名前を記述し、ファイルの末尾に #endif を記述します。名前の決め方にルールはありませんが、ヘッダファイルごとに異なるものにしなければなりません。そのため、ヘッダファイル自身の名前をもとにして作ることが多いです。ここでは、xyz.h というヘッダファイルに対して、XYZ_H_INCLUDED という名前にしました。

_INCLUDED の部分は、「このヘッダファイルはすでに1度インクルードしている」ということを表現したものです。また、ほかのディレクトリであれば、同じ名前のヘッダファイルを作ることは可能であるため、ディレクトリ名も付加したり、作成日時を使ったりする方法もあります。

ヘッダファイルの先頭行に、#pragma once とだけ書く方法もよく使われています。こちらの方が圧倒的に手軽で安全ですが、C++ の標準仕様に含まれている方法ではないので、処理系によっては機能しません(とはいえ、現在の主要な処理系は対応しています)。

ヘッダファイル自身が使うものに関する宣言やインクルードは、自身で記述するようにします。たとえば、ヘッダファイルに std::string f(); という関数の宣言があるなら、std::string のために必要な #include <string> を記述します。こうしないと、ヘッダファイルをインクルードする側で、std::string の定義が見つからないという理由でコンパイルエラーになるかもしれません。ヘッダファイル自身が何を必要としているかは、ヘッダファイルを記述したプログラマーが把握していることなので、自分で使うものは自分で書くべきです。

ただし、クラスや構造体の参照型の型名を使っているだけの場合や、列挙型の名前を使っているが列挙子は必要ない場合には、それらの定義があるヘッダファイルをインクルードせず、宣言を記述したほうがコンパイルの効率が良くなります。構造体の宣言と、列挙型の宣言の方法はあとで取り上げます。

また、このルールを徹底すると、ヘッダファイルがほかのヘッダファイルをインクルードする構造になりますから、インクルードガードが重要になります。main.cpp が a.h と b.h をインクルードしており、a.h が b.h をインクルードしていたら、b.h の内容は main.cpp に2回取り込まれることになります。インクルードガードがないと、ODR に違反する可能性があります。

ヘッダファイルに記述しないもの

ヘッダファイルに記述しないほうがいいものとして、以下があります。

関数の定義は原則としてソースファイルの方に書き、ヘッダファイルには宣言だけを書きます。ただし、インライン関数 (inline function) は例外で、定義をヘッダファイルに書きます。インライン関数については、今のところ深入りしませんが、関数本体のコードを呼び出し元のコードに展開することによって、関数呼出しの実行時コストを消し去り、高速化を図る仕組みです。

インライン関数については、「クラス」のページで取り上げます。

グローバル変数の定義はソースファイルの方に書き、ヘッダファイルには宣言だけを書きます。定義をヘッダファイルに記述すると、インクルードした側に定義が取り込まれますから、複数のソースファイルからインクルードされると定義の重複が起こり、ODR に違反します。

マクロ (macro) についてはまだ説明していませんが、プリプロセスの段階で、ソースコード上の文字の並びを、ほかの文字の並びに置き換えるという強力な機能です。ヘッダファイルの中で定義してしまうと、その定義があることに気付かず、インクルードした側に影響を及ぼすため危険であるといえます。

マクロについては、「プリプロセス」のページで取り上げます。

構造体、クラスの宣言

構造体やクラスの宣言さえ見えていれば、定義は不要なケースがあります。このルールを利用すると、インクルードするヘッダファイルの個数を減らせる場合があります。インクルードを減らすことはビルド時間の削減につながり、開発効率が上がります。

構造体やクラスの宣言は、次のように記述します。

struct 構造体型の名前;
class クラス型の名前;

次のコードでは、定義が見えなくても、宣言が見えていれば問題ありません。

struct Size;  // 宣言

void print_size(const Size& size);  // OK. 参照型として名前が必要なだけなら、定義は不要

次のコードでは、定義が必要です。

struct Size;  // 宣言

Size size {};     // エラー。Size型の大きさが分からない
Size get_size();  // エラー。Size型の大きさが分からない

void print_size(const Size& size)
{
    std::cout << size.width << ", " << size.height << "\n";  // エラー。メンバがみえない
}

メンバを使おうとしている場合は、宣言だけではメンバの存在が見えないので、定義が必要なのは明らかでしょう。

構造体やクラスの型名を使おうとしている場合、それが参照型なら宣言だけで済みますが、そうでなければ定義が必要です。メンバが登場しないので、型名さえわかれば良さそうですが、もう1つ、型の大きさ(バイト数)が必要かどうかがポイントになっています。引数や戻り値で構造体を受け渡すときや、構造体型の変数を定義するときなどには、その型の大きさが分かっていないといけません。そして、大きさを知るには、どんなメンバがあるかが見えなくてはなりません。参照型の場合は、参照先の実際の大きさとは無関係に一定の大きさであるため、定義が見えなくても問題ありません。

【上級】ポインタ型の場合も宣言だけで済みます。

列挙型の宣言

列挙型」のページでも取り上げましたが、列挙型にも定義と宣言の区別があります。構造体やクラスの場合と同様、宣言だけで済ませることによって、不要なインクルードを減らせます。

列挙型の宣言は次のように記述します。

enum class 列挙名;
enum struct 列挙名;

enum class 列挙名 : 基底型;
enum struct 列挙名 : 基底型;
enum 列挙名 : 基底型;

unscoped enum の場合は、列挙子の一覧がないと型の大きさが決定できないため、基底型の指定が必要です。また、基底型の指定の有無やその型は、定義のほうと一致しなければなりません。

列挙子を使わない場面では、定義が見えなくても、宣言が見えていれば問題ありません。

enum class CardMark;  // 宣言
std::string get_mark_string(CardMark card_mark);  // OK

enum CardMark : int;  // 宣言(基底型の指定が必要)
std::string get_mark_string(CardMark card_mark);  // OK

列挙子を使う場合は、定義が必要です。

enum class CardMark;  // 宣言

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 "";
    }
}

ポーカープログラムの例

それでは、ポーカーのプログラムをヘッダファイルを使って書き換えてみます。main.cpp、poker_hand.cpp、card.cpp に加えて、poker_hand.h と card.h を作ります。そして、ほかのソースファイルから使いたい関数の宣言や、型の定義などをヘッダファイルに記述するようにします。

// main.cpp
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#include "card.h"
#include "poker_hand.h"

// 手札を配る
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();
    }

    std::sort(std::begin(hand_cards), std::end(hand_cards), cards_sort_compare);
}

int main()
{
    cards_type deck(trump_card_num);
    init_trump(deck);
    shuffle_cards(deck);
    
    // 手札を配る
    cards_type player_cards {};
    hand_out_cards(deck, player_cards);

    // 手札の内容を表示
    for (auto& card : player_cards) {
        std::cout << get_mark_string(card.mark) << " : " << get_card_number_string(card.number) << "\n";
    }

    // 役判定
    auto judge = judge_poker_hand(player_cards);
    std::cout << get_poker_hand_string(judge) << "\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);
}

// カードの整列に使う比較関数
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);

// カードの整列に使う比較関数
// 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

ヘッダファイルをインクルードするということは、そのヘッダファイルに依存しているということです。依存関係には注意を払ってください。このサンプルの場合、

となっています。card.cpp/.h はトランプゲーム全般で使えるようにするつもりで作っているので、これらのファイルから、ポーカー用の実装をしている main.cpp や poker_hand.cpp/.h に依存するのは間違っています。反対に、ポーカーゲームの実装である main.cpp や poker_hand.cpp/.h が card.h に依存することは問題ないといえます。

まとめ


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


参考リンク


練習問題

問題の難易度について。

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

問題1 (確認★)

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

解答・解説

問題2 (確認★)

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

解答・解説

問題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; };

解答・解説

問題4 (応用★★★)

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

解答・解説


解答・解説ページの先頭



更新履歴




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