第5章 飛んでいきな

 この章ではC/C++で悪名高い goto 文について話します。「goto 文は絶対悪だ」という人もいますが、気をつけて使えば有用なこともあります。


 それでは、今回の要点です。


 では、いってみましょう。


 ファイル2つのデータを使ってもう1つのファイルにデータを書き込むとします。すると、同時にファイルを3つ開くことになります。

 ファイルを使うときは必ずファイルが開けない可能性を考える必要があります。そして、使い終わったら必ずファイルを閉じることも必要です。

 さて、これらを踏まえてファイルを3つ開いてみましょう。

bool Func()
{
    FILE *pf1, *pf2, *pf3;
    bool bRet = false;

    pf1 = fopen(szFile1, "r");
    pf2 = fopen(szFile2, "r");
    pf3 = fopen(szFile3, "w");

    if(!(pf1 == NULL || pf2 == NULL || pf3 == NULL))
    {
        Func2(pf1, pf2, pf3);
        bRet = true;
    }

    if(pf1 != NULL)
        fclose(pf1);
    if(pf2 != NULL)
        fclose(pf2);
    if(pf3 != NULL)
        fclose(pf3);

    return bRet;
}

 一旦ファイルを全て開いています。しかし、ファイル1が開けなかったときはすぐに関数を終わらせたのでいいですし、いろいろ無駄があることが分かります。ファイル1、ファイル2を開くのに失敗してもファイル3を作ろうとしてしまうというのも問題ですね。



 ということで、ファイルを開く毎にエラー判定してみましょう。

bool Func()
{
    FILE *pf1, *pf2, *pf3;
    bool bRet = false;

    pf1 = fopen(szFile1, "r");
    if(pf1 == NULL)
        return false;

    pf2 = fopen(szFile2, "r");
    if(pf2 == NULL)
    {
        fclose(pf1);
        return false;
    }

    pf3 = fopen(szFile3, "w");
    if(pf3 != NULL)
    {
        Func2(pf1, pf2, pf3);
        fclose(pf3);
        bRet = true;
    }

    fclose(pf1);
    fclose(pf2);

    return bRet;
}

 fclose(pf1); が2つもありますし、プログラムも非常に読みにくく分かりにくいです。



bool Func()
{
    FILE *pf1, *pf2, *pf3;
    bool bRet = false;

    pf1 = fopen(szFile1, "r");
    if(pf1 == NULL)
        <return bRet; の行へ飛ぶ>

    pf2 = fopen(szFile2, "r");
    if(pf2 == NULL)
        <fclose(pf1); の行へ飛ぶ>

    pf3 = fopen(szFile3, "w");
    if(pf3 == NULL)
        <fclose(pf2); の行へ飛ぶ>

    Func2(pf1, pf2, pf3);
    bRet = true;

    fclose(pf3);
    fclose(pf2);
    fclose(pf1);

    return bRet;
}

 もし、このときこのようにできたらすっきりすると思います。



 C/C++言語には、このようなときに便利な命令があります。それが goto です。

bool Func()
{
    FILE *pf1, *pf2, *pf3;
    bool bRet = false;

    pf1 = fopen(szFile1, "r");
    if(pf1 == NULL)
        goto ERROR_1;

    pf2 = fopen(szFile2, "r");
    if(pf2 == NULL)
        goto ERROR_2;

    pf3 = fopen(szFile3, "w");
    if(pf3 == NULL)
        goto ERROR_3;

    Func2(pf1, pf2, pf3);
    bRet = true;

    fclose(pf3);
ERROR_3:
    fclose(pf2);
ERROR_2:
    fclose(pf1);
ERROR_1:

    return bRet;
}

 ERROR_1: と、<名前>: のようになっている部分があります。これがラベルです。ラベル自体は何の動作も行いません。

 goto はこのラベルの位置へと飛んでいく命令です。

 ラベルのスコープ(第1部第68章参照)はその関数の中全体です。goto 文よりも前にラベルがあっても構いません。

 ラベルの後には必ず何か文が来なければいけません。ブロックの終わりに飛ぶときは、ラベルの後に空文を入れておく必要があります。

 また、変数の宣言をまたいで飛ぶこともできません。またいでしまうときは、変数の宣言を goto 文の前にもっていく必要があります。



 このように、goto 文はエラー処理の時に活躍するのです。


 この goto 文は他にも多重ループを抜けるときにも利用できます。break 文では1つのループしか抜けることができません。そこで、goto 文を利用すれば多重ループも抜けることができます。

void Func2(FILE* pf1, FILE* pf2, FILE* pf3)
{
    int i, nNumbers;
    int num1, num2;

    nNumbers = 0;
    while(1)
    {
        for(i = 0; i < 8; i++)
        {
            // ショートサーキット(第2章参照)
            if(fscanf(pf1, "%d", &num1) == EOF ||
               fscanf(pf2, "%d", &num2) == EOF)
            {
                fputc('\n', pf3);
                goto LAST;
            }

            fprintf(pf3, "%d ", num1 + num2);
            nNumbers++;
        }
        fputc('\n', pf3);
    }

LAST:
    fprintf(pf3, "\n%d 回の計算を行いました。", nNumbers);
}

 2つのファイルに書いてある数をそれぞれ足し、その合計をもう1つのファイルに書いていくことにしました。

 8つの数を書き終えると改行するようにしています。fputc は1文字だけファイルに書き込む関数です。

 fscanf 関数は、ファイルの終端に達すると EOF を返します(EOF はマクロであり、置き換えられるテキストは (-1) です)。どちらかのファイルが終わるとループを抜けることにします。

 しかし、ここに break 文を書いても抜けるのは for ループだけです。while ループまでは抜けられず、for ループの後に while を抜けるかどうかを判定するコードが必要になります。しかし、goto 文を使えば余計な処理を増やさずにループを抜けることができるのです。


 さて、goto 文でいろいろな所に飛べるからといって、好き勝手することは厳禁です。goto が悪名高いのは、この好き勝手ができるということにあります。

 例えば、for 文の、while 文の代わりに if, goto 文を使うことはできます。しかし、そういうときは for 文、while 文を使った方が意味がよく分かります。わざわざ goto 文を使って分かりにくくすることはしてはいけません。

 そして、1重ループをぬけるのに goto 文を使うこともできます。ループの先頭に戻るというのも goto 文で可能です。しかし、これらには break 文、continue 文というものが用意されているので、これらを使うようにしましょう。

 また、ブロックの外から中へ goto してはいけません。ブロック内というのは、普通はある条件を満たしている時だけ入れるようにしています。goto 文で入るときはこの条件判定を無視してしまうことになり、さまざまなバグの可能性を秘めています。ブロック内で使われている変数の初期化についての問題も秘めています。

 「何とか気をつければどうでもいいじゃないか」という心構えは、「いかに楽をしてバグを少なくするか」を考えている自分にはとても真似できません。一杯努力をして、その努力の分だけ危なくなるなんて、まっぴら御免です。

 goto 文を好き勝手使ったプログラムのことをスパゲティプログラムと呼びます。「あっちこっちに飛んで、プログラムの流れがまるでからまったスパゲティのようだ」つまり「どこがどうなってるのか、よーわからん」プログラムのことです。「goto 文を全面禁止せよ」と主張する人は、スパゲティプログラムを組みたがる人が出てこないようにということなのでしょう。

 というように、goto 文は気をつけて使わないとバグの温床になってしまうような命令です。スパゲティプログラムは厳禁です! しかし、だからといって goto 文をかたくなに使わないというのも頑固というものです。危険な使い方さえしなければ、goto は非常に便利な命令です。

 薬と同じですね。大量に服用すれば危険ですが、量を守って気をつけて服用すれば健康を守ってくれます。goto 文の処方箋は出しました。処方箋を守らないと、あなたのプログラムは死んでしまうかもしれませんよ...。プログラムを生かすも殺すもあなた次第なのです。


 さて、今回の要点です。


 最後にもう1度。スパゲティプログラムは厳禁です! それでは。


第4章 参照先は何もなし? | 第6章 コンマ演算子

Last update was done on 2000.9.7

この講座の著作権はロベールが保有しています