なるべく小さい実行ファイルで、ちょっとしたファイル操作を行いたい、 ただそれだけの目標でプログラムを書きました。 大きな目標があるわけではありませんが、ごくまれにこういうコードが欲しくなるかもしれません。
動作は Windows 10、Visual Studio 2015 で確認していますが、 新しいバージョンでも問題なく動作するかと思います。
ここで作成しているのは、ウィンドウを持たない「コンソールアプリケーション」であり、 プロジェクトのプロパティで「マルチバイト文字セットを使用する」を選択しています。
また、実行環境を選ばずに実行できるようにするには、 「C/C++」の「コード生成」で、 「ランタイムライブラリ」で「マルチスレッド(/MT)」を選択して、 ランタイムライブラリを exe に取り込んでおくと良さそうです。 もちろん実行ファイルのサイズが大きくなりますので、状況に応じて考えます。
テストは
最後にはちょっとだけ、ファイルデータを操作しています。
VS Community 2022 をインストールする
Microsoft の開発システム Visual Studio には、小規模な開発者であれば無償で利用できる Community 版が用意されています。
旧バージョンをアンインストールし、新しい VS 2022 をインストールする様子を記録しています。
なお、本サイトのご利用に際しては、必ずプライバシーポリシー(免責事項等)をご参照ください。
投稿 October 8, 2023
とりあえずはパスの管理を確認しておきます。
例えば exe と同じフォルダにあるファイルにアクセスするためには
ですのでまず最初に、exe のパスを取得し、どの
#include "windows.h" int main() { char path[MAX_PATH] = { 0 }; GetModuleFileName(NULL, path, MAX_PATH); char *dir_name = strrchr(path, '\\'); if (dir_name != NULL) { *dir_name = '\0'; }
プロジェクトは Windows コンソールアプリとして作成していますので、main 関数で始まっています。 今はここに全ての機能を実装します。
上記のコードを実行すると、結果として文字列 path に、exe ファイルがいるフォルダ名が設定されます。 GetModuleFileName 関数 を使用するために、windows.h ヘッダーをインクルードする必要があります。
標準 C ライブラリだけで実行したい場合は、fopen 関数でファイルを開き、fstat 関数で情報を取得する方法がありますが、自身のファイル名がわからないといけません。 getcwd 関数 でカレントディレクトリを取得できますが、「ワーキングフォルダ」を指定した場合にどうなるのかなど、今は試していないため不明です。
シンボル MAX_PATH は値 260 を持ち、WinDef.h で定義されています。 パスの長さがこれを超えることは、基本的にはできません。
GetModuleFileName Win32 API
で、自身のファイル名を含めたフルパスを取得します。
例えば
C:¥data¥simplefile¥simplefile.exe
を実行している場合は、この通りの文字列が返され、path にセットされます。
strrchr 関数
は、第 1 引数で指定した文字列から、第 2 引数で指定した文字を探し、最後に出現した位置を返してくれる関数です。
NULL 以外が返された場合は、指定の文字が見つかった、という意味で、
第 1 引数で指定した文字列内にあるポインタとして返されることになっています。
例えば C:¥data¥simplefile¥simplefile.exe
から
検索する文字がシングルクウォーテーションになっているにも関わらず、¥ が 2 つあるのに疑問がある場合は、 エスケープ シーケンス を参照してください。 ここでは ¥¥ と書いて、¥ サイン 1 文字を意味しています。
返されたポインタは path 内に出現する最後の ¥ を指していますので、
これを文字列の終端を意味する ¥0、つまり値 0 に書き換えることにより、ファイル名の部分を削除(無視されるように)しています。
path = "C:¥data¥simplefile0simplefile.exe0"
という形となり、最初の 0 の部分で文字列終わりと判断されることになります。
末尾の 0 は、文字列の終わりを示す、もともとあった 0 です。
これで path が、C:¥data¥simplefile のように、 ファイル名がなくなって、exe がいるディレクトリ名のみとなりました。
投稿 October 8, 2023
GetModuleFileName 関数 で path に設定された文字列を確認したり、 最終的に path にちゃんとディレクトリ名として保存されたかを確認するためのコードを用意します。
#if 1 { FILE *fpLog; errno_t e = fopen_s(&fpLog, "log.txt", "wt"); if (e == 0) { fwrite(path, strlen(path), 1, fpLog); fclose(fpLog); } } #endif
fopen_s 関数 で、log.txt をファイル名として、wt モード、つまり書き込みでテキストとして開きます。 失敗すると 0 以外の値が返されます。
昔ながらの fopen 関数を使おうとすると「安全性が低いため」コンパイル時にエラーになります。 無視して使うこともできますが、安全性が高まった、末尾に _s が付く関数を使用するように指示されます。
ファイルを開けた場合、 fwrite 関数 でテキストを書き込みます。
第 1 パラメータが書き込みデータを持つバッファ、
第 2 パラメータが書き込みサイズですので、
strlen 関数
で path のサイズを取得しています。
第 3 パラメータは、それをいくつ書き込むか、ですので 1 つだけで、
最終パラメータにファイルポインタを指定しています。
最後に fclose 関数 でファイルをクローズして完了です。
全体を #if 1 〜 #endif で囲っているのは、この確認用コードが不要になったらまるごと除外するのに #if 0 〜 #endif としたいためです。 もちろんコードを削除しても構わないのですが、あとでまた使いたいときに参照できるよう、しばらくは残しておきたいです。
さらにその内側で { 〜 } でブロックにしているのは、 fpLog や e といった変数を別の個所でも独自に定義できるように、です。
今のコンパイラなら、if (1){ 〜 } や if (0) { 〜 } でも、同じことになるかもしれません。
このコードを前セクションのコードの末尾に追加し、 ここまでを x86、Release でビルドして、できた exe ファイルをダブルクリックして実行すると、 同じフォルダに log.txt が作成され、exe のあるフォルダ名が記録されましたので、ここまで OK です。
なお、Visual Studio のデバッガから起動すると、プロジェクトのパスが作業フォルダになりますから、 exe と同じパスには log.txt が作成されません。 プロジェクトのパスで確認してください。
実行ファイルのショートカットを作成し、 「作業フォルダー」に別のフォルダを指定して実行すると、 log.txt ファイルが作成される場所が「作業フォルダー」で指定した場所になりますが、 書き込まれる実行ファイルのパスは、ちゃんと実行ファイルのあるフォルダになっています。
投稿 October 8, 2023
では、
SetCurrentDirectory(path); if (_mkdir("backup")) { return 0; }
_mkdir 関数 は引数で指定したディレクトリを作成してくれます。 成功した場合は 0 が返されます。
ディレクトリの作成に失敗した場合はこの先を実行しないように、 return 0 でプログラムを終わるようにしています。
実行すると、exe のあるフォルダに backup という名前でサブフォルダが作成されます。 作業フォルダーが別の場所であっても、ちゃんと exe のあるフォルダに作成されました。
では、backup サブフォルダがない状態から
_mkdir("backup¥¥20231005")
のようにすると、一気に複数階層作ることはできるのでしょうか?
できませんでした。 _mkdir 関数が失敗し、ENOENT などが返されているのでしょう。
いったん backup フォルダを作成し、それが成功した上で _mkdir("backup¥¥20231005") を呼び出せば、無事作成されました。
なお、backup サブフォルダがすでにあるのに backup サブフォルダを作成しようとすると、 希望的にはすでにあるので「成功扱い」にして欲しいのですが、実際には _mkdir 関数は 0 を返さず -1 を返してエラーとなりました。 このとき errno に EEXIST(値は 17 と定義されています)が設定されることになっているようですから、上記のコードの判定方法が良くありませんね。 backup サブフォルダがすでにあると、そこでコードが終了となってしまいます。
int r = _mkdir("backup"); if (r != 0 && errno != EEXIST) { return 0; } r = _mkdir("backup\\20231005"); if (r != 0 && errno != EEXIST) { return 0; }
こういう形になっていれば、backup サブフォルダがあってもなくても、また 20231005 サブフォルダがあってもなくても、 通ったあとには無事に作成されている、ということになりますね。
投稿 October 5, 2023
exe と同じフォルダに data.txt という名前のファイルを置いておき、サブフォルダにコピーします。
標準 C ライブラリにはファイルコピー関数はないようです。
すでに GetModuleFileName 関数や SetCurrentDirectory 関数を使用しているので CopyFile 関数 を使えばいいわけですが、ここでは C 関数のみで実行してみます。
FILE *fp; long size = 0; if (fopen_s(&fp, "data.txt", "rb") == 0) { fseek(fp, 0, SEEK_END); size = ftell(fp); fseek(fp, 0, SEEK_SET); char *buf = new char[size + 1]; fread(buf, size, 1, fp); fclose(fp);
読み込み部分です。
data.txt を rb モード、つまり読み込み、バイナリモードでオープンしています。 この場合はテキストモードでも同じかもしれませんが、シンプルなのはバイナリモードでしょう。
まずは fseek 関数 に SEEK_END を渡して、 ファイルポインタをファイルの末尾に移動させています。
そこで ftell 関数 を呼び出すと、現在のファイルポインタの位置、つまりは先頭からのバイト数、事前にファイルの末尾に移動させていますから、 まとめればファイルサイズが返される、という具合です。
fseek 関数 に SEEK_SET を渡して、 ファイルポインタをファイルの先頭に戻しておきます。
ファイル全体を読み込むのに必要なサイズがわかりましたので、 データをいったん保持するバッファ buf を、size + 1 バイトで用意します。 +1 バイトする必要はないと思いますが、安心のためにプラスしています。
fread 関数 で、buf に、size バイトのデータを 1 つ、ファイルから読み込んでいます。
読み込めたら fclose 関数を呼び出し、いったん完了です。
if (fopen_s(&fp, "backup\\20231005\\data.txt", "wb") == 0) { fwrite(buf, size, 1, fp); fclose(fp); } delete[] buf; }
書き込み部分です。
ファイル名は何でもいいのですが、とりあえずは作成したサブフォルダに同名で書き込もうとしています。
ファイルのオープンモードは、書き込みのバイナリモードです。
ファイルを開けた場合のみ、 fwrite 関数 で、buf にある size バイトのデータを 1 つ、ファイルに書き込んでいます。
fclose 関数でファイルをクローズして完了です。
delete[] で、確保したメモリを解放することを忘れてはいけません。
これで、exe と同じフォルダにある data.exe を、任意のサブフォルダにコピーすることができました。
投稿 October 10, 2023
では、Win32 API である GetModuleFileName 関数 を使わないで、 getcwd 関数 で代替できるのか、試してみます。
関数の説明としては、正式には _getcwd 関数 のように、アンダースコアで始まる関数名を使うように、と最初に書かれています。
動作としては、現在の作業ディレクトリの完全なパスを返してくれる、というものです。 現在の作業ディレクトリがルートの場合、文字列は円記号 ¥ で終わり、ルート以外の場は、円記号はなくディレクトリの名前で終わることになっています。
次のコードで試してみます。
char* buffer; if ((buffer = _getcwd(NULL, 0)) == NULL) { return 0; } else { FILE *fpLog; errno_t e = fopen_s(&fpLog, "log.txt", "wt"); if (e == 0) { fwrite(buffer, strlen(buffer), 1, fpLog); fclose(fpLog); } free(buffer); }
エクスプローラーから実行すると、説明通り、フルパスが返されました。 これは OK です、むしろ exe 名を外す手間が省けています。
しかし別の作業フォルダーを指定したショートカットから実行すると、 指定の作業フォルダーのパスが取得されましたので、実行ファイルのあるパスではありません。
つまりGetModuleFileName 関数を代替できていないということです。 getcwd、get current working directory ですね。
Bing で調べる限りでは、実行ファイルのあるパスを C 標準ライブラリで置き換えることはできなさそうです。
なお、_getcwd(NULL,0) が成功してパスを取得できた場合は、 内部でメモリが確保されたままになっていますので、free 関数で解放してあげる必要があります。
最初からつまずきましたから、windows.h のインクルードをもう回避できませんが、 一応、カレントディレクトリの設定、SetCurrentDirectory の代替はあるかも調べておきます。
素直に
chdir 関数
のようです。
何が「素直」かと言えば、昔の dos コマンドと同じだなぁ、と。実際に今もコマンドプロンプトで使えそうですが、今は cd と短縮されているようです。
これは問題なく置き換えできました。
あとちょっとのところが残念ですが、exe の場所を基準にする必要があるケースも限られていると思いますから、 何をしたいかによっては windows.h をインクルードしないで済ませることができるかもしれませんね。
投稿 October 11, 2023
せっかく一時的にファイルデータをメモリに読み込んでいますから、 コピー時にちょっとデータを操作すれば、簡易な暗号化ができるのではないでしょうか?
例えば PNG ファイルを開けなくしてみます。
PNG ファイルは PNG ファイルシグネチャと呼ばれる 8 バイトの固定データ、 89 50 4E 47 0D 0A 1A 0A で始まることとなっています。 ですので、最初の 89(16 進数です)を別の値に書き換えれば、PNG ファイルとして認識されず、開けなくなるはずです。
buf にデータを読み込んだあと、書き込みしている部分を少し変更します。 先に、buf に読み込むデータを data.png に変更したあと、書き込み前にデータを改変します。
buf[0] = ~buf[0]; if (fopen_s(&fp, "data.png", "wb") == 0) { fwrite(buf, size, 1, fp); fclose(fp); }
上記のようにすると、書き込み前にファイルの先頭の 1 バイトに対して XOR 値を書き込みますので、
1000 1001 = 0x89
0111 0110 = 0x76
になり、
ペイントで開こうとしても、 「このファイルは読み取れません。」 になり、ダメです。 Photoshop Elements でも開けませんし、Visual Studio のグラフィックデザイナーでも開けません。
この場合、XOR を取っていますので、もう 1 度書き換えを行うと、 もとの値に戻り、png として開けるようになります。
念のためですが、拡張子が png のままですし、それを識別する最初の 8 バイトのうちの先頭バイトだけを書き換えていますから、 バイナリエディタがあれば簡単に元に戻せてしまいます。 あくまで「簡易」です。
JPEG ファイルの場合は、 SOI マーカと呼ばれるデータ、FF D8 から始まっているようですから、 ファイル名だけ .png から .jpg に書き換えれば、同じように動作することでしょう。
試してみると、確かに開けなくなりました。
mp3 や mp4 ファイルはどうでしょうか。
音楽ファイル mp3 の場合は、うまくいきませんでした。
最初のバイトデータは確かに書き換えられていますが、 Windows 搭載の「メディアプレーヤー」では再生できてしまいました。 「重要性が高くないデータ部なので無視されている」と考えていいのでしょうか。
ちなみに動画ファイル mp4 の場合は、メディアプレーヤーでは再生できなくなりました。 もう 1 回通せば、XOR されて元に戻り、再生できるようになりました。
テキストファイルの場合は、単に先頭バイトを XOR しただけでは、まず読み取りに成功すると思われます。
テキストファイルの 1 文字目がマルチバイト文字の場合、つまり日本語のような 1 バイト文字ではない場合、 試したところでは、最初の文字を 1 バイト文字として認識し、以降の文字列もすべて崩れました。
しかし、最初の 1 文字が 1 バイト文字の場合、その文字だけが別の文字に置き換わるか文字化けするだけで、 以降の文字は 1 バイト文字でも 2 バイト文字でも関係なく、もとのまま読めました。
最初バイトデータ buf[0] を 0x00 にしても、0xFF にしても開けてしまいましたので、 (プレーンな)テキストデータを簡単に読めなくするのは難しそうです。 全データを XOR するなどの処理を行えば読めなくなると思いますが、試しません。 それぐらいだと解読が容易ですから。
目的にもよりますが、対象ファイルをドロップしたら(簡易な)暗号化がかかる、外れる、だと便利なのかもしれません。
古い Visual C++ プロジェクトから新しい環境に移行したとき、_sprintf でエラーが出る場合の回避方法について、書いています。
Visual C++ では、リストボックスの文字サイズをプロパティで変更することはできません。 ここでは自由な文字サイズに変更したり、別のフォントにしたりしています。
Windows PC で 2 台のモニタを接続している場合、2 台接続を検出し、座標を特定するためのコードについて書いています。
すでにインストールされている VS 2019 をアンインストールし、VS Community 2022 をインストールしなおしています。
本サイトの新着情報を紹介しています。
Windows 開発関連の情報を、書いています。
Android 開発関連の情報を、書いています。