UnicodeビルドのMFCでUTF-8テキストをCStdioFile::ReadStringすると、スマートクォートなどが「â」のように文字化けすることがあります。原因はReadStringそのものではなく「ファイルをどう開いたか」。UTF-8として正しく読み込む手順と、実務でハマりやすい落とし穴をまとめます。
起きている現象:UTF-8の一部文字だけが化ける
UTF-8で保存したテキストに、スマートクォート(“ ”)やダッシュ(– —)などのASCII外文字が含まれていると、Unicodeビルド(TCHAR == wchar_t)にもかかわらず、読み込んだCStringに次のような「見慣れた化け方」が出ることがあります。
| 元の文字(UTF-8のつもり) | CStdioFile::ReadStringでの見え方(例) | よくある意味 |
|---|---|---|
| “ | â | UTF-8のバイト列を1バイト文字として誤解釈したサイン |
| ” | â | 同上(UTF-8の多バイトが分解されている) |
| – | â | エンダッシュ(E2 80 93)が分解されていることが多い |
| — | â | エムダッシュ(E2 80 94)が分解されていることが多い |
コピー&ペーストでは正しく表示できる、エディタで開くと正しい、なのにReadStringだけが化ける──この状況は「ファイルの中身はUTF-8だが、読み込み時のデコードがUTF-8になっていない」ケースがほとんどです。
原因:CStdioFileはUTF-8を自動判別せず、開いた時の変換設定に従う
Unicodeビルドだからといって、CStdioFileがUTF-8を自動で解釈してくれるわけではありません。ポイントは次の1点です。
- CStdioFile::ReadStringは「内部で使っているFILE*(Cランタイムのストリーム)」がどの文字コード変換モードで開かれているかに依存する
つまり、UTF-8ファイルを「ANSI(システムの既定コードページ)」として開いてしまうと、UTF-8の多バイト列(例:E2 80 9C)が1バイトずつ別文字に分解され、結果としてâのようなゴミ文字列になります。
よくある読み込みコード(化けやすい)
MFCの典型的なパターンは、次のようにCStdioFile::Openで開く方法です。
CStdioFile file;
if (file.Open(L"C:\\MyFile.txt", CFile::modeRead | CFile::typeText))
{
CString line;
while (file.ReadString(line))
{
// UTF-8の“ ”などが â â に化けることがある
}
file.Close();
}
この方法は「改行を扱えるテキスト読み込み」には便利ですが、UTF-8デコードまで面倒を見てくれるわけではないため、UTF-8ファイルにはそのままでは不向きです。
解決策:ccs=UTF-8で開いたFILE*をCStdioFileに渡す
最も手堅く、かつ既存のCStdioFile::ReadStringを活かせる対処は、CRT(Cランタイム)側でUTF-8→ワイド文字(UTF-16)変換が行われるモードで開くことです。MSVCのfopen/_wfopen系には、モード文字列にccs=を付けてテキスト変換を指定できる拡張があります。
手順(推奨)
_wfopen_s(またはfopen_s)で、モードにccs=UTF-8を付けて開く- 取得した
FILE*をCStdioFileのコンストラクタへ渡す - あとは普段通り
ReadStringで1行ずつ読む
動くコード例(Unicodeビルド想定)
FILE* fp = nullptr;
// 重要:ccs=UTF-8 を付けて「UTF-8として」テキストオープンする
// ※スペース有無が気になる場合は、スペース無しで書くと無難
errno_t e = _wfopen_s(&fp, L"C:\MyFile.txt", L"rt,ccs=UTF-8");
if (e == 0 && fp)
{
// CStdioFileに渡したら「CStdioFileが所有する」前提で扱うのが安全
CStdioFile file(fp);
fp = nullptr; // 以降、この変数をfcloseしない(二重クローズ防止)
CString line;
while (file.ReadString(line))
{
// line は正しくUnicode文字として読める(“ ” も保持される)
}
// 明示Closeしておくと事故が減る
file.Close();
}
これで、UTF-8のスマートクォートや記号類がCStringに正しく入り、以後のMFC描画やログ出力でも「本来のUnicode文字」として扱えます。
モード文字列の意味を整理
| 指定 | 意味 | 注意点 |
|---|---|---|
rt | テキストとして読み込み(改行変換あり) | ccs=はテキストモードで使うのが前提 |
rb | バイナリとして読み込み(改行変換なし) | バイナリで開くと、CRTのテキスト変換が効かず、自前変換が必要になりやすい |
ccs=UTF-8 | UTF-8として読み、ワイドI/Oと組み合わせてUTF-16へ変換 | MSVCの拡張。標準Cの移植性を優先する場合は別案も検討 |
ccs=UNICODE | UTF-16(LE/BEはBOMに依存)として扱う | UTF-8ファイルに対して使うと崩れる |
「CFile::typeUnicode」で崩れる理由:UTF-8ではなくUTF-16前提になりやすい
「UnicodeならCFile::typeUnicodeを付ければ良いのでは?」と考えがちですが、MFCのtypeUnicodeは文脈によってUTF-16(BOM付き等)を想定していることが多く、UTF-8に対しては期待通りに動かないことがあります。結果として「8bit ASCII相当にされる」「さらに崩れる」といった症状につながります。
| 指定の意図 | 想定されるファイル | UTF-8ファイルに対する結果 |
|---|---|---|
CFile::typeText | ANSI/ロケール依存のテキスト | UTF-8がACP扱いになり、スマートクォート等が文字化けしやすい |
CFile::typeUnicode | UTF-16(BOM付き等) | UTF-8をUTF-16として扱い、読み込みが破綻しやすい |
CRTのccs=UTF-8 | UTF-8 | UTF-8→UTF-16変換され、UnicodeビルドのCStringと整合する |
まず確認:ファイルは本当にUTF-8か(スマートクォートのバイト列)
原因切り分けで一番効くのが「ファイルの中身をバイト列で確認する」ことです。UTF-8なら、代表的な文字は次のようなバイト列になります。
| 文字 | Unicodeコードポイント | UTF-8バイト列(16進) |
|---|---|---|
| “ | U+201C | E2 80 9C |
| ” | U+201D | E2 80 9D |
| – | U+2013 | E2 80 93 |
| — | U+2014 | E2 80 94 |
PowerShellでの簡易チェック例
WindowsならPowerShellで先頭付近のバイトを確認できます(UTF-8 BOMの有無も分かります)。
# 先頭64バイトを16進で表示(BOMがあるなら EF BB BF が先頭に見える)
Format-Hex -Path "C:\MyFile.txt" -Count 64
もしファイル先頭がFF FEやFE FFならUTF-16(LE/BE)の可能性が高く、逆にUTF-8のつもりで作ったファイルが別エンコードで出力されている、という根本原因も疑えます。
クローズ周りでハマらないための実装ポイント
FILE*からCStdioFileを構築する方法は便利な反面、「どちらがクローズするか」を曖昧にすると二重クローズやリークの原因になります。安全側に倒すコツは次の通りです。
CStdioFileに渡したら、そのFILE*は以後触らない(fcloseしない)- 処理の最後に
file.Close()を明示して終了させる - 例外経路を考えるなら、MFC例外(
CFileExceptionなど)を捕まえて確実にCloseする
二重クローズを避ける小技
FILE* fp = nullptr;
if (_wfopen_s(&fp, path, L"rt,ccs=UTF-8") == 0 && fp)
{
CStdioFile file(fp);
fp = nullptr; // ここで所有権を「手放した」ことを明確にする
try
{
CString line;
while (file.ReadString(line))
{
// ...
}
file.Close();
}
catch (...)
{
// 例外時もCloseを試みる(Close自体が例外を投げる可能性もあるので注意)
try { file.Close(); } catch (...) {}
throw;
}
}
「デストラクタ任せ」でも動くことは多いですが、チーム開発や長期運用では「いつ閉じたか」をコード上で明確にしておくと、後からの保守で効いてきます。
それでもccs=UTF-8を使えない場合の代替案(自前でUTF-8→UTF-16変換)
ccs=UTF-8はMSVCの拡張なので、環境差・ランタイム差を嫌う場合や、より細かく制御したい場合は「バイナリで読み込み→自前でUTF-8デコード」が堅実です。WindowsならMultiByteToWideCharが最短ルートになります。
バイナリ読み込み+MultiByteToWideCharの例
#include <vector>
#include <windows.h>
CString ReadAllTextUtf8ToCString(const wchar_t* path)
{
// 1) バイナリで全読み込み(改行処理は後段で行う)
CFile file;
if (!file.Open(path, CFile::modeRead | CFile::typeBinary))
return L"";
const ULONGLONG size = file.GetLength();
std::vector<BYTE> buf(static_cast<size_t>(size));
if (size > 0)
file.Read(buf.data(), static_cast<UINT>(size));
file.Close();
// 2) UTF-8 BOMが付いていればスキップ(EF BB BF)
size_t offset = 0;
if (buf.size() >= 3 && buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF)
offset = 3;
// 3) UTF-8 → UTF-16
const char* src = reinterpret_cast<const char*>(buf.data() + offset);
const int srcLen = static_cast<int>(buf.size() - offset);
const int wlen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, srcLen, nullptr, 0);
if (wlen <= 0)
return L"";
std::vector<wchar_t> wbuf(static_cast<size_t>(wlen) + 1);
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, srcLen, wbuf.data(), wlen);
wbuf[wlen] = L'\0';
return CString(wbuf.data());
}
この方法は「行単位の読み込み」ではなく「全体読み込み」ですが、UTF-8としての解釈が完全に自分の管理下に置けるため、原因不明の文字化けに悩まされにくくなります。大量ファイルやストリーム処理では、チャンク読み+デコードの実装に発展させるのも現実的です。
実務でありがちな勘違い・落とし穴
| 落とし穴 | なぜ起きる | 回避策 |
|---|---|---|
| UnicodeビルドならUTF-8も読めると思い込む | Unicodeビルドは「アプリ内部表現がUTF-16になる」だけで、ファイルI/Oのデコードは別問題 | UTF-8で開く(ccs=UTF-8)か、明示的にUTF-8デコードする |
CFile::typeUnicodeをUTF-8用だと誤解する | MFCの文脈ではUTF-16(BOM)前提の挙動になることが多い | UTF-8はccs=UTF-8、UTF-16はtypeUnicodeのように用途を分ける |
| ソースの/utf-8や文字セット設定で直ると思う | それは「ソースコードの解釈」や「リテラルの扱い」で、実行時のファイルデコードとは無関係 | 実行時にUTF-8として読む手順を用意する |
| Closeの責任が曖昧でリーク/二重クローズ | FILE*とCStdioFileの所有権が混ざる | CStdioFileに渡したらFILE*は触らない。明示file.Close() |
まとめ:ReadStringを捨てる必要はない。UTF-8として「開く」のが核心
CStdioFile::ReadString自体が「UTF-8非対応」というより、UTF-8として開かれていないストリームを読んでいることが文字化けの正体です。MSVC環境のMFCアプリなら、_wfopen_s+ccs=UTF-8でFILE*を作ってCStdioFileに渡すのが最短で、既存実装への影響も小さく済みます。
それでも環境差が心配なら、バイナリ読み込み+MultiByteToWideChar(CP_UTF8)の自前デコードに切り替えると、文字化け問題を根本から制御できます。どちらの方式でも、「ファイルの実体がUTF-8であるか」をバイト列で確認しておくと、調査の手戻りが激減します。

コメント