こんにちは
今日は、Windows API の CreateFile 関数を使用してファイルを操作する際に遭遇する可能性のある一般的な問題についての話をします。
具体的には、"const char *" や "LPCSTR"と"LPCWSTR"という2つの異なるデータ型の間で発生する型の不整合について解説します。
対象読者は、文字コードについて詳しくない人や、C/C++/Windows プログラミング初学者向けです。
コードの誤りとエラーメッセージ
まず始めに以下のコードを見てみましょう
これは、私が最初に書いたコードで、ファイルパスをユーザーに入力してもらい、そのファイルパスのファイルの時間の情報を取得するサンプルプログラムです。
#include <windows.h>
#include <iostream>
// CreateFile 関数で特定のファイルを指定し、ファイルの操作(今回の場合ファイルの時間の情報を得る)を行う
void DisplayFileTimes(const char* path) {
HANDLE hFile;
FILETIME ftCreate, ftAccess, ftWrite;
SYSTEMTIME stUTC, stLocal;
// ファイルを開く
hFile = CreateFile(path,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL);
// 以下略
// ...
// ユーザーにファイルパスを入力してもらう
int main() {
char path[256];
std::cout << "Enter the file path: ";
std::cin.getline(path, 256);
DisplayFileTimes(path);
return 0;
}
上記のソースコードをビルドすると、以下のエラーが発生します。
このエラーは、CreateFile の行で発生しています。
"argument of type "const char *" is incompatible with parameter of type "LPCWSTR""
エラーの原因
エラーメッセージが示しているように、問題は関数 CreateFile の第一引数で、この関数は実際には CreateFileW を呼び出しています。
CreateFileW は第一引数のファイルパスを LPCWSTR 型(つまり、ワイド文字列)として期待しています。
しかし、上記のコードでは、ANSI 文字列("const char *"型)を引数として渡しています。これが型の不整合を引き起こし、結果としてビルドエラーが発生します。
解決策
この問題を解決するための最も簡単な方法は、CreateFileA 関数を明示的に呼び出すことです。
これにより、関数は ANSI 文字列を期待するようになります。
しかし、より一般的な、Unicode 文字列に対応した解決策としては、CreateFileW を呼び出し、ファイルパスを Unicode 文字列として渡すことです。これにより、国際的な文字セットをサポートすることが可能になります。
以下に、これを実装する方法を示します。
#include <windows.h>
#include <iostream>
void DisplayFileTimes(LPCWSTR path) {
HANDLE hFile;
FILETIME ftCreate, ftAccess, ftWrite;
SYSTEMTIME stUTC, stLocal;
// Open the file
hFile = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("Could not open file (%d)\n", GetLastError());
return;
}
// 以下略
// ...
// 文字列を wchar_t 型で取得し、すべて
int main() {
wchar_t path[256];
std::wcout << L"Enter the file path: ";
std::wcin.getline(path, 256);
DisplayFileTimes(path);
return 0;
}
詳細については、以下の通り補足説明をします。
補足説明:ANSI と Unicode の違い
ANSI と Unicode は、文字を表現するための2つの異なる仕組みです。
ANSI は主に西欧の文字を表現するためのもので、1文字を1バイト(つまり8ビット)で表現します。
一方、Unicode は世界中の文字を表現することができ、1文字を2バイト(つまり16ビット)で表現します。
Windows API は、ANSI 文字列と Unicode 文字列の両方をサポートしています。
関数名の末尾に "A" がついている関数は ANSI 文字列を使用し、"W" がついている関数は Unicode 文字列を使用します。
- "A": ANSI版の関数を表します。ANSI (American National Standards Institute) 文字セットは、主に英数字と基本的な記号を表現するためのもので、1文字を1バイトで表現します。Windows APIの関数で"A"が末尾に付くと、その関数はANSI文字列を引数に取ることを意味します。
- "W": Unicode版の関数を表します。Unicodeは、世界中のほとんどすべての文字を表現することができる文字セットで、1文字を2バイト(ワイド文字)で表現します。Windows APIの関数で"W"が末尾に付くと、その関数はUnicode文字列を引数に取ることを意味します。
例えば、CreateFileAはANSI文字列のファイル名を引数に取り、CreateFileWはUnicode文字列のファイル名を引数に取ります。どちらの関数も同じ機能(ファイルを作成または開く)を提供しますが、引数として受け付ける文字列の形式が異なります。
これらの関数は、Windowsの国際化(ローカライゼーション)を支える重要な要素で、開発者が自身のアプリケーションを多言語対応させる際に役立ちます。
しかし、多くの場合、関数名の末尾には何もついていない関数(例えば CreateFile)があり、これらの関数はプロジェクト設定に基づいて "A" または "W" の関数を呼び出します。
Visual Studio では、既定で Unicode 文字セットが使用されているため、今回の場合では CreateFileW API が暗黙的に利用されます。
その結果、path の変数 を char* 型で渡してしまうと、今回のようなエラーが発生してしまいます。
詳細は、以下の Microsoft 公式サイト内の「Unicode 関数と ANSI 関数」内に記載があるので、このページを一度読むことをお勧めします。
補足説明:Visual Studio 上のプロジェクトの文字セットの設定について
Visual Studio で"CreateFile"のような Windows API 関数がANSI版("A"が末尾)かUnicode版("W"が末尾)かどうかを決定する設定は、プロジェクトのプロパティで指定します。
具体的な手順は以下の通りです:
- Visual Studioでプロジェクトを開きます。
- ソリューションエクスプローラーでプロジェクト名を右クリックし、ドロップダウンメニューから"プロパティ"を選択します。
- プロパティページの左側にあるツリーから"構成プロパティ"を展開し、"詳細"を選択します。
- "詳細"の設定の中に"文字セット"という項目があります。これが、プロジェクトで使用する文字セット(ANSI か Unicode)を指定する設定です。
- "文字セット"のドロップダウンメニューから、以下のいずれかを選択します:
"マルチバイト文字セットを使用する":これを選択すると、Windows API 関数は ANSI版("A"が末尾)を呼び出します。
"Unicode文字セットを使用する":これを選択すると、Windows API 関数は Unicode版("W"が末尾)を呼び出します。 - 設定が完了したら、右下の"OK"ボタンをクリックしてプロパティページを閉じます。これで、指定した文字セットに基づいて Windows API 関数が呼び出されるようになります。
補足説明: Windows API での文字列の型についての補足
以下の図は、後述の参考資料の stackoverflow の記事にあった図ですが、
「Windows API での文字列の型の早見表」としてわかりやすかったため、引用して記載します。
Item | 8-bit(ANSI) | 16-bit (Wide) | Varies |
---|---|---|---|
character | CHAR | WCHAR | TCHAR |
string | LPSTR | LPWSTR | LPTSTR |
string (const) | LPCSTR | LPCWSTR | LPCTSTR |
結論
Windows API を使用してプログラムを書く際は、ANSI と Unicode の違いに注意することが重要です。
特に、CreateFile のような関数は、文字列を引数として取るため、適切な文字列型を使用することが不可欠です。
この記事が、今回のような文字列の違いに関する問題の理解と解決の一助となれば幸いです。
もし、今回の記事で間違っている点や、修正すべき点等がありましたら、有識者の方はコメントで教えていただけますと幸いです。
それでは、Happy Coding!