Code of Poem
Unknown programmer's programming note.

数当てゲームを見直す

前回作った数当てゲームを見直して、少し修正しようと思います。もう一度コードを全部載せておきます。

#include <iostream>
#include <cstdlib>
#include <limits>
using namespace std;

int main()
{
    char play_again;
    do {
        int a = 42; // ランダムな数字
        int count = 0;
        int last_number;
        while (count < 10) {
            int guess_number;
            cin >> guess_number;
            cin.ignore(numeric_limits<streamsize>::max(), '\n');
            if (cin.fail()) {
                cerr << "入力が間違ってます...終了します\n";
                exit(1);
            }
                        
            if (guess_number == a) {
                cout << "当たりです!\n";
                break;
            } else {
                cout << "はずれです。";
                if (guess_number > a) {
                    cout << "大きすぎます。\n";
                } else {
                    cout << "小さすぎます。\n";
                }
                
                if (count > 0) {
                    cout << "前回の数字:" << last_number << "\n";
                }
                last_number = guess_number;
            }
            ++count;
        }
        cout << "もう1回プレイしますか? [y/n] ";
        cin >> play_again;
        cin.ignore(numeric_limits<streamsize>::max(), '\n');
    } while (play_again == 'y');
}

main関数がやや長い気がします。このくらいのプログラムならさほど気にならないかもしれないのでモチベーションはやや低いのですが、少し分割して、プログラムの見通しを良くしたいところです。例えば、main関数は次のように書けたらいいかもしれません。

int main()
{
    do {
        ゲームをプレイする;
    } while (もう一度プレイする???);
}

ゲームプレイの部分を「ゲームをプレイする」という何かにまとめてしまい、もう一度プレイするか尋ねる部分を「もう一度プレイする???」という何かにまとめてしまいます。こうした、プログラムの一部分を切り離して、その切り離した部分に名前をつけて、あとでその名前を使って使い回せるようにする機能が用意されています。C++では関数と呼ばれます。現代のプログラミング言語の多くが多少の差異はあれど同じような機能を備えています。呼び名は関数が一番多いと思いますが他にも、プロシージャ、サブルーチン、サブプログラム、メソッドなどと呼ばれることもあります。

関数を作るにはある決まった書き方に従う必要があります。すでに関数は登場していて、「main」が関数です。なので想像はつくとは思います。最も基本的なものは次のような書き方をします。

戻り値の型 関数の名前(引数1の型 引数1の名前, 引数2の型 引数2の名前, まだあれば後ろに続く..)
{
    // 関数本体の処理
    ...
}

戻り値がなければ戻り値の型はvoidと書きます。引数は2つでも3つでも、たくさん取ることが出来ます。引数を取らないとき(0個のとき)は括弧の中を空にします。なので一番シンプルな関数は、

void 関数の名前()
{
    // 関数本体の処理
    ...
}

という形を取ります。

例として少し書いてみます。

// Hello, world!とコンソールに出力する関数
void hello()
{
    std::cout << "Hello, world!\n";
}

// 2つのdouble型の値を引数に取り、大きい方を返す関数
double max_number(double a, double b)
{
    if (a > b) return a;
    return b;
}
      
// 8個のint型の値を引数に取り、その合計を返す関数
int sum_of_eight_ints(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
    return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8;
}

これらの関数はあまり実用的ではないです。max_numberに相当する関数はstd::maxが用意されているのでそちらを使えば良いし、sum_of_eight_intsは8個固定の数を足し合わせることが必要になる状況は限られているでしょう。helloは嬉しいかもしれません。ここでは例としてあげただけなので深く突っ込まないことにしておきます。

関数を使うには名前の後ろに括弧をつけて関数に渡す値を記述します。名前(引数)の形です。今書いた関数を使ってみます。

#include <iostream>

// ここに上の関数を書く...

int main()
{
    hello();

    // 結果をそのままstd::coutに使う
    std::cout << max_number(10.001, 10.0) << '\n';

    // 結果を変数に代入することもできる
    int s = sum_of_eight_ints(1, 2, 3, 4, 5, 6, 7, 8);
    std::cout << s << '\n';
}
// 出力
// Hello, world!
// 10
// 36

たぶん期待した結果が得られたと思います。

関数を使うところで関数のシグネチャと一致していないとコンパイルエラーになります。例えばsum_of_eight_intsに8個ではなく7個しか引数を渡さなかったとします。

int s = sum_of_eight_ints(1, 2, 3, 4, 5, 6, 7);

すると以下のようなエラーになります(出力はGeanyで実行したものです)。

g++ -Wall -o "functions1" "functions1.cpp"(ディレクトリ: /home/userx/projects/geany/scratch)
functions1.cpp: In function ‘int main()’:
functions1.cpp:28:50: error: too few arguments to function ‘int sum_of_eight_ints(int, int, int, int, int, int, int, int)’
      int s = sum_of_eight_ints(1, 2, 3, 4, 5, 6, 7);
                                                  ^
functions1.cpp:16:5: note: declared here
  int sum_of_eight_ints(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
      ^~~~~~~~~~~~~~~~~
コンパイル失敗

「too few arguments」と言っているので、引数の数が足りないと知ること出来ます。このような分かりやすいエラーになることもあれば、時には非常に分かりづらいメッセージになることもあります。

このエラーでは引数のことを「arguments」といっているのですが、他に「parameters」と呼ばれることもあります。この2つは次のように使い分けられることが定着しているようです

parameters
関数を作るところ(関数の定義)で並べた名前
arguments
関数に実際に渡された値

GCCのメッセージなどを見るとだいたいこのように使い分けているようにみられます。

日本語だと次のように使い分けているのを目にすることが多いと思います。

仮引数
関数を作るところ(関数の定義)で並べた名前
実引数
関数に実際に渡された値

ややこしいことに「formal parameter」と「actual parameter」と使いわける場合もあるようです。日本語の仮引数、実引数という呼び名はこちらが元になっていると思われます。厳密な定義はプログラミング言語によっても違いが出てきそうです。

単に「引数」と言った場合、仮引数か実引数かは文脈で判断できることがほとんどだと思います。

関数の宣言と定義

関数を使うところでは、その場所からその関数の宣言が見えてなければいけません。例えば、main関数でそれよりも下に記述してある関数fooを呼び出したとします。

int main()
{
    foo(); // fooはmainよりも下に書いてある
}

void foo()
{
    // 何もしない
}

といったコードがあったとして、これをコンパイルするとエラーになります。

g++ -Wall -o "functions2" "functions2.cpp"(ディレクトリ: /home/userx/projects/geany/scratch)
functions2.cpp: In function ‘int main()’:
functions2.cpp:3:5: error: ‘foo’ was not declared in this scope
      foo();
      ^~~
functions2.cpp:3:5: note: suggested alternative: ‘bool’
      foo();
      ^~~
      bool
コンパイル失敗

「error: ‘foo’ was not declared in this scope (fooはこのスコープで宣言されていない)」というエラーになりました。「note: suggested alternative: ‘bool’」というのはfooがスペルミスではないかと推測して、boolにしたらどうかという親切な提案をしてくれています。

fooをmainの上に書けばこのエラーは解決します。

void foo()
{
    // 何もしない
}

int main()
{
    foo(); // これは大丈夫
}

しかし、必ず全ての関数について使うところより先に書かないといけないとなると、とても不便です。不便なだけでなくそうすることが不可能なこともあります。

void foo(int n)
{
    if (n == 0) return;
    else bar(--n);
}

void bar(int n)
{
    foo(n);
}

int main()
{
    bar(100);
}

これをコンパイルするとエラーになります。

g++ -Wall -o "functions3" "functions3.cpp"(ディレクトリ: /home/userx/projects/geany/scratch)
functions3.cpp: In function ‘void foo(int)’:
functions3.cpp:4:10: error: ‘bar’ was not declared in this scope
     else bar(--n);
          ^~~
functions3.cpp:4:10: note: suggested alternative: ‘char’
     else bar(--n);
          ^~~
          char
コンパイル失敗

「error: ‘bar’ was not declared in this scope (barはこのスコープで宣言されていない)」となりました。関数barは関数fooより後ろに書かれているため、fooからbarが見えないためです。fooとbarの位置を入れ替えてみます。

void bar(int n)
{
    foo(n);
}

void foo(int n)
{
    if (n == 0) return;
    else bar(--n);
}

int main()
{
    bar(100);
}

これもコンパイルしたらエラーになりました。

g++ -Wall -o "functions3" "functions3.cpp"(ディレクトリ: /home/userx/projects/geany/scratch)
functions3.cpp: In function ‘void bar(int)’:
functions3.cpp:3:5: error: ‘foo’ was not declared in this scope
     foo(n);
     ^~~
functions3.cpp:3:5: note: suggested alternative: ‘bool’
     foo(n);
     ^~~
     bool
コンパイル失敗

今度はbarでfooが宣言されていないというエラーになりました。これを解決するには、何とかして関数を記述する前に別の関数の名前(例えばbar)が確かに関数の名前であるということを知る必要があります。

barをfooよりも先に宣言することで解決出来ます。関数を宣言するには、関数の処理の内容の部分{...}を記述せずにおきます。

void bar(int n);

void foo(int n)
{
    if (n == 0) return;
    else bar(--n);
}

void bar(int n)
{
    foo(n);
}

int main()
{
    bar(100);
}

これはエラーになりません。void bar(int n);の部分が関数の宣言です。この部分が見えれば、それより後ろでは、barといのはたしかに関数の名前だと、コンパイラは知ることが出来ます。一方、{...}で関数の処理を記述するのは、それは関数の定義です。ややこしいのですが、普通に関数を記述した場合は関数の定義なのですが、同時に宣言でもあります。なので、別に独立して関数を宣言しなくても、関数を定義した後ろ部分より後のコードでは関数を使うことが出来ます。

こんな変なプログラム書かないから宣言はあまり重要じゃないんじゃない?という訳ではありません。この機能は極めて重要です。プログラムが複数のソースコードに分かれる場合(ごく普通にあることです)、もしこの関数の宣言がなかったら、別のソースコードにある関数を呼び出すことが出来ません。ソースコードをまるごとincludeしてしまうという手もあるにはありますが…。複数のソースコードにプログラムが分かれる場合取られる手法は、まず別のソースコードにある関数を宣言して(ヘッダファイルをインクルードして取り込むことも多いです)、その定義、つまり処理の部分はリンク時に解決するというものです。このやり方はうまくいきます。

関数を別のファイルに書く実験

最初の実験 - 失敗

サンプルを作ってみます。今度はfooをfoo.cppにbarをbar.cppにmainをmain.cppにと分けてコードを書くことにします。

foo.cpp

void foo(int n)
{
    if (n == 0) return;
    else bar(--n);
}

bar.cpp

void bar(int n)
{
    foo(n);
}

main.cpp

int main()
{
    foo(100);
}

コンパイル

ls -l
合計 12
-rw-r--r-- 1 userx userx 32 12月 10 16:35 bar.cpp
-rw-r--r-- 1 userx userx 63 12月 10 16:35 foo.cpp
-rw-r--r-- 1 userx userx 29 12月 10 16:35 main.cpp
gcc foo.cpp bar.cpp main.cpp 
foo.cpp: In function ‘void foo(int)’:
foo.cpp:4:10: error: ‘bar’ was not declared in this scope
     else bar(--n);
          ^~~
foo.cpp:4:10: note: suggested alternative: ‘char’
     else bar(--n);
          ^~~
          char
bar.cpp: In function ‘void bar(int)’:
bar.cpp:3:5: error: ‘foo’ was not declared in this scope
     foo(n);
     ^~~
bar.cpp:3:5: note: suggested alternative: ‘bool’
     foo(n);
     ^~~
     bool
main.cpp: In function ‘int main()’:
main.cpp:3:5: error: ‘foo’ was not declared in this scope
     foo(100);
     ^~~
main.cpp:3:5: note: suggested alternative: ‘bool’
     foo(100);
     ^~~
     bool

foo、barの宣言が見つからないとエラーが報告されました。

2番目の実験 - 成功

これを解決するには、それぞれのソースコードで使用している関数の宣言をする必要があります。

foo.cpp

void bar(int n);

void foo(int n)
{
    if (n == 0) return;
    else bar(--n);
}

bar.cpp

void foo(int n);

void bar(int n)
{
    foo(n);
}

main.cpp

void foo(int n);
      
int main()
{
    foo(100);
}

もう一度コンパイルしてみます。

gcc foo.cpp bar.cpp main.cpp 
ls -lt
合計 32
-rwxr-xr-x 1 userx userx 16584 12月 10 16:44 a.out
-rw-r--r-- 1 userx userx    47 12月 10 16:44 main.cpp
-rw-r--r-- 1 userx userx    81 12月 10 16:44 foo.cpp
-rw-r--r-- 1 userx userx    50 12月 10 16:44 bar.cpp
./a.out

今度はコンパイルエラーにならず、実行ファイルが生成されました。

3番目の実験 - 成功

しかし、このように利用する側が関数のシグネチャをいちいち覚えておいて、記述し直さないといけないのはいかにも大変そうです。このような場合、外部に公開する関数の宣言はヘッダファイルに書くことが多いです。

foo.cpp

#include "foobar.h"

void foo(int n)
{
    if (n == 0) return;
    else bar(--n);
}

bar.cpp

#include "foobar.h"

void bar(int n)
{
    foo(n);
}

main.cpp

#include "foobar.h"
      
int main()
{
    foo(100);
}
foobar.h
void foo(int n);
void bar(int n);

先と同じようにコンパイルしてみます。

ls -lt
合計 16
-rw-r--r-- 1 userx userx 34 12月 10 16:53 foobar.h
-rw-r--r-- 1 userx userx 53 12月 10 16:51 bar.cpp
-rw-r--r-- 1 userx userx 84 12月 10 16:51 foo.cpp
-rw-r--r-- 1 userx userx 50 12月 10 16:51 main.cpp
gcc foo.cpp bar.cpp main.cpp 
ls -lt
合計 36
-rwxr-xr-x 1 userx userx 16584 12月 10 16:55 a.out
-rw-r--r-- 1 userx userx    34 12月 10 16:53 foobar.h
-rw-r--r-- 1 userx userx    53 12月 10 16:51 bar.cpp
-rw-r--r-- 1 userx userx    84 12月 10 16:51 foo.cpp
-rw-r--r-- 1 userx userx    50 12月 10 16:51 main.cpp
./a.out

コンパイルは通って実行ファイルを得ることが出来ました。

4番目の実験 - リンクエラー

逆に、宣言は見つかったけど定義が見つからない場合はリンクエラーになります。上のソースコードを使って試してみます。

gcc -c foo.cpp bar.cpp main.cpp
ls -l
合計 28
-rw-r--r-- 1 userx userx   53 12月 10 16:51 bar.cpp
-rw-r--r-- 1 userx userx 1400 12月 10 16:59 bar.o
-rw-r--r-- 1 userx userx   84 12月 10 16:51 foo.cpp
-rw-r--r-- 1 userx userx 1416 12月 10 16:59 foo.o
-rw-r--r-- 1 userx userx   34 12月 10 16:53 foobar.h
-rw-r--r-- 1 userx userx   50 12月 10 16:51 main.cpp
-rw-r--r-- 1 userx userx 1400 12月 10 16:59 main.o
gcc foo.o main.o
/usr/bin/ld: foo.o: in function `foo(int)':
foo.cpp:(.text+0x1b): undefined reference to `bar(int)'
collect2: error: ld returned 1 exit status

gcc -c files..のように「-c」オプションを付けると、リンクを行わず、コンパイルのみを行います。その結果、.cppに対応するオブジェクトファイル、.oが生成されます。gcc foo.o main.oではリンクを行っています。あえてbar.oだけリンクせずおくと、bar(int)が未定義だというエラーを見ることが出来ました。

5番目の実験 - 成功

今度はbar.oも含めてリンクしてみます。

gcc foo.o bar.o main.o 
ls -lt
合計 48
-rwxr-xr-x 1 userx userx 16584 12月 10 17:06 a.out
-rw-r--r-- 1 userx userx  1400 12月 10 16:59 main.o
-rw-r--r-- 1 userx userx  1400 12月 10 16:59 bar.o
-rw-r--r-- 1 userx userx  1416 12月 10 16:59 foo.o
-rw-r--r-- 1 userx userx    34 12月 10 16:53 foobar.h
-rw-r--r-- 1 userx userx    53 12月 10 16:51 bar.cpp
-rw-r--r-- 1 userx userx    84 12月 10 16:51 foo.cpp
-rw-r--r-- 1 userx userx    50 12月 10 16:51 main.cpp
./a.out

無事、実行ファイルを作ることが出来ました。

このプログラムのソースコードのレイアウトを図にすると次のようになります。

Functions foobar files diagram

話が逸れまくりました。そろそろ戻りたいと思います。

ゲームのコードを関数を使って書き直す

何をしていたかと言うと、数当てゲームのコードのmain関数を書き直そうとしていたところでした。

最初の試みは次のようなものです。

#include <iostream>
#include <cstdlib>
#include <limits>
using namespace std;

void play_the_game()
{
    int a = 42; // 1から100までのランダムな数
    int count = 0;
    int last_number;
    while (count < 10) {
        int guess_number;
        cin >> guess_number;
        cin.ignore(numeric_limits<streamsize>::max(), '\n');
        if (cin.fail()) {
            cerr << "入力が間違ってます...終了します\n";
            exit(1);
        }
                    
        if (guess_number == a) {
            cout << "当たりです!\n";
            break;
        } else {
            cout << "はずれです。";
            if (guess_number > a) {
                cout << "大きすぎます。\n";
            } else {
                cout << "小さすぎます。\n";
            }
            
            if (count > 0) {
                cout << "前回の数字:" << last_number << "\n";
            }
            last_number = guess_number;
        }
        ++count;
    }
}

bool play_again()
{
    char play_again;
    cout << "もう1回プレイしますか? [y/n] ";
    cin >> play_again;
    cin.ignore(numeric_limits<streamsize>::max(), '\n');
    return play_again == 'y';
}

int main()
{
    do {
        play_the_game();
    } while (play_again());
}

main関数はかなり小さくなりました。ゲームプレイ本体の部分がまだ大きい気がします。もう少し分割したいところです。

次の試み

基本的な作りは変えないで、できるだけ関数に分割してみました。

#include <iostream>
#include <cstdlib>
#include <limits>
using namespace std;

int random_number(int a, int b)
{
    return 42; // aからbまでのランダムな数
}    

int player_guesses_number()
{
    int number;
    cin >> number;
    cin.ignore(numeric_limits<streamsize>::max(), '\n');
    if (cin.fail()) {
        cerr << "入力が間違ってます...終了します\n";
        exit(1);
    }
    return number;
}

bool check_result(int guess_number, int right_number)
{
    if (guess_number == right_number) {
        cout << "当たりです!\n";
        return true;
    }
    
    cout << "はずれです。";
    if (guess_number > right_number) {
        cout << "大きすぎます。\n";
    } else {
        cout << "小さすぎます。\n";
    }
    return false;
}

void print_last_number(int last_number, int tries_count)
{
    if (tries_count > 0) {
        cout << "前回の数字:" << last_number << "\n";
    }
}

void play_the_game()
{
    int a = random_number(1, 100);
    int count = 0;
    int last_number;
    while (count < 10) {
        int guess_number = player_guesses_number();
        if (check_result(guess_number, a)) {
            break;
        }
        print_last_number(last_number, count);
        last_number = guess_number;
        ++count;
    }
}

bool play_again()
{
    char play_again;
    cout << "もう1回プレイしますか? [y/n] ";
    cin >> play_again;
    cin.ignore(numeric_limits<streamsize>::max(), '\n');
    return play_again == 'y';
}

int main()
{
    do {
        play_the_game();
    } while (play_again());
}

このプログラムで定義した関数は以下の7つです。

最初のmain関数に全部詰め込んでいたバージョンよりプログラム全体の行数は増えています。最初のバージョンは45行で、最後のバージョンは77行でした。

これでよくなったのかどうかはなんとも言えないです。このくらい小さなプログラムだと効果がはっきりと出てきてないです。どの程度まで関数に分けるかは判断が難しいです。

for文

ついでにfor文のことを書いておきます。

play_the_game関数の中のループはwhile文を使って次のようになっていました。

int count = 0;
while (count < 10) {
    // なにかする
    ++count;
}

このコードをよくみてみると、

  1. 繰り返しをカウントする変数を定義して初期化する部分
  2. 繰り返しをカウントする変数が条件を満たすか判定する部分
  3. 繰り返しをカウントする変数を更新

という3つの部分に分かれています。このようなパターンは頻出するので、特別にfor文が用意されています。この繰り返しは、for文を使って次のように書くことが出来ます。

for (int count = 0; count < 10; ++count) {
    // なにかする
}

for文もよくつかうのでwhile文とセットで覚えておくほうがいいです。