C言語を学習する過程で、多くのプログラマーが「goto文は使ってはいけない」という教えを一度は耳にするはずです。
無条件にプログラムの実行順序を飛ばすことができるこの命令は、強力である一方で、プログラムの構造を破壊する危険性を秘めています。
しかし、C言語の標準仕様には現在も含まれ続けており、Linuxカーネルなどの大規模なシステム開発においても、特定の条件下では積極的に活用されています。
本記事では、goto文の基本的な使い方から、なぜ忌み嫌われるのかという歴史的背景、そして現代のプログラミングにおいて推奨される具体的な活用例までをプロの視点で詳しく解説します。
C言語のgoto文とは:基本的な役割と構文
C言語における goto 文は、プログラム内の実行フローを、同一関数内の任意の位置に定義された「ラベル」へ強制的にジャンプさせるための制御文です。
通常、C言語はソースコードの上から下へと順番に実行されますが、goto 文を使用することで、条件分岐やループなどの通常の制御構造を無視して、指定した場所へ処理を飛ばすことができます。
goto文の基本構文
goto 文を使用するには、ジャンプ先の目印となる「ラベル」と、そこへジャンプするための「goto命令」の2つが必要です。
goto ラベル名;
// ... 中間の処理 ...
ラベル名:
// ジャンプ後に実行される処理
ラベル名は、通常の変数名などと同様の命名規則に従い、末尾にコロン : を付けることで定義します。
このラベルは、goto 文よりも前(上方向)にあっても、後ろ(下方向)にあっても構いません。
上方向へのジャンプはループ(繰り返し)のような動作を作り出し、下方向へのジャンプは処理のスキップのような動作を実現します。
ラベルの定義方法とスコープ
goto 文で指定するラベルには、関数スコープという特性があります。
これは、同じ関数内であればどこからでも参照できる一方で、関数をまたいで他の関数内のラベルへジャンプすることはできないという制約を意味します。
また、ラベルの直後には何らかの文(ステートメント)が必要です。
関数の最後にラベルを配置し、その直後に何も処理がない場合は、空文であるセミコロン ; を置く必要があります。
goto文が「原則禁止」とされる理由
多くの開発現場やプログラミング教育の場において、goto 文の使用は厳しく制限、あるいは禁止されています。
これには、ソフトウェア工学の歴史における重要な背景と、実務上の明確なデメリットが存在します。
スパゲッティプログラムと可読性の低下
goto 文の最大の問題点は、プログラムの実行順序が複雑に入り乱れる「スパゲッティコード」を誘発することです。
通常、if 文や for 文、while 文などの構造化された制御文を使用すれば、処理の入り口と出口が明確になり、コードを読み進める際の論理的な流れを追いやすくなります。
しかし、goto 文が多用されると、処理が突然別の場所に飛び、そこからさらに別の場所へ飛ぶといった連鎖が起こり得ます。
その結果、ある時点でのプログラムの状態(変数の値やメモリの状況)を把握することが極めて困難になり、コードの可読性は著しく低下します。
構造化プログラミングの原則に反する
1968年、計算機科学者のエドガー・ダイクストラ氏は「Go To Statement Considered Harmful(goto文は有害とみなされる)」という有名なエッセイを発表しました。
これをきっかけに、プログラムを「順次」「選択」「反復」という3つの基本構造だけで構成する構造化プログラミングの考え方が普及しました。
goto 文は、この構造化の概念を根底から壊す可能性があるため、現代的なプログラミングパラダイムにおいては「避けるべき悪習」として定着したのです。
保守性とデバッグの困難さ
goto 文によって複雑化したコードは、バグの温床となります。
例えば、変数の初期化処理を goto で飛び越えてしまい、未定義の値を使用してクラッシュするといったミスが発生しやすくなります。
また、後から修正を加える際、どこからジャンプしてくるか分からないラベルを不用意に削除・変更すると、予期せぬ場所で論理エラーが発生します。
このような保守コストの増大は、チーム開発において致命的な問題となるため、多くのコーディングガイドライン(MISRA Cなど)では goto 文の使用を禁止しています。
goto文のメリットと適切な活用シーン
一方で、goto 文を完全に排除することが必ずしも最善とは限りません。
C言語のような低レイヤーを扱う言語では、goto を使うことでかえってコードが簡潔になり、効率が向上するケースが存在します。
多重ループ(ネスト)からの脱出
C言語の break 文は、現在実行中の最も内側のループを1つ抜けることしかできません。
そのため、3重、4重に重なった深いループの最深部でエラーや特定の条件を検知し、一気にループの外へ脱出したい場合、break 文だけでは複数のフラグ管理が必要になり、コードが煩雑になります。
このような場合、goto 文を使用してループの外側のラベルへ一気にジャンプする手法は、非常に合理的で可読性の高い解決策となります。
エラー処理とリソース解放の集約(クリーンアップ処理)
関数内で複数のリソース(メモリ、ファイルポインタ、ミューテックスなど)を確保する場合、途中でエラーが発生した際にそれまでに確保したリソースをすべて適切に解放(クリーンアップ)しなければなりません。
もし goto を使わない場合、各エラーチェックの箇所で同じ解放処理を何度も記述するか、複雑な入れ子構造の if 文を書くことになります。
ここで、関数の終端に cleanup 用のラベルを用意し、エラー時にはそこへジャンプするように構成すれば、リソース解放のロジックを一箇所に集約でき、解放漏れ(メモリリークなど)を防ぐことができます。
実践:goto文の使い方とコード例
ここでは、前述した「適切な活用シーン」に基づいた具体的なコード例を紹介します。
多重ループを一括で抜けるプログラム
以下の例は、2次元配列の中から特定の値を探索し、見つかった瞬間にすべてのループを抜ける処理です。
#include <stdio.h>
int main() {
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
int target = 5;
int found = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (matrix[i][j] == target) {
found = 1;
goto found_label; // 多重ループを一度に脱出
}
}
}
printf("ターゲットは見つかりませんでした。\n");
return 0;
found_label:
printf("ターゲット %d を発見しました。\n", target);
return 0;
}
このコードでは、break を使う場合に必要となる「見つかったかどうかを判定するフラグのチェック」を外側のループで行う必要がなく、ロジックが非常にシンプルになっています。
共通のエラー処理へジャンプするプログラム
次に、ファイル操作とメモリ確保を伴う関数でのエラーハンドリングの例を見てみましょう。
#include <stdio.h>
#include <stdlib.h>
int process_data(const char *filename) {
FILE *fp = NULL;
char *buffer = NULL;
int result = 0;
fp = fopen(filename, "r");
if (fp == NULL) {
goto error;
}
buffer = (char *)malloc(1024);
if (buffer == NULL) {
goto error;
}
// データ処理のロジック(失敗した場合は result = -1 して goto error)
if (/* 何らかのエラー条件 */ 0) {
result = -1;
goto error;
}
// 正常終了時の処理
free(buffer);
fclose(fp);
return 0;
error:
// エラー発生時の一括クリーンアップ
if (buffer != NULL) free(buffer);
if (fp != NULL) fclose(fp);
return -1;
}
このように、関数の出口付近にエラー処理をまとめるパターンは、「On Error Goto」パターンとも呼ばれ、Linuxカーネルなどのプロフェッショナルなコードベースでも広く採用されています。
goto文を使用する際の注意点と制約
goto 文を安全に使用するためには、C言語の仕様上の制約を正しく理解しておく必要があります。
関数をまたぐジャンプは不可能
先述の通り、goto 文のジャンプ先ラベルは、その goto 文と同じ関数内に存在しなければなりません。
もし別の関数へジャンプしたい場合は、標準ライブラリの setjmp.h に定義されている setjmp 関数と longjmp 関数を使用する必要があります。
ただし、これらは goto 文以上に制御が複雑になるため、例外処理などの特殊な用途を除いて使用は推奨されません。
変数の宣言を飛び越える際の問題
C言語(特にC99以降)では、変数の初期化を伴う宣言を goto 文で飛び越えて、その変数を使用する箇所へジャンプすることはコンパイルエラーまたは未定義動作の原因となります。
| 状況 | 結果 |
|---|---|
| 変数の宣言(初期化なし)を飛び越える | 許容される(ただし推奨されない) |
| 変数の初期化(int a = 10; など)を飛び越える | C99以降ではコンパイルエラーになる場合がある |
| 可変長配列(VLA)の宣言を飛び越える | コンパイルエラー |
ジャンプ先でその変数を使用する場合、実行時には初期化コードを通っていないため、変数の値が不定となり深刻なバグを引き起こします。
setjmpとlongjmpとの違い
goto が「ローカルなジャンプ」であるのに対し、setjmp と longjmp は「非ローカルなジャンプ」と呼ばれます。
これらはスタックフレームを直接操作して関数呼び出しの履歴を遡るため、非常に強力ですが、メモリ管理やオブジェクトの生存期間を破壊するリスクが非常に高く、現代のC言語開発では極めて限定的な場面でしか利用されません。
goto文を使わずに書く代替手法
「goto を使いたい」と感じたとき、まずは他の構造化された手法で代用できないかを検討すべきです。
多くの場合、以下の方法でより安全に記述できます。
フラグ変数を用いたループ脱出
多重ループを抜ける最も一般的な代替案は、フラグ用変数を用意することです。
int found = 0;
for (int i = 0; i < 10 && !found; i++) {
for (int j = 0; j < 10; j++) {
if (target_check(i, j)) {
found = 1;
break; // 内側を抜ける
}
}
// ここで !found が評価され、外側も抜ける
}
この方法は構造化の原則に従っていますが、ループの条件式が複雑になるというデメリットもあります。
return文による関数終了
処理が複雑になった場合は、その部分を別の関数に切り出すのが最善です。
関数化してしまえば、どこからでも return 文を呼び出すだけで処理を終了し、呼び出し元へ戻ることができます。
do-while(0)とbreakの組み合わせ
マクロ定義などでよく使われるテクニックに、do { ... } while(0) の中で break を使う方法があります。
do {
if (error_a) break;
if (error_b) break;
// 正常処理
} while(0);
// クリーンアップ処理
これは goto を使わずに「一箇所へジャンプする」動作を模倣するものですが、本来のループとしての意味を持たないため、ややトリッキーな手法とみなされます。
現代のプログラミングにおけるgoto文の評価
現代のソフトウェア開発において、goto 文は「原則として使用禁止だが、特定のパターンにおいてのみ例外的に許可される」という立ち位置にあります。
例えば、GoogleのC++コーディングガイドラインや、多くの企業の標準規格では、goto の使用を原則認めていません。
しかし、前述の「関数下部でのクリーンアップ」に限っては、コードの重複を減らし、エラーハンドリングの漏れを防止する唯一のスマートな解決策として、熟練したプログラマーの間で容認されています。
重要なのは、goto を「楽をするため」に使うのではなく、「コードをよりシンプルで堅牢にするため」に使うという姿勢です。
もし goto を使わずに書いたコードが、過度なネストやフラグの乱立で読みづらくなっているのであれば、その時こそが goto 文の出番かもしれません。
まとめ
C言語の goto 文は、正しく使えば強力な武器になりますが、扱いを誤ればプログラムの品質を著しく低下させる諸刃の剣です。
- 禁止される理由: スパゲッティコード化を招き、可読性と保守性を損なうため。
- 主な活用例: 深い多重ループからの一括脱出や、関数内での共通エラー処理(クリーンアップ)の集約。
- 使用時の鉄則: 必ず「下方向(関数の出口方向)」へのジャンプに限定し、同じ関数内でのみ使用すること。
初心者の方は、まずは if や while などの構造化文を完璧に使いこなせるようになることを優先してください。
その上で、大規模な開発やエラー処理の設計において、どうしてもコードが複雑化してしまう場合の「最後の手段」として、goto 文の適切な利用を検討してみるのが良いでしょう。
技術の背景にある「なぜ」を理解することで、より高品質なソースコードを記述できるようになります。






