C言語で本番運用されるプログラムを書くとき、終了処理をどう整理するかはとても重要です。
特に、異常終了や途中でのreturnが増えてくると、最後に必ず実行したいクリーンアップ処理を漏れなく書くのが難しくなります。
この記事では、C標準ライブラリが用意しているプログラム終了時に自動で呼び出されるフック機構であるatexitの使い方を、図解とサンプルコードを交えて詳しく解説します。
C言語のatexitとは何か
atexitの基本的な役割

atexit関数は、プログラムがexitによって終了するときに呼び出される関数を事前登録する仕組みです。
登録された関数は、プログラム終了時に自動的に実行されるため、後片付けやログ出力などをまとめて記述できます。
標準的な説明を整理すると、次のようになります。
- ヘッダファイル
#include <stdlib.h>で宣言されています。 - プロトタイプは
int atexit(void (*func)(void));です。 - 引数
funcは引数も戻り値もない関数です。 - 登録した関数はプログラムが正常終了するときに自動で呼ばれます。
ここでポイントになるのは、「mainの最後に片付けを書く」のではなく「終了時に呼ばれる関数を登録しておく」という発想に切り替えることです。
exitとatexitの関係

C標準では、プログラム終了には2つの代表的なパターンがあります。
return 0;などでmain関数を抜けるexit(status);を呼び出す
どちらの場合でも、最終的にはexitが呼び出され、その過程でatexitで登録しておいた関数が実行されます。
つまり「プログラムが正常に終了する限り、atexitで登録した関数は呼ばれる」と考えられます。
逆に言うと異常終了時に_Exitやabortを使うと、atexitの処理は実行されない点に注意が必要です。
atexitの基本的な使い方
最も単純なサンプルコード
まずは、終了メッセージを表示するだけのシンプルな例で使い方を確認します。
#include <stdio.h>
#include <stdlib.h>
// atexitで登録する終了処理用の関数
// ・引数なし
// ・戻り値なし
void on_exit_handler(void) {
printf("プログラム終了処理を実行しています...\n");
}
int main(void) {
// 終了時に呼び出したい関数を登録
if (atexit(on_exit_handler) != 0) {
// 登録に失敗した場合のエラーメッセージ
fprintf(stderr, "atexitの登録に失敗しました\n");
return EXIT_FAILURE;
}
printf("メイン処理を実行中...\n");
// ここでmainを抜けると、自動的にon_exit_handlerが呼び出される
return EXIT_SUCCESS;
}
メイン処理を実行中...
プログラム終了処理を実行しています...
この例では、main関数の中でatexit(on_exit_handler);と登録しただけで、最後に明示的にon_exit_handlerを呼び出していないことに注目してください。
それでも、プログラム終了時に自動的に呼び出されています。
atexitで登録できる関数の条件
atexitで登録できるのは「引数も戻り値もない関数」だけです。
型としては次の関数ポインタに一致する必要があります。
void func(void);
そのため、たとえば次のような関数は直接登録できません。
void cleanup_with_arg(int code); // NG: 引数あり
int cleanup_with_ret(void); // NG: 戻り値あり
それでもどうしても引数持ちの処理をしたい場合は、グローバル変数やstatic変数に状態を保存しておき、atexitハンドラ内で参照するといった工夫を行います。
登録順序と呼び出し順序(LIFO)
複数登録したときの挙動

atexitで複数の関数を登録した場合、最後に登録したものから順に呼び出されます。
これはスタック構造(LIFO)になっていると理解すると分かりやすくなります。
次のサンプルで実際の動きを確認してみます。
#include <stdio.h>
#include <stdlib.h>
void handler1(void) {
printf("handler1: 終了処理1\n");
}
void handler2(void) {
printf("handler2: 終了処理2\n");
}
void handler3(void) {
printf("handler3: 終了処理3\n");
}
int main(void) {
// 登録順に注目
atexit(handler1);
atexit(handler2);
atexit(handler3);
printf("メイン処理中...\n");
return 0;
}
メイン処理中...
handler3: 終了処理3
handler2: 終了処理2
handler1: 終了処理1
「登録順」と「実行順」が逆になることが、後片付けを設計するときの重要なポイントです。
なぜLIFOが便利なのか
例えば、次のような順番でリソースを確保したとします。
- ログファイルを開く
- メモリバッファを確保する
- 通信ソケットを開く
このとき、クリーンアップの順番は一般的に逆順のほうが安全です。
- 通信ソケットを閉じる
- メモリバッファを解放する
- ログファイルを閉じる
atexitは「確保した順に登録していけば、勝手に逆順で解放される」という構造を提供してくれます。
複雑なプログラムほど、このLIFOルールを活用することでクリーンな終了処理を書きやすくなります。
実用的な例: ファイルとメモリの片付け
リソース管理にatexitを使うイメージ

ここでは、ファイルと動的メモリの後片付けをatexitに任せる例を見ていきます。
実務でもよくあるパターンです。
#include <stdio.h>
#include <stdlib.h>
// グローバルにリソースハンドルを持つ例
static FILE *g_log_fp = NULL;
static char *g_buffer = NULL;
// 終了処理(1): ログファイルを閉じる
void close_log_file(void) {
if (g_log_fp != NULL) {
printf("ログファイルを閉じます\n");
fclose(g_log_fp);
g_log_fp = NULL;
}
}
// 終了処理(2): メモリバッファを解放する
void free_buffer(void) {
if (g_buffer != NULL) {
printf("メモリバッファを解放します\n");
free(g_buffer);
g_buffer = NULL;
}
}
int main(void) {
// ログファイルを開く
g_log_fp = fopen("sample.log", "w");
if (g_log_fp == NULL) {
perror("ログファイルのオープンに失敗しました");
return EXIT_FAILURE;
}
// 終了時にログファイルを閉じるよう登録
if (atexit(close_log_file) != 0) {
fprintf(stderr, "close_log_fileの登録に失敗しました\n");
fclose(g_log_fp);
return EXIT_FAILURE;
}
// メモリバッファを確保
g_buffer = malloc(1024);
if (g_buffer == NULL) {
fprintf(stderr, "メモリの確保に失敗しました\n");
return EXIT_FAILURE; // ここでreturnしてもclose_log_fileは呼ばれる
}
// 終了時にメモリを解放するよう登録
if (atexit(free_buffer) != 0) {
fprintf(stderr, "free_bufferの登録に失敗しました\n");
free(g_buffer);
g_buffer = NULL;
return EXIT_FAILURE;
}
fprintf(g_log_fp, "メイン処理を実行中...\n");
printf("メイン処理が完了しました\n");
// ここでreturnすると、free_buffer -> close_log_fileの順に呼ばれる
return EXIT_SUCCESS;
}
この例のポイントを整理します。
- ファイルとメモリをそれぞれ専用の終了処理関数に分離しています。
atexitへの登録順序は「ファイル → メモリ」ですが、実際に呼ばれる順序は「メモリ → ファイル」です。- 途中で
returnしても、すでに登録済みの終了処理はきちんと実行されるため、片付け漏れを防ぎやすくなります。
なお、実際にこのコードを動かすと標準出力のメッセージの後に、登録順に応じて片付けメッセージが表示されます。
ログファイルsample.logには、"メイン処理を実行中...\n"が出力されているはずです。
atexitを安全に使うための注意点
abortや_Exitでは呼ばれない
atexitで登録した関数は、abortや_Exitで終了した場合には呼ばれません。
異常終了パスが多いプログラムでは、この点を意識して設計する必要があります。
例えば、致命的エラーではabortを使う方針を取る場合、「その場合はリソース解放はあきらめる」と割り切るか、あるいは致命的でもexitを通して終了処理を動かすなど、方針をチームで統一しておくと安全です。
マルチスレッド環境での扱い
マルチスレッドプログラムでもatexitは使えますが、次のような点に注意が必要です。
- atexitで登録されるのはプロセス単位であり、スレッド単位ではありません。
- 終了処理が呼ばれるときには、すでに他のスレッドが終了している可能性があります。
- そのため、スレッド同期に依存したリソース解放処理をatexitに入れると危険です。
スレッド関連のクリーンアップは、可能であればpthread_cleanup_pushなど、スレッド用のクリーンアップ機構を使うことを検討してください。
どこまでをatexitに任せるか
atexitは万能ではありません。
例えば次のような処理をすべてatexit側に寄せてしまうと、かえって見通しが悪くなります。
- エラー時に直ちにユーザーへ詳細なメッセージを表示したい処理
- 処理途中で再試行などの分岐を伴うロジック
- 終了前に対話的に確認を行うようなUI処理
atexitに任せるのは、基本的に次のような副作用が小さく、順番が重要な「片付けだけ」の処理に限定するとよいです。
- メモリ解放
- ファイルクローズ
- ログのフラッシュ
- 一時ディレクトリの削除
atexitを使うべき場面・避けるべき場面
atexitが有効に働く典型例
次のような状況では、atexitを使うことでコードがシンプルになり、安全性も上がります。
- 途中で
returnやgotoが多い長いmain関数 - ライブラリとして使うわけではなく、1プロセス完結タイプのバッチツールやコマンドラインツール
- 開くファイルやソケット、確保するメモリが多く、終了時の解放順が重要な場合
このようなケースでは、「リソース確保時に、同時に終了処理も登録する」というコーディングスタイルを取ると、バグを減らしやすくなります。
逆に避けたほうがよいケース
一方、次のような場合には、atexitにあまり依存しない方がよいことが多いです。
- 長時間動き続けるサーバープロセスで、終了時よりも生存中のエラー処理が重要な場合
forkやexecを多用するプロセス制御系のプログラム- 再初期化や再起動を繰り返すライブラリ(API)側のコード
こうした場面では、より明示的な「init/cleanup」APIを用意し、呼び出し側の責任でクリーンアップしてもらう方が設計として安全です。
まとめ
atexitは、C言語の標準ライブラリが用意している「プログラム終了時に必ず呼ばれるフック機構」です。
引数と戻り値を持たない関数を登録しておくだけで、exitやmainからのreturn時に、自動的かつ逆順(LIFO)で終了処理を実行してくれます。
これにより、ファイルクローズやメモリ解放などのクリーンアップ処理を漏れなく行いやすくなり、複雑なエラーパスを持つプログラムでも安全性を高めることができます。
ただし、abortや_Exitでは呼ばれないことや、マルチスレッド環境での扱いなど、いくつかの制約も存在します。
実務では、リソース片付けと終了順序が重要な部分に絞って、atexitをバランスよく活用することが、読みやすく安全なCプログラムへの近道になります。
