Visual Studioから実行すると読み込めるのに、同じexeをダブルクリックすると、「ファイルが見つからない」になります。ファイルは、確かに、そこにあるのに。
原因の定番は、相対パスが、実行ファイルの場所ではなく「作業フォルダ」を基準にしていることです。同じexeを、起動するフォルダだけ変えて3通り実行し、成功と失敗を実測しました。さらに、開けたかを確認しないで使うと、デバッグ版で何が起きるか(Debug Assertion Failed)も、実際に出たダイアログで見ていきます。
相対パスとは?
相対パスとは、
・「どこから見た道順か」を省略して書いたパス。C++のfopen("data/config.txt")のようなパスは、実行ファイルの場所ではなく、プログラムの「作業フォルダ(カレントディレクトリ)」から見た道順として解釈される
です。ここが、いちばん間違えやすい点です。「exeの隣にあるdataフォルダ」のつもりで書いても、実際に探されるのは、プログラムを起動したときの作業フォルダの下のdataフォルダです。
実験:同じexeを、3つの作業フォルダで動かす
次のようなフォルダ構成を作りました。exeとdata/config.txtは、同じbinフォルダの中にあります。
path
└─ bin
├─ open_rel.exe
└─ data
└─ config.txtプログラムは、data/config.txtを相対パスで開いて、成功か失敗かを表示するだけです。作業フォルダも一緒に表示します。
▼open_rel.cpp
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <filesystem>
int main() {
std::printf("作業フォルダ : %s\n", std::filesystem::current_path().string().c_str());
FILE* fp = std::fopen("data/config.txt", "rb"); // 相対パスで開く
if (!fp) {
std::printf("読み込み失敗 : %s (errno=%d)\n", std::strerror(errno), errno);
return 1;
}
char buf[64] = {};
size_t n = std::fread(buf, 1, sizeof(buf) - 1, fp);
std::fclose(fp);
std::printf("読み込み成功 : %zu バイト\n", n);
return 0;
}このexeを、作業フォルダを変えて起動しました。
[open_rel.exe]
作業フォルダ = exeのフォルダ
作業フォルダ : C:\blogtest\path\bin
読み込み成功 : 16 バイト
作業フォルダ = 1つ上のフォルダ
作業フォルダ : C:\blogtest\path
読み込み失敗 : No such file or directory (errno=2)
作業フォルダ = C:\ (ドライブ直下)
作業フォルダ : C:\
読み込み失敗 : No such file or directory (errno=2)| 起動したときの作業フォルダ | 結果 |
|---|---|
exeと同じフォルダ(bin) | 成功(16バイト読めた) |
1つ上のフォルダ(path) | 失敗(No such file or directory、errno=2) |
C:\(ドライブ直下) | 失敗(同じ) |
ファイルもexeも一切動かしていないのに、作業フォルダが違うだけで、読み込みが失敗しました。これが、「自分の環境では動く」の正体です。
作業フォルダは、起動のしかたで変わる
作業フォルダは、プログラムの中で決めるのではなく、起動した側が決めるものです。たとえば、次のように変わります。
- コマンドプロンプトから起動:
cdで移動した場所が作業フォルダになる(今回の実験は、これを使った) - ショートカットから起動:ショートカットのプロパティにある「作業フォルダー」が使われる
- Visual Studioのデバッグ実行:プロジェクトのプロパティの「デバッグ → 作業ディレクトリ」で決まる
Visual Studioの作業ディレクトリは、既定ではプロジェクトのフォルダになるのが一般的で、ビルドしたexeが入っているx64\Debugなどとは別の場所です。ソースの隣にassetフォルダを置いて開発していると、Visual Studioからは動き、x64\Debugのexeを直接起動すると動かない、ということが起きます。
【重要】罠:開けたかを確認しないと、デバッグ版でダイアログが出る
fopenは、開けないとNULLを返します。この結果を確認せずに、そのままfreadなどに渡すと、どうなるでしょうか。わざと試しました。
▼open_nocheck.cpp(開けたかを確認していない)
#include <cstdio>
int main() {
FILE* fp = std::fopen("data/config.txt", "rb"); // 開けなかったのに、確認しない
char buf[16] = {};
std::fread(buf, 1, sizeof(buf), fp); // NULL のまま読んでしまう
std::printf("終わり\n");
return 0;
}
Debug Assertion Failed!
Program: C:\blogtest\path\bin\open_nocheck.exe
File: minkernel\crts\ucrt\src\appcrt\stdio\fread.cpp
Line: 48
Expression: stream != nullptr
For information on how your program can cause an assertion
failure, see the Visual C++ documentation on asserts.
(Press Retry to debug the application)デバッグビルドでは、CRT(C言語の標準ライブラリ)が、stream != nullptrという式が成り立たないことを検出して、Debug Assertion Failed!のダイアログを出しました。原因は、「ファイルが無い」ことなのに、ダイアログには、fread.cppという内部の名前しか出ないので、原因が分かりにくくなります。
これは、私のDirectXのプロジェクトでも実際に起きました。シェーダーのファイル(.cso)を相対パスで読んでいたので、ビルドしたexeをx64\Debugから直接起動したところ、今回と同じ種類のDebug Assertion Failed!(fileno.cpp、Expression: stream.valid())が出て、起動できませんでした。「ファイルが無い」のに、「ストリームが不正」と言われるので、原因にたどり着くまでに時間がかかりました。
なお、リリースビルドでは、同じコードがアクセス違反などで落ちることがありますが、今回はリリースビルドでは試していません。
直し方
①開けたかを、必ず確認する
最初の実験のように、fopenがNULLでないかを確認して、失敗したら、どのパスを開こうとしたかを表示します。それだけで、原因の切り分けが、ずっと速くなります。
②実行ファイルの場所を基準にしてパスを作る
作業フォルダに頼らないよう、実行ファイルがあるフォルダを取得して、そこからパスを組み立てます。WindowsではGetModuleFileNameWで、実行ファイルのパスが取れます。
▼open_exe.cpp(実行ファイルの場所を基準にする)
#include <windows.h>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <filesystem>
// 実行ファイルがあるフォルダを返す(作業フォルダとは関係ない)
static std::filesystem::path ExeDir() {
wchar_t buf[MAX_PATH];
GetModuleFileNameW(nullptr, buf, MAX_PATH);
return std::filesystem::path(buf).parent_path();
}
int main() {
std::printf("作業フォルダ : %s\n", std::filesystem::current_path().string().c_str());
std::filesystem::path path = ExeDir() / "data" / "config.txt"; // 実行ファイルからの相対で組み立てる
FILE* fp = _wfopen(path.c_str(), L"rb");
if (!fp) {
std::printf("読み込み失敗 : %s (errno=%d)\n", std::strerror(errno), errno);
return 1;
}
char buf[64] = {};
size_t n = std::fread(buf, 1, sizeof(buf) - 1, fp);
std::fclose(fp);
std::printf("読み込み成功 : %zu バイト\n", n);
return 0;
}同じ3つの作業フォルダで、この版を動かしました。
[open_exe.exe]
作業フォルダ = exeのフォルダ
作業フォルダ : C:\blogtest\path\bin
読み込み成功 : 16 バイト
作業フォルダ = 1つ上のフォルダ
作業フォルダ : C:\blogtest\path
読み込み成功 : 16 バイト
作業フォルダ = C:\ (ドライブ直下)
作業フォルダ : C:\
読み込み成功 : 16 バイト作業フォルダが、どこであっても、成功しました。fopenではなく_wfopenを使っているのは、パスをワイド文字列のまま渡して、日本語などを含むフォルダでも扱えるようにするためです(今回の実験は、英数字のパスだけで確認しています)。
③開発中は、作業フォルダを決めておく
exeの場所を基準にするのが難しいときは、Visual Studioの「作業ディレクトリ」と、配布時のショートカットの「作業フォルダー」を、同じ場所にそろえる方法もあります。ただし、ユーザーが別の方法で起動する可能性があるので、配布するアプリでは、②が確実です。
ファイルが開けないときのよくある失敗例と対処法
①Visual Studioで動いたので、そのまま配布する
作業フォルダが、開発中の環境でたまたま合っていただけです。配布前に、exeを別のフォルダにコピーして、ダブルクリックで起動してみると、相対パスの問題が見つかります。
②失敗しても何も表示しない
NULLを確認して、どのパスを開こうとしたかを表示します。作業フォルダ(current_path)も一緒に表示すると、原因の特定が、ほぼ一瞬で済みます。
③アセットの置き場所を、作業フォルダに依存したまま増やす
テクスチャ、シェーダー、サウンドと、読み込むファイルが増えるほど、相対パスの前提が散らばります。読み込みの入口になる関数で、exeの場所を基準にしたパスに、一括で直すようにしておくと、あとが楽です。
注意点
- 相対パスは、実行ファイルの場所ではなく、作業フォルダが基準
- 作業フォルダは、起動のしかたで変わる(コマンドプロンプト、ショートカット、Visual Studio)
fopenは開けないとNULLを返す。確認せずに使うと、デバッグ版でDebug Assertion Failed!- 実行ファイルの場所は、
GetModuleFileNameWで取れる(Windowsの場合) - 失敗したときは、開こうとしたパスと作業フォルダを表示する
- 今回の検証は、Windows 11(日本語環境)、MSVC 19.44のデバッグビルド。英数字のパスだけで確認した
要点を整理
- 同じexeでも、作業フォルダを変えるだけで、読み込みが成功(exeの隣)から失敗(
errno=2)に変わった - 確認せずに使うと、デバッグ版で
Debug Assertion Failed!が出て、原因が分かりにくい GetModuleFileNameWで実行ファイルの場所を基準にしたら、3つの作業フォルダのすべてで成功した- 配布前に、exeを別のフォルダにコピーして起動してみる
「ファイルがあるのに開けない」は、ほとんどの場合、ファイルではなく、作業フォルダの問題でした。開けなかったときに、パスと作業フォルダを表示するだけで、原因はすぐに見えてきます。


