ファイルを完全に消すつもりが、思わぬ環境差で削除に失敗することがあります。
C言語のremove()
はシンプルですが、WindowsとLinuxでは挙動が異なる点があり注意が必要です。
本記事では安全に削除するためのコツやエラー時の対処、パスや文字コードの扱いまで、初心者の方にもわかりやすく詳しく解説します。
remove()の基本
remove()でできること
C言語のremove()
は、指定したパスのエントリを削除する関数です。
標準Cではファイルまたは空のディレクトリの削除が対象ですが、実装やOSにより扱いが異なることがあります。
特にWindowsのCランタイムではディレクトリは削除不可です。
この差異は後述します。
削除は#include <stdio.h>
で利用でき、戻り値は0
が成功、0以外
が失敗です。
失敗時の理由はerrno
に設定されます。
remove()の最小コード例
最小限の使い方として、ファイルを作成し、閉じてから削除します。
初心者がつまずきやすいのはfclose()
を忘れて削除が失敗する点です。
Windowsでは特に影響が大きいです。
// 最小のremove()使用例: ファイルを作成してから削除する
// コンパイル例: gcc -o remove_min remove_min.c など
#include <stdio.h>
#include <errno.h>
int main(void) {
const char *path = "demo_remove_min.txt";
// 1) ファイルを作成して何か書く
FILE *fp = fopen(path, "w");
if (!fp) {
perror("fopen 失敗");
return 1;
}
fprintf(fp, "remove()の最小例です。\n");
// 2) 必ず閉じる(Windowsでは開いたままの削除に失敗しやすい)
fclose(fp);
// 3) 削除する
if (remove(path) == 0) {
printf("削除成功: %s\n", path);
} else {
perror("remove 失敗");
return 1;
}
return 0;
}
削除成功: demo_remove_min.txt
remove()の戻り値とerrno
戻り値
remove()
は成功で0、失敗で非0を返します。
失敗時の理由はerrnoで判定します。
errnoの扱い
代表的なerrno
は以下です。
OSやランタイムにより差がありますが、概ね次のように考えれば十分です。
ENOENT
: 指定パスが存在しないEACCES
またはEPERM
: 権限がない、またはWindowsでディレクトリをremove()
した、または他プロセスがロック中などENOTEMPTY
またはEEXIST
: ディレクトリが空でないENOTDIR
: パスの一部がディレクトリでない(壊れたパス)
エラー文字列はperror()
またはstrerror(errno)
で表示できます。
// errnoの表示例
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void) {
if (remove("no_such_file.txt") != 0) {
// perrorは自動でerrnoに応じた説明文を出力します
perror("remove 失敗");
// 詳細を自前で組み立てる場合
fprintf(stderr, "errno=%d (%s)\n", errno, strerror(errno));
}
return 0;
}
remove 失敗: No such file or directory
errno=2 (No such file or directory)
remove()のエラー例
初心者が遭遇しやすい2つのエラーを実演します。
// 1) 存在しないファイルの削除 -> ENOENT
// 2) "中身のある"ディレクトリの削除 -> 失敗(OSによりerrnoは異なる)
#include <stdio.h>
#include <errno.h>
#include <string.h>
#if defined(_WIN32)
#include <direct.h> // _mkdir
#define MKDIR(dir) _mkdir(dir)
#define SEP "\\"
#else
#include <sys/stat.h> // mkdir
#include <unistd.h>
#define MKDIR(dir) mkdir(dir, 0700)
#define SEP "/"
#endif
int main(void) {
// 1) 存在しないファイル
if (remove("this_file_does_not_exist.tmp") != 0) {
perror("remove 失敗(存在しないファイル)");
}
// 2) 中身のあるディレクトリを作って削除を試みる
const char *d = "dir_nonempty";
const char *child = "dir_nonempty" SEP "dummy.txt";
// ディレクトリ作成
if (MKDIR(d) != 0) {
perror("mkdir 失敗(既に存在?)");
}
// 中にファイルを作る
FILE *fp = fopen(child, "w");
if (fp) {
fputs("dummy", fp);
fclose(fp);
}
// 中身のあるディレクトリをremove -> 一般に失敗
if (remove(d) != 0) {
fprintf(stderr, "remove 失敗(ディレクトリ): errno=%d (%s)\n",
errno, strerror(errno));
}
return 0;
}
remove 失敗(存在しないファイル): No such file or directory
remove 失敗(ディレクトリ): errno=39 (Directory not empty)
LinuxではENOTEMPTY
が典型、WindowsではEACCES
になることもあります。
いずれにせよ中身があるディレクトリはremoveでは消せません。
WindowsとLinuxの違い
ディレクトリ削除の可否
最も大きな違いはディレクトリの扱いです。
- Linux(POSIX系):
remove()
はファイルに加え空ディレクトリも削除できます。非空は失敗します。 - Windows(MSVC CRT):
remove()
はファイルのみ削除できます。ディレクトリは_rmdir()
で削除します。
下表は概要です。
項目 | Linux | Windows |
---|---|---|
ファイル削除 | 可能(remove ) | 可能(remove ) |
空ディレクトリ削除 | 可能(remove またはrmdir ) | 不可(_rmdir を使用) |
非空ディレクトリ削除 | 不可 | 不可 |
開いているファイルの扱い
OS間で開いているファイルの削除可否が異なります。
- Linux: 別プロセスが開いていても、
remove()
は成功することが多いです。名前とディレクトリエントリだけが消え、内容は最後のハンドルが閉じられるまで保持されます。 - Windows: ほとんどの場合
remove()
は開いているファイルに対して失敗します(EACCES
など)。特別な共有フラグで開いていない限り削除できません。
したがって、必ずfclose()
してからremove()
を呼ぶのが、安全で移植性の高い手順です。
パス表記と文字コード
- 区切り: Linuxは
/
、Windowsは\
ですが、Windowsでも/
が多くの場合使えます。 - 絶対パス: Linuxは
/home/user/file
のように/
から開始。WindowsはC:\path\file
、UNCは\server\share\file
です。 - 文字コード: Linuxの
char
パスは実質的にUTF-8が一般的です。Windowsのchar
版remove
は現在のANSIコードページ(日本語OSならCP932)解釈であり、Unicodeの日本語パスで誤動作する恐れがあります。_wremoveでUTF-16パスを使うのが確実です。
安全に削除するチェックリスト
使い終わったらfcloseしてからremove
開いたハンドルは必ず閉じてから削除します。
Windowsでは未閉鎖のファイルは削除失敗の主因です。
// 正しい順序: fopen -> 書く/読む -> fclose -> remove
#include <stdio.h>
int main(void) {
const char *p = "close_then_remove.txt";
FILE *fp = fopen(p, "w");
if (!fp) { perror("fopen"); return 1; }
fputs("OK", fp);
fclose(fp); // ここが重要
if (remove(p) == 0) {
puts("削除成功");
} else {
perror("remove");
}
return 0;
}
削除成功
事前の存在確認は不要
存在確認のためにstat()
やfopen()
でチェックする必要はありません。
remove()
は対象が存在しなければENOENT
を返します。
事前確認をするとTOCTTOU(チェックと実行の間に状態が変わる競合)を招きます。
直接remove()
し、失敗時にerrno
で分岐するのが安全です。
#include <stdio.h>
#include <errno.h>
int main(void) {
if (remove("maybe_exists.tmp") != 0) {
if (errno == ENOENT) {
puts("すでに無いので問題なし");
} else {
perror("remove 失敗");
}
} else {
puts("削除成功");
}
return 0;
}
すでに無いので問題なし
絶対パスの確認で誤削除を防ぐ
意図しないカレントディレクトリで相対パスを削除すると危険です。
絶対パスかどうかを確認し、必要なら自前で基準ディレクトリを前置しましょう。
// 簡易: 絶対パス判定ヘルパー
#include <stdio.h>
#include <string.h>
static int is_absolute_path(const char *p) {
#ifdef _WIN32
if (!p || !p[0]) return 0;
// UNCパス: \\server\share...
if ((p[0] == '\\' && p[1] == '\\') || (p[0] == '/' && p[1] == '/')) return 1;
// ドライブ指定: C:\ または C:/
if (((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) &&
p[1] == ':' && (p[2] == '\\' || p[2] == '/')) return 1;
// 拡張パス \\?\
if (strncmp(p, "\\\\?\\", 4) == 0 || strncmp(p, "//?/", 4) == 0) return 1;
return 0;
#else
return p && p[0] == '/';
#endif
}
int main(void) {
const char *target = "logs/app.log"; // 相対パス例
if (!is_absolute_path(target)) {
fprintf(stderr, "警告: 相対パスです。削除は見直してください: %s\n", target);
// ここで安全な基準ディレクトリと連結する等の対策を行う
}
return 0;
}
警告: 相対パスです。削除は見直してください: logs/app.log
エラー時はerrnoで理由を表示
詳細なエラーログはトラブルシュートの要です。
perror()
に加えerrno
を数値でも出しましょう。
再現状況の共有が容易になります。
#include <stdio.h>
#include <errno.h>
#include <string.h>
static void log_remove_error(const char *path) {
fprintf(stderr, "remove失敗: path=\"%s\", errno=%d (%s)\n",
path, errno, strerror(errno));
}
int main(void) {
const char *p = "locked_or_missing.txt";
if (remove(p) != 0) {
log_remove_error(p);
}
return 0;
}
remove失敗: path="locked_or_missing.txt", errno=2 (No such file or directory)
ディレクトリは専用API(rmdir)を使う
ディレクトリはrmdir()
系を使うのが鉄則です。
WindowsとLinuxで関数名が異なるので分岐します。
なおrmdir
はいずれも空ディレクトリのみ削除できます。
// ディレクトリ削除の移植用ヘルパー
#include <stdio.h>
#include <errno.h>
#if defined(_WIN32)
#include <direct.h> // _rmdir
#define rmdir_portable _rmdir
#else
#include <unistd.h> // rmdir
#define rmdir_portable rmdir
#endif
int main(void) {
const char *d = "empty_dir";
#if defined(_WIN32)
_mkdir(d);
#else
mkdir(d, 0700);
#endif
if (rmdir_portable(d) == 0) {
puts("ディレクトリ削除成功");
} else {
perror("ディレクトリ削除失敗");
}
return 0;
}
ディレクトリ削除成功
Windowsの日本語パスは_wremoveを検討
Windowsで日本語を含むパスを確実に扱うには_wremove()
が安全です。
wchar_t*
(UTF-16)のパスを受け付けます。
// Windows限定: 日本語ファイル名を_wremoveで削除
#ifdef _WIN32
#include <wchar.h> // _wremove, wprintf
#include <stdio.h>
int main(void) {
const wchar_t *path = L"日本語ファイル.txt";
// まず作ってみる(ワイドAPIでなくてもCreateFileなどでもOKだが簡略化)
FILE *fp = _wfopen(path, L"w");
if (fp) { fputws(L"test", fp); fclose(fp); }
if (_wremove(path) == 0) {
wprintf(L"削除成功: %ls\n", path);
} else {
perror("_wremove 失敗");
}
return 0;
}
#endif
削除成功: 日本語ファイル.txt
MinGW系やランタイムの違いで挙動が異なる場合があります。
Unicode対応は一貫してワイド文字APIを使うと安全です。
よくある質問
remove()はごみ箱に入る?
入りません。
remove()
はOSのリンクを直接削除します。
Windowsでごみ箱(Recycler/Recycle Bin)へ送るにはShell
のAPIを使います。
以下は古典的なSHFileOperationW
(非推奨だが簡便)の例です。
// Windows限定: ごみ箱へ送る例(SHFileOperationW, 簡易・非推奨API)
// コンパイル時に: -lShell32 が必要な環境あり
#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
int move_to_recycle_bin(const wchar_t *path) {
// SHFileOperationは二重終端の複数文字列を要求
wchar_t from[MAX_PATH + 2] = {0};
wcsncpy(from, path, MAX_PATH);
from[wcslen(path) + 1] = L'// Windows限定: ごみ箱へ送る例(SHFileOperationW, 簡易・非推奨API)
// コンパイル時に: -lShell32 が必要な環境あり
#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
int move_to_recycle_bin(const wchar_t *path) {
// SHFileOperationは二重終端の複数文字列を要求
wchar_t from[MAX_PATH + 2] = {0};
wcsncpy(from, path, MAX_PATH);
from[wcslen(path) + 1] = L'\0';
SHFILEOPSTRUCTW op = {0};
op.wFunc = FO_DELETE;
op.pFrom = from;
op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT;
int ret = SHFileOperationW(&op);
return ret == 0 ? 0 : -1;
}
#endif
';
SHFILEOPSTRUCTW op = {0};
op.wFunc = FO_DELETE;
op.pFrom = from;
op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT;
int ret = SHFileOperationW(&op);
return ret == 0 ? 0 : -1;
}
#endif
近年はIFileOperation
やSHFileOperation
の代替が推奨されています。
Linuxデスクトップの「ゴミ箱」は仕様が多様で、C標準APIでは扱いません。
ワイルドカード(*.txt)は使える?
remove()自体はワイルドカードを展開しません。
シェルが展開するかどうかは環境依存です。
Linuxのbashなどでは*.txt
は実行前にファイル列へ展開されますが、Windowsのcmd.exeは基本的に展開しません。
Cプログラム内でパターンを処理したいなら、POSIXではglob()
が使えます。
// POSIX限定: glob()で*.txtを展開してremove
#ifndef _WIN32
#include <glob.h>
#include <stdio.h>
int main(void) {
glob_t g = {0};
if (glob("*.txt", 0, NULL, &g) == 0) {
for (size_t i = 0; i < g.gl_pathc; ++i) {
const char *p = g.gl_pathv[i];
if (remove(p) == 0) {
printf("削除: %s\n", p);
} else {
perror("remove 失敗");
}
}
globfree(&g);
}
return 0;
}
#endif
削除: a.txt
削除: b.txt
Windowsで同様のことをする場合はFindFirstFileW
とFindNextFileW
を使って列挙します。
相対パスの基準はどこ?
プロセスのカレントディレクトリが基準です。
デバッガやサービスで起動すると期待と異なることがあります。
getcwd()
で確認できます。
// 現在の作業ディレクトリを表示
#include <stdio.h>
#ifdef _WIN32
#include <direct.h>
#define getcwd_ _getcwd
#else
#include <unistd.h>
#define getcwd_ getcwd
#endif
int main(void) {
char buf[1024];
if (getcwd_(buf, sizeof(buf))) {
printf("CWD: %s\n", buf);
} else {
perror("getcwd 失敗");
}
return 0;
}
CWD: /home/user/project
読み取り専用属性だと削除できない?
- Windows: 読み取り専用属性が付いたファイルは
remove()
に失敗しやすいです(EACCES
)。_chmod
系で属性を外してから削除します。 - Linux: ファイルの読み取り専用フラグは削除可否に直接は影響せず、親ディレクトリの書き込み+実行権限が必要です。
Windowsで属性を外す例:
#ifdef _WIN32
#include <wchar.h>
#include <sys/stat.h> // _wchmod
#include <stdio.h>
int main(void) {
const wchar_t *p = L"readonly.txt";
FILE *fp = _wfopen(p, L"w");
if (fp) { fputws(L"test", fp); fclose(fp); }
// 読み取り専用にする
_wchmod(p, _S_IREAD);
// 書き込み可に戻す
_wchmod(p, _S_IWRITE);
// 削除
if (_wremove(p) == 0) wprintf(L"削除成功: %ls\n", p);
else perror("_wremove 失敗");
return 0;
}
#endif
削除成功: readonly.txt
拡張子の違いは関係ある?
関係ありません。
OSやremove()
は拡張子の意味を解釈しません。
.txt
でも.bin
でも、存在して権限があれば削除されます。
拡張子は主にアプリケーションの関連付けや表示上の慣習です。
まとめ
remove()はシンプルですが、WindowsとLinuxでの違い(ディレクトリ削除の可否、開いているファイルの扱い、文字コード)を理解しておくと実運用での事故を防げます。安全の要点は1) 必ずfclose()
してから削除、2) 事前確認より結果で判断、3) 絶対パスやCWDの把握、4) ディレクトリはrmdir
、5) Windows日本語パスは_wremove
です。
さらに、エラー時はerrno
を記録して原因を特定しましょう。
これらを押さえれば、移植性と安全性の高いファイル削除が実現できます。