こんにちは、ナナです。
プログラミング言語における基本的な動作は「順次」「分岐」「反復」の3つの要素にて構成されます。
大げさに言うと、この基本要素をぶっ壊すのが「goto文」です。C言語における「goto文」とは、ある意味「禁じ手」とされる道具なのです。
ただ、わたし自身は「goto文」を完全に使わないかと問われると「特定のケースに限っては使うことがある」としています。
「goto文」を使うケースと、使ってはダメなケースを紹介しましょう。
本記事では次の疑問点を解消する内容となっています。
では、「goto文」とは何かを学んでいきましょう。
goto文の書き方と特徴
それではgoto文を使ったプログラムとその特徴を示しましょう。
goto文の書式とサンプルプログラム
まずは「goto文」の書き方を紹介しましょう。
goto文は「goto命令」と「gotoラベル」の2つによって構成されます。
#include <stdio.h>
int main(void)
{
printf("Hello");
goto END; // goto命令
printf("World");
END: // gotoラベル
return 0;
}
Hello
結果として、「Hello」の文字列しか表示されていないのがわかりますね。
goto命令が実行されると、gotoラベルの場所にプログラムが移動します。これを「ジャンプする」と表現することもあります。
このようにとある関数内において、任意の場所にプログラムをジャンプさせることができます。
「goto」とは「~へ行く」という意味です。つまり、goto文とは「指定したラベルの場所へ行く」ということを示します。わかりやすいですね。
他の関数にプログラムをジャンプさせることはできませんよ!
goto文は「どこでもドア」であるということ
ドラえもんに登場する「どこでもドア」って道具がありますね。「goto文」ってプログラムの世界の「どこでもドア」なんです。
「どこでもドア」はあったら便利なんですが、「一瞬でどこにでも行ける」って、この世のルールを無視していますよね。
ルール無用の「どこでもドア」は実在していませんが、プログラムの世界での「goto文」は実在しているわけです。
そのため、次のようにラベルをいくつも配置して、複雑にgoto文を組み合わせたプログラムを作ることもできます。
#include <stdio.h>
int main(void)
{
char moji;
INPUT:
// キー入力を取得
moji = getchar();
if (moji == EOF)
{
goto END;
}
else if (moji == '\n')
{
goto INPUT;
}
printf("%c", moji);
goto INPUT;
END:
return 0;
}
「goto文」はラベルを用意すれば、どこにでも行ける魔法の道具になります。
このような複雑にからみ合った迷路のようなプログラムを、「スパゲッティプログラム」や「スパゲッティコード」と揶揄することがあります。
プログラムを任意の場所に移動させるって便利な面もあるのですが、「ルール無用で何でもあり」とも言えるのです。
安易に「goto文」を使うものではありません。
goto文はなぜ嫌われるのかを考察
「goto文はとにかく使うな!」ということをC言語を学び始めた方は有識者から指導されることがあります。
「goto文」禁止の文化
プログラムに不慣れな方に「goto文」を教えてしまうと、安易に「goto文」を使ってしまいバグを誘発したり、メンテナンスが大変になることがあります。
つまり、何でもありとなってしまう「goto文」というのは危険性が大きいのです。
そのため、企業におけるプログラム開発では、コーディングルールで使用禁止となっていることは珍しくありません。
多種多様なプログラミングレベルの人たちが混在する企業では、一律のルールを設けることで、品質低下を防止します。
上級者は「goto文」がなくても品質の高いプログラムを作ることができるため、一律禁止にしてしまった方が全体の品質を上げることができるのです。
goto文以外にもあるプログラムをジャンプさせる命令
C言語においてプログラムを別の場所にジャンプさせる命令というのは、「goto文」だけではありません。
C言語を学び始めたばかりの方は、他のジャンプ命令と「goto文」がそれほど異なるものという認識が薄いため、同じ感覚で使ってしまうということがあります。
それでは、これらのジャンプ命令は許されて「goto文」は禁止されるのはなぜなのでしょうか?
goto文は他のジャンプ命令とは異なる能力を持っています。まずは、return文などのジャンプ命令について考察しましょう。
「goto文」は他のジャンプ命令とは異なる異質な能力者なのです。
return文・continue文・break文におけるジャンプ場所の制約
「goto文」とその他のジャンプ命令との違いは、ジャンプできる場所に制約があることです。
「関数」や「反復処理」を1つのブロックとして捉えた場合、これらの命令はブロックの入口か出口にしかジャンプすることができません。
つまり、ジャンプができると言っても限られた制約の下で許可されたジャンプ命令なのです。
それに対し「goto文」は、gotoラベルを貼った場所に自由にジャンプすることができます。
この自由度の高さこそが、返ってルールを逸脱する要因となりうるのです。
どこにでも行ける「goto文」は自由人すぎるのです。
goto文を使ってはいけないアンチパターン
goto文は使うにしても「これはやらない方がいいよね!」というアンチパターンっていうのがあります。
次に紹介するようなプログラムは、けっして作らないようにしましょう。
プログラムの上方に戻るgoto文
プログラムの上から下に処理が進む順次処理が基本です。goto文を使って、上方にプログラムを巻き戻すのはやめましょう。
#include <stdio.h>
int main(void)
{
char moji;
INPUT:
moji = getchar();
if (moji != EOF)
{
printf("%c", moji);
// 上方にジャンプするgoto文
goto INPUT;
}
return 0;
}
上方にプログラムを戻したいということは、反復処理で表現できるはずです。「for文」や「while文」にできないかを検討しましょう!
制御ブロックに入るgoto文
「if文」や「while文」のブロック内にジャンプすることはやめましょう。
#include <stdio.h>
int main(void)
{
int i = 5;
char moji[10];
// for文のブロック内にジャンプ
goto BLOCK;
for (i = 0; i < 10; i++)
{
BLOCK:
moji[i] = getchar();
}
return 0;
}
「if文」や「for文」のブロックとは、とある条件が成立したときにのみ実行されるべき処理なのです。「goto文」で無条件に飛び込んでよい場所ではありません。
このようなプログラムが必要と考えたときは、プログラムの組み立て方が間違っているサインです。
考え方を変えましょう!
goto文を使用する唯一のパターン
それではわたしが「この場合だけgoto文を使うよ」と、自分ルールを決めているパターンを紹介しましょう。
あくまでも私が使用するシーンの紹介です。世の中には「絶対に使いませんっ」「自分はこのケースでも使用するよ」など様々なポリシーを持った方がいます。
資源の確保と解放が必要なプログラム
C言語において、管理がしづらく不具合が出やすいシーンとして「資源解放」というものがあります。
ここで表現する「資源」とは、「動的メモリ」や「ファイルハンドル」といった明示的な解放処理が必要なものです。プログラムで示しましょう。
動的メモリの確保と解放
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i;
char * pmem = NULL;
// 動的メモリの確保
pmem = (char *)malloc(100);
for (i=0 ; i < 100 ; i++)
{
pmem[i] = 'A';
}
// 動的メモリの解放
free(pmem);
return 0;
}
ファイルハンドルの生成と解放
#include <stdio.h>
#include <string.h>
int main(void)
{
int i;
FILE * fp = NULL;
// ファイルハンドルの生成
fopen_s(&fp, "sample.txt", "w");
for (i=0 ; i < 100 ; i++)
{
fputc('A', fp);
}
// ファイルハンドルの解放
fclose(fp);
return 0;
}
このようにmalloc関数やfopen_s関数で取得した資源は、最終的にfree関数やfclose関数による解放処理が必要となります。
ここで問題となるのが「資源の解放漏れ」の不具合です。
資源の確保・解放が多重になることによる解放漏れのリスク
1つの資源であれば、それほど解放漏れの恐れは少ないのですが、複数の資源になったとたん資源の解放処理というのは思ったより複雑になり、解放漏れのリスクが高くなります。
次のように動的メモリとファイルハンドルの両方を利用するプログラムを示しましょう。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int testFunc(void)
{
FILE * fp = NULL;
char * pmem = NULL;
// ファイルハンドルの生成
fopen_s(&fp, "sample.txt", "w");
// 動的メモリの確保
pmem = (char *)malloc(100);
if (pmem == NULL)
{
// 異常時の資源解放
fclose(fp);
return -1;
}
// 動的メモリへデータの読み込み
if (-1 == readData(pmem))
{
// 異常時の資源解放
free(pmem);
fclose(fp);
return -1;
}
// ファイルへのデータ書き出し
if (-1 == writeFile(pmem, fp))
{
// 異常時の資源解放
free(pmem);
fclose(fp);
return -1;
}
// 正常終了時の資源解放
fclose(fp);
free(pmem);
return 0;
}
白くなっている部分が資源の解放処理です。このように、プログラム異常時に資源を解放する処理が散乱していることがわかりますね。
しかも、場所によっては片方しか資源解放しない場合もあります。このようなプログラムはメンテナンスが非常に困難になっていきます。
関数内で資源がいくつも必要になる場合は、この異常時処理はものすごく複雑で解放漏れが起きやすくなります。
goto文を使った資源解放処理
こんな時に登場するのが、わたしが唯一使用する「goto文」を使った資源解放手順です。
先ほどのプログラムをgoto文を使って書き直してみましょう。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int testFunc(void)
{
int ret = -1; // 戻り値を異常で初期化
FILE * fp = NULL;
char * pmem = NULL;
// ファイルハンドルの生成
fopen_s(&fp, "sample.txt", "w");
// 動的メモリの確保
pmem = (char *)malloc(100);
if (pmem == NULL)
{
goto END;
}
// 動的メモリへデータの読み込み
if (-1 == readData(pmem))
{
goto END;
}
// ファイルへのデータ書き出し
if (-1 == writeFile(pmem, fp))
{
goto END;
}
// 正常終了
ret = 0;
END:
if (pmem != NULL)
{
free(pmem);
}
if (fp != NULL)
{
fclose(fp);
}
return ret;
}
異常時にgoto文によってENDラベルへジャンプさせました。ENDラベル先では資源の解放処理をまとめて書いておきます。
このようにすることによって、資源の解放処理が局所的に書けるようになりました。今後、獲得する資源がもし増加しても、同様の手順で追加すればよいですね。
ここで使用している「goto文」はあくまでも「return文」の延長線上にあるという考え方に基づいています。
「returnしたいが、解放処理を実施したい」、そのための「goto文」であるとルール付けしているのです。
goto文のまとめ
「goto文」は使うにしても非常に注意して使うものであり、安易に使う道具ではありません。C言語を学び始めたばかりの方は、やはり「使わない」ことを前提に学習しましょう。
この記事を参考に、皆さん自身が「自分はこうしよう!」と決められるように考えることが大事ですよ。