閉じる

【C言語】WindowsとLinuxで違う?remove()で安全にファイル・フォルダを削除

ファイルを完全に消すつもりが、思わぬ環境差で削除に失敗することがあります。

C言語のremove()はシンプルですが、WindowsとLinuxでは挙動が異なる点があり注意が必要です。

本記事では安全に削除するためのコツエラー時の対処パスや文字コードの扱いまで、初心者の方にもわかりやすく詳しく解説します。

remove()の基本

remove()でできること

C言語のremove()は、指定したパスのエントリを削除する関数です。

標準Cではファイルまたは空のディレクトリの削除が対象ですが、実装やOSにより扱いが異なることがあります。

特にWindowsのCランタイムではディレクトリは削除不可です。

この差異は後述します。

削除は#include <stdio.h>で利用でき、戻り値は0が成功、0以外が失敗です。

失敗時の理由はerrnoに設定されます。

remove()の最小コード例

最小限の使い方として、ファイルを作成し、閉じてから削除します。

初心者がつまずきやすいのはfclose()を忘れて削除が失敗する点です。

Windowsでは特に影響が大きいです。

C言語
// 最小の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)で表示できます。

C言語
// 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つのエラーを実演します。

C言語
// 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()で削除します。

下表は概要です。

項目LinuxWindows
ファイル削除可能(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のcharremove現在のANSIコードページ(日本語OSならCP932)解釈であり、Unicodeの日本語パスで誤動作する恐れがあります。_wremoveでUTF-16パスを使うのが確実です。

安全に削除するチェックリスト

使い終わったらfcloseしてからremove

開いたハンドルは必ず閉じてから削除します。

Windowsでは未閉鎖のファイルは削除失敗の主因です。

C言語
// 正しい順序: 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で分岐するのが安全です。

C言語
#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;
}
実行結果
すでに無いので問題なし

絶対パスの確認で誤削除を防ぐ

意図しないカレントディレクトリで相対パスを削除すると危険です。

絶対パスかどうかを確認し、必要なら自前で基準ディレクトリを前置しましょう。

C言語
// 簡易: 絶対パス判定ヘルパー
#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を数値でも出しましょう。

再現状況の共有が容易になります。

C言語
#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はいずれも空ディレクトリのみ削除できます。

C言語
// ディレクトリ削除の移植用ヘルパー
#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)のパスを受け付けます。

C言語
// 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(非推奨だが簡便)の例です。

C言語
// 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
注意

近年はIFileOperationSHFileOperationの代替が推奨されています。

Linuxデスクトップの「ゴミ箱」は仕様が多様で、C標準APIでは扱いません。

ワイルドカード(*.txt)は使える?

remove()自体はワイルドカードを展開しません

シェルが展開するかどうかは環境依存です。

Linuxのbashなどでは*.txtは実行前にファイル列へ展開されますが、Windowsのcmd.exeは基本的に展開しません。

Cプログラム内でパターンを処理したいなら、POSIXではglob()が使えます。

C言語
// 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で同様のことをする場合はFindFirstFileWFindNextFileWを使って列挙します。

相対パスの基準はどこ?

プロセスのカレントディレクトリが基準です。

デバッガやサービスで起動すると期待と異なることがあります。

getcwd()で確認できます。

C言語
// 現在の作業ディレクトリを表示
#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で属性を外す例:

C言語
#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を記録して原因を特定しましょう。

これらを押さえれば、移植性と安全性の高いファイル削除が実現できます。

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

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

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

URLをコピーしました!