MFCのCStdioFile::ReadStringでUTF-8が文字化けする原因と対策(Unicodeビルド対応)

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=を付けてテキスト変換を指定できる拡張があります。

手順(推奨)

  1. _wfopen_s(またはfopen_s)で、モードにccs=UTF-8を付けて開く
  2. 取得したFILE*CStdioFileのコンストラクタへ渡す
  3. あとは普段通り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-8UTF-8として読み、ワイドI/Oと組み合わせてUTF-16へ変換MSVCの拡張。標準Cの移植性を優先する場合は別案も検討
ccs=UNICODEUTF-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::typeTextANSI/ロケール依存のテキストUTF-8がACP扱いになり、スマートクォート等が文字化けしやすい
CFile::typeUnicodeUTF-16(BOM付き等)UTF-8をUTF-16として扱い、読み込みが破綻しやすい
CRTのccs=UTF-8UTF-8UTF-8→UTF-16変換され、UnicodeビルドのCStringと整合する

まず確認:ファイルは本当にUTF-8か(スマートクォートのバイト列)

原因切り分けで一番効くのが「ファイルの中身をバイト列で確認する」ことです。UTF-8なら、代表的な文字は次のようなバイト列になります。

文字UnicodeコードポイントUTF-8バイト列(16進)
U+201CE2 80 9C
U+201DE2 80 9D
U+2013E2 80 93
U+2014E2 80 94

PowerShellでの簡易チェック例

WindowsならPowerShellで先頭付近のバイトを確認できます(UTF-8 BOMの有無も分かります)。

# 先頭64バイトを16進で表示(BOMがあるなら EF BB BF が先頭に見える)
Format-Hex -Path "C:\MyFile.txt" -Count 64

もしファイル先頭がFF FEFE 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_sccs=UTF-8FILE*を作ってCStdioFileに渡すのが最短で、既存実装への影響も小さく済みます。

それでも環境差が心配なら、バイナリ読み込み+MultiByteToWideChar(CP_UTF8)の自前デコードに切り替えると、文字化け問題を根本から制御できます。どちらの方式でも、「ファイルの実体がUTF-8であるか」をバイト列で確認しておくと、調査の手戻りが激減します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次