C言語でプログラムを終了する方法はいくつかありますが、初心者の方がまず押さえるべきはexit関数による正常終了です。
この記事では、exit
の基本、return
との違い、終了ステータスの考え方、終了時に行われる後処理、注意点、そして実践的なサンプルまで、丁寧に解説します。
exitとは何か
役割と基本動作
exitは、プログラムを直ちに終了させ、OSや呼び出し元に終了ステータスを返す関数です。
C標準ライブラリに含まれており、#include <stdlib.h>
で利用できます。
終了時には次のような後処理が自動的に行われます。
- 標準入出力を含む全ての開いているストリームのフラッシュとクローズ
- atexitで登録された関数の実行と、グローバル領域の終了処理
- OSへの終了コードの通知
main関数からreturn
で戻ることは、標準上exit
を呼ぶのと等価です。
ただしmain以外の関数からreturn
してもプログラム全体は終了しません。
どこからでも終了したい場合にexit
を使います。
必要なヘッダーと代表的なマクロ
exit
はstdlib.h
に宣言されています。
終了コードには次のマクロを用いるのが移植性の観点から安全です。
EXIT_SUCCESS
: 正常終了を表すEXIT_FAILURE
: 異常終了を表す
0を正常、非0を異常として扱う環境が一般的ですが、数値を直書きするよりEXIT_SUCCESS
とEXIT_FAILURE
を使うことを推奨します。
基本的な使い方
最もシンプルな例
次のプログラムはメッセージを出力して正常終了します。
exit(EXIT_SUCCESS)
の代わりにreturn 0
でも同じ意味になります。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// 何らかの処理
puts("処理が完了しました。正常終了します。");
// 正常終了をOSに通知してプロセスを終える
exit(EXIT_SUCCESS);
// 到達しないコード
// return 0; // これでも同等ですが、すでにexitで終了しているため不要です
}
処理が完了しました。正常終了します。
改行なし出力がflushされることの確認
exitは標準出力のバッファも自動的にフラッシュします。
改行がなくても出力が失われないことを確認します。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// 改行なしで出力。通常はバッファに溜まる可能性がある
fputs("開始→", stdout);
// ここでflushされるため、改行がなくても表示される
exit(EXIT_SUCCESS);
}
開始→
returnとの違いを正しく理解する
mainからのreturnはexitと等価
C標準では、main関数からreturn
で戻ることはexit
を呼ぶのと等価です。
つまり、どちらもストリームのフラッシュやatexit
関数の実行が行われ、OSへ終了ステータスが返されます。
深い関数からでも終了できるのがexit
一方で、return
は現在の関数から呼び出し元に戻るだけです。
「どこからでも即座にプログラム全体を終えたい」場合はexit
を使います。
ただし、頻繁に深い場所から終了させる設計は保守性を下げるため、後述の注意点も参照してください。
ふるまいの比較表
以下はmain
からのreturn
とexit
の比較です。
項目 | mainからreturn | exit |
---|---|---|
意味 | mainの終了と同時にプロセス終了 | その場でプロセス終了 |
atexit登録関数 | 実行される | 実行される |
標準I/Oのフラッシュ | 行われる | 行われる |
終了ステータス | return値が使用される | 引数が使用される |
main以外からの利用 | 不可(戻るだけ) | 可能(プロセスを終える) |
「main以外の関数から終了したいときはexit」、「mainの最後で終えるならreturnでもexitでも等価」という整理で覚えると理解しやすいです。
終了ステータスを設計する
基本方針とマクロの活用
- 成功時は
EXIT_SUCCESS
- 失敗時は
EXIT_FAILURE
数値を直接書くよりマクロを用いると移植性が高まります。
エラー種類ごとに独自の非0コードを割り当てたい場合もありますが、まずは成功と失敗の2値から始めるのが初心者には分かりやすいです。
シェルから終了コードを確認する例
UNIX系のシェルでは$?
で直前の終了コードを確認できます。
$ ./a.out
$ echo $?
0
0が正常、非0がエラーとして扱われるため、スクリプト連携時に重要になります。
Windowsのコマンドプロンプトでも終了コードは参照でき、ビルドツールやCIが成功/失敗を判断する基準になります。
exitが行う後処理を具体的に知る
atexitで登録した関数の実行順序
atexitで登録した関数は、exit時に逆順で呼ばれます。
後から登録した関数が先に実行されます。
#include <stdio.h>
#include <stdlib.h>
void cleanup1(void) {
puts("cleanup1: 最後の後始末を実行");
}
void cleanup2(void) {
puts("cleanup2: 一時ファイルの削除を実行");
}
int main(void) {
// 終了時に行いたい処理を登録
atexit(cleanup1);
atexit(cleanup2);
puts("メイン処理中...");
// 何らかの事情でここで正常終了したい
exit(EXIT_SUCCESS);
}
メイン処理中...
cleanup2: 一時ファイルの削除を実行
cleanup1: 最後の後始末を実行
標準入出力のフラッシュとファイルクローズ
exitは開いている全てのファイルストリームをフラッシュし、クローズします。
そのため、fopen
で開いたファイルに書き込んだデータも、明示的にfclose
しなくても通常は失われません。
ただし、明示的なクローズやエラーチェックを行う設計の方が安全です。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fp = fopen("example.txt", "w");
if (!fp) {
perror("fopen");
exit(EXIT_FAILURE);
}
fputs("ファイルに書き込み中...\n", fp);
// 明示的にfcloseせずに終了
// exitが呼ばれるとfpはフラッシュ・クローズされる
exit(EXIT_SUCCESS);
}
このプログラム終了後、example.txt
には「ファイルに書き込み中…」が保存されています。
よくある誤解と注意点
exitの多用は設計を複雑にする
関数の深い場所から頻繁にexitすると、処理の流れが追いづらくなります。
可能であればエラーコードを返し、呼び出し元で一元的に終了判定と後片付けをする設計が読みやすいです。
例外的に「どうしても続行不能」な状況にだけexit
を使う方針が無難です。
ライブラリコードからのexitは避ける
再利用されるライブラリ関数の内部でexit
を呼ぶと、ライブラリ利用側が制御できなくなるため好まれません。
エラーは戻り値やエラーコールバックで伝えるのが一般的です。
マルチスレッド環境での注意
exitはプロセス全体を終了します。
スレッド単体を終了したい場合は環境に応じてthrd_exit
(C11スレッド)やpthread_exit
(POSIX)などを使用します。
数値直書きよりマクロを使う
1や-1などの数値を直接使うと移植性や可読性が落ちます。
EXIT_SUCCESS
/EXIT_FAILURE
を基本として、必要に応じて定義済みのエラーコード表を設計しましょう。
実践例
例1: 引数検証での早期正常終了・異常終了
引数が不足している場合は使い方を表示してEXIT_FAILURE
で終了、正しく渡されたら処理してEXIT_SUCCESS
で終了します。
#include <stdio.h>
#include <stdlib.h>
static void print_usage(const char *prog) {
fprintf(stderr, "使い方: %s <名前>\n", prog);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
print_usage(argv[0]);
// 失敗を表す終了ステータスを返す
exit(EXIT_FAILURE);
}
printf("こんにちは、%sさん!\n", argv[1]);
// 正常終了
exit(EXIT_SUCCESS);
}
$ ./greet
使い方: ./greet <名前>
$ echo $?
1
$ ./greet Taro
こんにちは、Taroさん!
$ echo $?
0
例2: atexitでリソースを解放してから終了
exit時に確実に実行したい後片付けをatexit
で登録しておくと、途中でexit
しても漏れなくクリーンアップできます。
#include <stdio.h>
#include <stdlib.h>
static FILE *g_log = NULL;
static char *g_buf = NULL;
static void cleanup_all(void) {
// ここはexit時に必ず呼ばれる
if (g_log) {
fputs("cleanup: closing log\n", g_log);
fclose(g_log);
g_log = NULL;
}
free(g_buf);
g_buf = NULL;
// 目に見えるように標準出力にも通知
puts("cleanup: resources freed");
}
int main(void) {
// 後片付けを登録
if (atexit(cleanup_all) != 0) {
fputs("atexit登録に失敗しました\n", stderr);
exit(EXIT_FAILURE);
}
g_log = fopen("app.log", "w");
if (!g_log) {
perror("fopen");
exit(EXIT_FAILURE);
}
g_buf = malloc(1024);
if (!g_buf) {
fputs("メモリ確保に失敗\n", stderr);
exit(EXIT_FAILURE); // cleanup_allが呼ばれる
}
fputs("processing...\n", g_log);
// 途中で想定外の条件が発生したと仮定
fputs("エラー: 入力が不正\n", stderr);
exit(EXIT_FAILURE); // ここで終了してもcleanup_allが呼ばれる
}
エラー: 入力が不正
cleanup: resources freed
app.logの内容:
processing...
cleanup: closing log
ログファイルが閉じられ、バッファが解放されていることが確認できます。
まとめ
exitはプログラムを正常に終了させ、OSへ明確な終了ステータスを返すための基本関数です。
mainからのreturn
はexit
と等価ですが、exit
は任意の場所からプロセス全体を終わらせられる点が実用的です。
終了コードにはEXIT_SUCCESS
/EXIT_FAILURE
を用い、atexit
と組み合わせることで、ストリームのフラッシュやリソース解放を確実に実施できます。
設計面では、exitの多用は処理の見通しを悪くするため、原則は上位でエラー処理を集約し、どうしても続行不能な場面に限って使用するのが良いでしょう。
以上を押さえておけば、「正常に終える」というプログラムの基本品質を堅実に満たせます。