閉じる

【C言語】exitで正常終了する方法とreturnとの違い

C言語でプログラムを終了する方法はいくつかありますが、初心者の方がまず押さえるべきはexit関数による正常終了です。

この記事では、exitの基本、returnとの違い、終了ステータスの考え方、終了時に行われる後処理、注意点、そして実践的なサンプルまで、丁寧に解説します。

exitとは何か

役割と基本動作

exitは、プログラムを直ちに終了させ、OSや呼び出し元に終了ステータスを返す関数です。

C標準ライブラリに含まれており、#include <stdlib.h>で利用できます。

終了時には次のような後処理が自動的に行われます。

  • 標準入出力を含む全ての開いているストリームのフラッシュとクローズ
  • atexitで登録された関数の実行と、グローバル領域の終了処理
  • OSへの終了コードの通知

main関数からreturnで戻ることは、標準上exitを呼ぶのと等価です。

ただしmain以外の関数からreturnしてもプログラム全体は終了しません

どこからでも終了したい場合にexitを使います。

必要なヘッダーと代表的なマクロ

exitstdlib.hに宣言されています。

終了コードには次のマクロを用いるのが移植性の観点から安全です。

  • EXIT_SUCCESS: 正常終了を表す
  • EXIT_FAILURE: 異常終了を表す

0を正常、非0を異常として扱う環境が一般的ですが、数値を直書きするよりEXIT_SUCCESSEXIT_FAILUREを使うことを推奨します。

基本的な使い方

最もシンプルな例

次のプログラムはメッセージを出力して正常終了します。

exit(EXIT_SUCCESS)の代わりにreturn 0でも同じ意味になります。

C言語
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // 何らかの処理
    puts("処理が完了しました。正常終了します。");

    // 正常終了をOSに通知してプロセスを終える
    exit(EXIT_SUCCESS);

    // 到達しないコード
    // return 0; // これでも同等ですが、すでにexitで終了しているため不要です
}
実行結果
処理が完了しました。正常終了します。

改行なし出力がflushされることの確認

exitは標準出力のバッファも自動的にフラッシュします

改行がなくても出力が失われないことを確認します。

C言語
#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からのreturnexitの比較です。

項目mainからreturnexit
意味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時に逆順で呼ばれます

後から登録した関数が先に実行されます。

C言語
#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しなくても通常は失われません。

ただし、明示的なクローズやエラーチェックを行う設計の方が安全です。

C言語
#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で終了します。

C言語
#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しても漏れなくクリーンアップできます。

C言語
#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からのreturnexitと等価ですが、exit任意の場所からプロセス全体を終わらせられる点が実用的です。

終了コードにはEXIT_SUCCESS/EXIT_FAILUREを用い、atexitと組み合わせることで、ストリームのフラッシュやリソース解放を確実に実施できます。

設計面では、exitの多用は処理の見通しを悪くするため、原則は上位でエラー処理を集約し、どうしても続行不能な場面に限って使用するのが良いでしょう。

以上を押さえておけば、「正常に終える」というプログラムの基本品質を堅実に満たせます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!