プログラムが終了する瞬間に、メモリ解放や一時ファイルの削除などの後始末を自動で実行できたら便利です。
C言語にはそのための標準仕組みとしてatexitが用意されています。
本記事では、初心者の方でも迷わないように基本から使い方、注意点、ベストプラクティスまでを丁寧に解説します。
atexitの基本
atexitとは
atexitは、プログラムが正常終了する際に自動的に呼び出される関数を登録するための標準ライブラリ関数です。
登録された関数は、後入れ先出し(LIFO)の順序で呼び出されます。
プロトタイプはint atexit(void (*func)(void));で、成功時は0、失敗時は非0を返します。
登録できる関数の数は実装依存ですが、最低でも32個は登録できることが規定されています。
仕様の要点
- 呼び出されるのは
exitによる正常終了時です。mainからのreturnもexitと等価に扱われます。 - 複数回登録された同一関数は、その回数分呼び出されます。
exitはatexitで登録された関数を呼び出した後に、標準入出力のバッファをフラッシュしてストリームを閉じます。したがって、atexit内でのprintf出力は最終的にフラッシュされます。
atexitの使いどころ
終了間際に確実に実行したいクリーンアップ処理を登録します。
例えば次のような用途が適しています。
- 一時ファイルの削除やファイルクローズ
- 動的に確保したメモリの解放
- ログの最終出力やフラッシュ
- ミューテックスの破棄や簡単なリソース解放
一方で、ネットワーク通信や時間のかかる計算など、長時間ブロックする処理は避けるのが基本です。
atexitに必要なヘッダー
#include <stdlib.h>をインクルードします。
合わせて標準入出力を使う場合は#include <stdio.h>も必要です。
#include <stdlib.h> // atexit, exit
#include <stdio.h> // printf など
atexitに登録できる関数(void func(void))
登録できるのは引数なし、戻り値なしの関数だけです。
関数ポインタの型はvoid (*func)(void)です。
引数を渡したい場合は、静的変数やグローバル変数、またはシングルトンな状態を参照するラッパー関数を用意します(後述)。
できないこと
- 直接引数を渡すことはできません
- 例外や
longjmpで終了関数から外へ飛ぶ設計は避けましょう - 終了関数の中で
exitを呼ぶのは非推奨です(後述)
atexitの使い方
基本の書き方
最小構成では、終了時に実行したい処理を関数として定義し、mainなどでatexit登録します。
戻り値はチェックし、失敗時の代替策を用意するのが安全です。
基本例
#include <stdlib.h>
#include <stdio.h>
static void on_exit_cleanup(void) {
// 終了時に必ず呼ばれる関数
printf("[cleanup] goodbye!\n");
}
int main(void) {
if (atexit(on_exit_cleanup) != 0) {
// 登録失敗時の対処。実装依存で稀に失敗する可能性があります。
fprintf(stderr, "atexit registration failed\n");
return 1;
}
printf("main is running...\n");
// ここでreturnしてもexitと同様にcleanupが呼ばれます
return 0;
}
main is running...
[cleanup] goodbye!
実用的な例として、メモリ解放と一時ファイル削除を登録してみます。
引数を渡せない制約は、静的変数に状態を保持することで回避します。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
static char *g_buffer = NULL; // 後でfreeするためのグローバル
static char g_tmp_path[256] = {0}; // 削除対象の一時ファイルパス
static void cleanup_memory(void) {
if (g_buffer) {
printf("[cleanup] free buffer\n");
free(g_buffer);
g_buffer = NULL;
}
}
static void cleanup_tempfile(void) {
if (g_tmp_path[0] != '#include <stdlib.h>
#include <stdio.h>
#include <string.h>
static char *g_buffer = NULL; // 後でfreeするためのグローバル
static char g_tmp_path[256] = {0}; // 削除対象の一時ファイルパス
static void cleanup_memory(void) {
if (g_buffer) {
printf("[cleanup] free buffer\n");
free(g_buffer);
g_buffer = NULL;
}
}
static void cleanup_tempfile(void) {
if (g_tmp_path[0] != '\0') {
printf("[cleanup] remove tempfile: %s\n", g_tmp_path);
remove(g_tmp_path); // 失敗しても終了処理は継続
g_tmp_path[0] = '\0';
}
}
int main(void) {
// 終了時クリーンアップの登録は「早め」が基本
if (atexit(cleanup_tempfile) != 0 || atexit(cleanup_memory) != 0) {
fprintf(stderr, "atexit registration failed\n");
return 1;
}
// リソースの獲得
g_buffer = malloc(1024);
if (!g_buffer) {
fprintf(stderr, "malloc failed\n");
return 1; // atexitに登録済みなので、ここでreturnしても解放される
}
strcpy(g_buffer, "hello atexit");
// 一時ファイルの作成
strcpy(g_tmp_path, "demo_tmp.txt");
FILE *fp = fopen(g_tmp_path, "w");
if (!fp) {
fprintf(stderr, "failed to create tempfile\n");
return 1; // atexitにより残骸を掃除
}
fprintf(fp, "temp data: %s\n", g_buffer);
fclose(fp);
printf("work done. exiting now...\n");
return 0;
}
') {
printf("[cleanup] remove tempfile: %s\n", g_tmp_path);
remove(g_tmp_path); // 失敗しても終了処理は継続
g_tmp_path[0] = '#include <stdlib.h>
#include <stdio.h>
#include <string.h>
static char *g_buffer = NULL; // 後でfreeするためのグローバル
static char g_tmp_path[256] = {0}; // 削除対象の一時ファイルパス
static void cleanup_memory(void) {
if (g_buffer) {
printf("[cleanup] free buffer\n");
free(g_buffer);
g_buffer = NULL;
}
}
static void cleanup_tempfile(void) {
if (g_tmp_path[0] != '\0') {
printf("[cleanup] remove tempfile: %s\n", g_tmp_path);
remove(g_tmp_path); // 失敗しても終了処理は継続
g_tmp_path[0] = '\0';
}
}
int main(void) {
// 終了時クリーンアップの登録は「早め」が基本
if (atexit(cleanup_tempfile) != 0 || atexit(cleanup_memory) != 0) {
fprintf(stderr, "atexit registration failed\n");
return 1;
}
// リソースの獲得
g_buffer = malloc(1024);
if (!g_buffer) {
fprintf(stderr, "malloc failed\n");
return 1; // atexitに登録済みなので、ここでreturnしても解放される
}
strcpy(g_buffer, "hello atexit");
// 一時ファイルの作成
strcpy(g_tmp_path, "demo_tmp.txt");
FILE *fp = fopen(g_tmp_path, "w");
if (!fp) {
fprintf(stderr, "failed to create tempfile\n");
return 1; // atexitにより残骸を掃除
}
fprintf(fp, "temp data: %s\n", g_buffer);
fclose(fp);
printf("work done. exiting now...\n");
return 0;
}
';
}
}
int main(void) {
// 終了時クリーンアップの登録は「早め」が基本
if (atexit(cleanup_tempfile) != 0 || atexit(cleanup_memory) != 0) {
fprintf(stderr, "atexit registration failed\n");
return 1;
}
// リソースの獲得
g_buffer = malloc(1024);
if (!g_buffer) {
fprintf(stderr, "malloc failed\n");
return 1; // atexitに登録済みなので、ここでreturnしても解放される
}
strcpy(g_buffer, "hello atexit");
// 一時ファイルの作成
strcpy(g_tmp_path, "demo_tmp.txt");
FILE *fp = fopen(g_tmp_path, "w");
if (!fp) {
fprintf(stderr, "failed to create tempfile\n");
return 1; // atexitにより残骸を掃除
}
fprintf(fp, "temp data: %s\n", g_buffer);
fclose(fp);
printf("work done. exiting now...\n");
return 0;
}
work done. exiting now...
[cleanup] free buffer
[cleanup] remove tempfile: demo_tmp.txt
複数登録の実行順
最後に登録したものから順に呼び出される(LIFO)ことを確認します。
同じ関数を複数回登録すると、その回数分呼ばれます。
#include <stdlib.h>
#include <stdio.h>
static void f1(void) { printf("f1\n"); }
static void f2(void) { printf("f2\n"); }
static void f3(void) { printf("f3\n"); }
int main(void) {
atexit(f1);
atexit(f2);
atexit(f3);
atexit(f2); // 同じ関数をもう一度
printf("registered f1, f2, f3, f2; exiting...\n");
return 0; // f2 -> f3 -> f2 -> f1 の順に呼ばれる
}
registered f1, f2, f3, f2; exiting...
f2
f3
f2
f1
mainのreturnとexitでの動作
mainからのreturnはexitと同等で、どちらでもatexitは実行されます。
#include <stdlib.h>
#include <stdio.h>
static void on_exit(void) { printf("[cleanup] called\n"); }
int main(int argc, char **argv) {
atexit(on_exit);
if (argc > 1) {
printf("calling exit(0)\n");
exit(0);
} else {
printf("returning from main\n");
return 0;
}
}
実行結果例1(引数なしで実行)
returning from main
[cleanup] called
実行結果例2(引数ありで実行)
calling exit(0)
[cleanup] called
atexitの注意点
引数は渡せない
登録関数はvoid func(void)固定のため、直接引数は渡せません。
必要な情報は次の方法で参照します。
- 静的またはグローバル変数に状態を保持する
- 1つの終了関数内で複数のリソースリストを走査して一括解放する
- ライブラリ化する場合は、初期化関数でグローバル状態を設定してから
atexit登録する
複数の独立したリソースがある場合は、1リソース1終了関数に分けると見通しが良くなります。
異常終了や強制終了では動かない
異常終了やプロセス強制終了ではatexitは呼ばれません。
代表的なケースを表にまとめます。
| 終了の仕方 | atexitは呼ばれるか | 備考 |
|---|---|---|
| mainからreturn | はい | returnはexitと等価 |
| exit(n) | はい | LIFO順で呼び出し後、ストリームがフラッシュ/クローズ |
| abort() | いいえ | 異常終了。呼ばれない |
| _Exit(n) | いいえ | 即時終了。後処理なし |
| quick_exit(n) | いいえ | at_quick_exitが対象。atexitは対象外 |
| SIGKILLなどの強制終了 | いいえ | OSが即時終了。後処理なし |
| SIGTERM受信→ハンドラでexit | はい | ハンドラからexitすれば実行される |
本当に必ず実行したい処理は、OS側の機構やジャーナリング、耐障害設計を別途検討してください。
atexitはあくまで正常終了時の保険です。
終了関数内でexitは呼ばない
終了関数はexitから呼ばれます。
その中で再びexitを呼ぶと、処理の再入や多重実行のリスクがあり、実装依存の挙動になります。
終了関数は早くreturnすることを基本とし、どうしても中断したい特殊ケースは_Exitで即時終了を検討します(ただしこの場合も残りの後処理は実行されません)。
時間のかかる処理は避ける
終了直前の文脈では、処理の予測可能性と速さが重要です。
ネットワークやユーザインタラクション、長時間待ちが発生するI/Oは避け、少量の解放・クローズ・削除程度にとどめます。
ログ出力は短く、失敗しても失敗を飲み込むほうが安全です。
実行順に依存しすぎない
複数のモジュールがそれぞれatexit登録を行うと、最終的な実行順は登録された順序に依存します。
モジュール間で「どちらが先に呼ばれるべきか」に強い依存を持たせる設計は避けるべきです。
必要ならば、1つの集約終了関数を用意し、その関数から意図した順番でサブ処理を呼ぶ構成にします。
初心者向けベストプラクティス
開始時に登録して漏れを防ぐ
リソースを獲得したら、すぐに対応する終了関数を登録するのが安全です。
早めの登録により、以降の途中エラーで早期returnしてもクリーンアップ漏れを防げます。
1関数1役割でシンプルに
終了関数は短く・単機能であるほど安全です。
例えば「メモリ解放」「一時ファイル削除」「ログフラッシュ」を別々の関数に分け、必要な数だけ登録します。
一時ファイルやメモリの片付けに使う
一時ファイルの削除やメモリ解放はatexitの得意分野です。
以下は、簡潔な一時ファイル削除のラッパ例です。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
static char g_tmp_path[256];
static void remove_tmp_on_exit(void) {
if (g_tmp_path[0]) {
printf("[cleanup] remove %s\n", g_tmp_path);
remove(g_tmp_path);
}
}
int main(void) {
atexit(remove_tmp_on_exit); // 先に登録
strcpy(g_tmp_path, "mytmp.dat");
FILE *fp = fopen(g_tmp_path, "w");
if (!fp) {
fprintf(stderr, "cannot create tempfile\n");
return 1; // atexitにより後片付け
}
fputs("temporary\n", fp);
fclose(fp);
printf("done.\n");
return 0;
}
done.
[cleanup] remove mytmp.dat
小さなプログラムのクリーンアップに最適
コマンドラインツールや短命プロセスでは、atexitで十分なクリーンアップが実現できます。
大規模で長時間稼働するサーバーでは、終了時に頼るよりもスコープごとの明示解放を基本とし、atexitは最終防衛線として位置づけましょう。
まとめ
atexitは、C言語で終了時のクリーンアップを自動化するためのシンプルかつ強力な仕組みです。
void func(void)の関数を登録するだけで、正常終了時にLIFO順で実行されます。
引数が渡せない制約は静的変数や一括管理関数で補い、異常終了では呼ばれない点に注意しましょう。
初心者の方は、リソース取得後すぐの登録・1関数1役割・短時間で終わる処理に限定するという原則を守るだけで、多くのクリーンアップ漏れを防げます。
まずは小さなプログラムで習慣化し、より堅牢なコードへとステップアップしてください。
