コマンドライン引数 解答ページ | Programming Place Plus 新C++編

トップページ新C++編コマンドライン引数

このページの概要 🔗

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



解答・解説 🔗

問題1 (基本★★) 🔗

コマンドライン引数で、0個以上の整数を入力させ、その合計値を標準出力へ出力するプログラムを作成してください。整数とみなせない入力は 0 として扱うことにします。


たとえば次のように書けます。

#include <iostream>
#include <sstream>

int main(int argc, char** argv)
{
    int total {0};

    for (int i { 1 }; i < argc; ++i) {
        std::istringstream iss {argv[i]};
        int value {};
        iss >> value;
        if (iss) {
            total += value;
        }
    }

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

コマンドライン引数:

1 2 x 10 -3 20

実行結果:

30

コマンドライン引数を受け取るには、main関数に2つの仮引数を追加します。それぞれの型は intchar** で、名前は自由にできます(かといって変える必要もないです)(本編解説)。

argc にはコマンドライン引数の個数 + 1 が入っています。+1 されるのは、argv[0] のところに、プログラムを実行するための名前が入ってくるためです。サンプルコードでは、argv[0] はいらないので、変数i を 1 から開始させることで読み飛ばしています。

argv はポインタへのポインタになっています。コマンドライン引数は文字列として渡されてくるうえ、その個数に応じた要素数をもった配列になっているためです。少々イメージが難しいですが、std::vector<std::string> の形をイメージしてもらえれば意味することは分かるかと思います。argv[1] とすれば、1個目のコマンドライン引数の文字列にアクセスできます。

今回、欲しいのは整数値ですが、コマンドライン引数は文字列なので、整数型に変換する必要があります。std::istringstream を使って変換を行えば、整数にできないものをエラーとして検知できます。問題文では「整数とみなせない入力は 0 として扱う」とありますから、そのとおり丁寧に書けば、

        if (iss) {
            total += value;
        }
        else {
            // 変換でエラーが発生している場合
            total += 0;
        }

となります。

問題2 (応用★★) 🔗

コマンドライン引数で指定された個数の疑似乱数を、標準出力に出力するプログラムを作成してください。

たとえば、gen_rand 30 とした場合は 30個の乱数を、改行しながら出力します。


疑似乱数については、「シャッフルと乱数」のページで取り上げています。基本的に以前説明したとおりの使い方をすればいいだけです。

次のようにして実現できます。

#include <cstdlib>
#include <iostream>
#include <random>
#include <sstream>

int main(int argc, char** argv)
{
    if (argc != 2) {
        std::cerr << "コマンドライン引数の個数が正しくありません。\n";
        std::quick_exit(EXIT_FAILURE);
    }

    std::istringstream iss {argv[1]};
    int gen_num {};
    iss >> gen_num;
    if (!iss) {
        std::cerr << "コマンドライン引数の指定が正しくありません。\n";
        std::quick_exit(EXIT_FAILURE);
    }

    // 疑似乱数を生成して出力する
    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());
    for (int i {0}; i < gen_num; ++i) {
        std::cout << rand_engine() << "\n";
    }
}

コマンドライン引数:

30

実行結果:

1368240285
2004822180
1892819821
5033805
4041808575
1765707343
2267154318
1713806123
2307774281
2045285666
2611571918
791122686
1266231140
678783747
3208756050
1289847607
779371287
2442255584
1395660849
3465021571
258443108
1300711825
3033244244
1145013994
2578038202
2922739847
3466515092
1594015309
3233005696
3758171747

コマンドライン引数の個数や、指定する内容が正しいかといったエラーチェックをきちんと行うようにしてください。間違った入力で、プログラムが異常な動作をしてはいけません(本編解説)。

問題3 (応用★★★) 🔗

問題2で作成したプログラムに、生成する乱数の下限値を指定する –min オプションと、上限値を指定する –max オプションを実装してください。

--min の直後、--max の直後に整数を置くようにします。たとえば、gen_rand 30 --min 0 --max 10000 と指定された場合、0~10000 の範囲の乱数を 30個生成します。2つのオプションが指定される順番は入れ替わっても構いませんし、片方だけ指定することもできるものとします。

そのほかの仕様は自由に決めてください。


たとえば次のように実現できます。

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <limits>
#include <random>
#include <sstream>

int main(int argc, char** argv)
{
    if (argc < 2) {
        std::cerr << "コマンドライン引数の個数が正しくありません。\n";
        std::quick_exit(EXIT_FAILURE);
    }

    // 生成する乱数の個数
    int gen_num {};
    {
        std::istringstream iss {argv[1]};
        iss >> gen_num;
        if (!iss) {
            std::cerr << "コマンドライン引数の指定が正しくありません。\n";
            std::quick_exit(EXIT_FAILURE);
        }
    }

    // 最小値・最大値の指定
    int min {std::numeric_limits<int>::min()};
    int max {std::numeric_limits<int>::max()};
    for (int i {2}; i < argc; ++i) {
        if (std::strcmp(argv[i], "--min") == 0) {
            std::istringstream iss {argv[i + 1]};
            int value {};
            iss >> value;
            if (iss) {
                min = value;
            }
            else {
                std::cerr << "--min に与えるパラメータが正しくありません。\n";
                std::quick_exit(EXIT_FAILURE);
            }
        }
        else if (std::strcmp(argv[i], "--max") == 0) {
            std::istringstream iss {argv[i + 1]};
            int value {};
            iss >> value;
            if (iss) {
                max = value;
            }
            else {
                std::cerr << "--max に与えるパラメータが正しくありません。\n";
                std::quick_exit(EXIT_FAILURE);
            }
        }
    }

    if (min > max) {
        std::cerr << "最小値の指定が、最大値の指定よりも大きくなっています。\n";
        std::quick_exit(EXIT_FAILURE);
    }

    // 疑似乱数を生成して出力する
    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());
    std::uniform_int_distribution<int> dist(min, max);
    for (int i {0}; i < gen_num; ++i) {
        std::cout << dist(rand_engine) << "\n";
    }
}

コマンドライン引数:

30 --min 10000 --max 50000

実行結果:

44215
45593
46510
32068
10366
25439
15425
43174
32533
12702
42549
16906
44489
18006
25141
25246
11699
28616
21953
48355
38264
35958
19617
10686
15234
12913
32150
34704
32361
14949

argv から “–min” や “–max” を探して、見つかれば、その直後のところ(argv[2] で見つけたなら argv[3])に入っている整数値を取り出して、minmax に保存しておきます。もし複数回、“–min” や “–max” が登場した場合は、最後に見つけたものを採用することにしています(そのためには、単に minmax を上書きしてしまえばいいです)。1つも指定がなかったときは、minmax は初期値のままになるので、int型の最小値・最大値が使われることになります。

生成される乱数を minmax のあいだに収めるには、std::uniform_int_distribution<int> を使います。この方法は、「シャッフルと乱数」のページで取り上げました。

問題2では、std::mt19937 をデフォルトの使い方で使っており、この場合、生成される値の型は std::uint_fast32_t という符号無し整数型でした(「シャッフルと乱数」のページを参照)。一方、問題3のコード例では std::uniform_int_distribution<int> を使っており、int型なので、負数も生成されるようになっています。仕様が異なるのが嫌ならば、問題2のほうでも std::uniform_int_distribution<int> を使うようにして、int型の範囲内の値になるようにすればいいでしょう。

問題4 (応用★★★) 🔗

バイナリエディタのプログラムに、16進数の A~F の表示を a~f に変更するオプションを実装してください。


小文字 (lower-case) ということで、-l あるいは --lower というオプションを追加することにします。コマンドライン引数の個数が一定でなくなるため、argc によるエラーチェックも変更しなければなりません。さらなるオプション追加のことも想定しつつ改造を施してみます。

#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>

constexpr auto app_name = "binary_editor";
constexpr auto version_str = "1.0.1";

// オプション
using option_t = unsigned int;
constexpr option_t option_none = 0;          // オプションなし
constexpr option_t option_lower_case = 0b1;  // 小文字で表示する

// コマンドライン解析の結果
enum class AnalysisCmdlineArgsResult {
    correct,        // 正しい指定
    correct_exit,   // 正しい指定だが、プログラムは終了させる
    error,          // エラー
};

// 大文字・小文字
enum class Letter {
    upper,
    lower,
};


// コマンドライン引数を std::vector<std::string> に格納して返す
static std::vector<std::string> argv_to_vector(int argc, char** argv);

// コマンドライン引数を解析する
static AnalysisCmdlineArgsResult analysis_cmdline_args(const std::vector<std::string>& cmdline_args, option_t& option);

// ヘルプメッセージを出力
static void print_help();

// バージョン情報を出力
static void print_version();

// バイト列を標準出力へ出力する
//
// address: 対象のバイト列を指し示すポインタ
// size: 対象のバイト列のバイト数
// letter: 16進数のアルファベットの表示方法
static void print_byte_string(const void* address, std::size_t size, Letter letter);


int main(int argc, char** argv)
{
    // コマンドライン引数を std::vector に詰め替える
    auto cmdline_args = argv_to_vector(argc, argv);

    // コマンドライン引数の解析
    option_t option {};
    switch (analysis_cmdline_args(cmdline_args, option)) {
    case AnalysisCmdlineArgsResult::correct:
        break;

    case AnalysisCmdlineArgsResult::correct_exit:
        std::quick_exit(EXIT_SUCCESS);
        break;

    case AnalysisCmdlineArgsResult::error:
        std::cerr << "コマンドライン引数の指定が正しくありません。\n";
        print_help();
        std::quick_exit(EXIT_FAILURE);
        break;
    }

    // 対象のファイルをオープン
    std::string path {cmdline_args.at(1)};
    std::ifstream ifs(path, std::ios_base::in | std::ios_base::binary);
    if (!ifs) {
        std::cerr << path << "が開けません。\n";
        std::quick_exit(EXIT_FAILURE);
    }

    // ファイルの中身をすべて読み込む
    std::istreambuf_iterator<char> it_ifs_begin(ifs);
    std::istreambuf_iterator<char> it_ifs_end {};
    std::vector<char> bytes(it_ifs_begin, it_ifs_end);

    // バイト列を出力
    print_byte_string(
        bytes.data(),
        bytes.size(),
        (option & option_lower_case) ? Letter::lower : Letter::upper
    );
}

// コマンドライン引数を std::vector<std::string> に格納して返す
static std::vector<std::string> argv_to_vector(int argc, char** argv)
{
    std::vector<std::string> vec {};
    for (int i {0}; i < argc; ++i) {
        vec.push_back(argv[i]);
    }
    return vec;
}

// コマンドライン引数を解析する
static AnalysisCmdlineArgsResult analysis_cmdline_args(const std::vector<std::string>& cmdline_args, option_t& option)
{
    option = option_none;

    switch (cmdline_args.size()) {
    case 2:
        if (cmdline_args.at(1) == "-h" || cmdline_args.at(1) == "--help") {
            print_help();
            return AnalysisCmdlineArgsResult::correct_exit;
        }
        else if (cmdline_args.at(1) == "-v" || cmdline_args.at(1) == "--version") {
            print_version();
            return AnalysisCmdlineArgsResult::correct_exit;
        }

        break;

    case 3:
        if (cmdline_args.at(2) == "-l" || cmdline_args.at(2) == "--lower") {
            option |= option_lower_case;
        }
        break;

    default:
        return AnalysisCmdlineArgsResult::error;
    }

    return AnalysisCmdlineArgsResult::correct;
}

// ヘルプメッセージを出力
static void print_help()
{
    std::cout << "使い方:\n"
              << "> " << app_name << " 対象のファイルパス\n"
              << "> " << app_name << " 対象のファイルパス オプション\n"
              << "> " << app_name << " -h(または --help)\n"
              << "> " << app_name << " -v(または --version)\n"
              << "\n"
              << "-h(--help)    使い方を表示します。\n"
              << "-v(--version) バージョン情報を表示します。\n"
              << "\n"
              << "オプション:\n"
              << "-l (--lower)    16進数のアルファベット部分を小文字で表示します。"
              << std::endl;
}

// バージョン情報を出力
static void print_version()
{
    std::cout << app_name << " version: " << version_str << std::endl;
}

// バイト列を標準出力へ出力する
static void print_byte_string(const void* address, std::size_t size, Letter letter)
{
    auto p = static_cast<const unsigned char*>(address);
    auto remain_byte = size;

    while (remain_byte > 0) {
        std::size_t byte_num {remain_byte >= 16 ? 16 : remain_byte};

        for (std::size_t i {0}; i < byte_num; ++i) {
            std::cout << std::setw(2) << std::setfill('0') << std::hex
                      << (letter == Letter::upper ? std::uppercase : std::nouppercase)
                      << static_cast<unsigned int>(*p) << " ";
            ++p;
        }
        std::cout << "\n";

        remain_byte -= byte_num;
    }
}

コマンドライン引数の解析が複雑になるので、analysis_cmdline_args関数として抜き出しました。指定されたオプションが分かる必要もあるので、引数で option_t型の変数の参照を渡すようにしています。また、analysis_cmdline_args関数は、解析結果に応じた戻り値を返し、プログラムを続行すべきか、終了すべきかを呼び出し元に伝えています。

オプションはビット単位で管理ができるようにしています。ビット単位にしたのは、今後さらにオプションの種類が増える可能性を考慮したものです。ただし、これが最善の方法ということではありません。

たとえば、パラメータ付きのオプションが追加されると、パラメータ部分を管理できない問題に気付くでしょう。

print_byte_string関数にも引数を追加しており、大文字・小文字のどちらでの表示が望まれているかを指定できるようになっています。やや細かい話のようですが、ここで bool is_lowercase のような仮引数にするのではなく、(面倒ですが)scoped enum を使うことで、コードが分かりやすく、安全になります。bool型の引数にすると、呼び出し側には truefalse とだけ書かれることになるので、コードを読むときに一見して何を意味した引数なのか分からず、いちいち関数の仕様を確認しなければならなくなります。

std::cout で出力するとき、16進数のアルファベット部分の表示は小文字がデフォルトなので、std::uppercase を呼ばないようにするのでいいですが、サンプルコードのように std::nouppercase を使うこともできます。ループの内側にあって、何度も繰り返し実行される場所なので、判定をループの外で1度だけ行うかたちにしたほうが効率は上がるかもしれません。


あとは、ヘルプメッセージにオプションの存在を追記してあります。


参考リンク 🔗



更新履歴 🔗




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