お世話になっています。
テキストモードでのファイル読み込みの挙動について、質問がございます。
テキストモードでオープンしたファイルをReadすると、なぜかファイルサイズよりも余分
にバッファが確保されてしまいます。
ある程度のサイズ(私の環境では約1MB)を超えるファイルだと現象が出ます。
また、改行を含まないファイルでは現象が出ないことを確認しています。
なぜ、このような現象が出るのか、教えていただけないでしょうか?
OS:Windows10 64bit
開発環境:VisualStudio2010(コンソールアプリケーション。また、プロジェクトの文字セ
ットを「マルチバイト文字セットを使用する」としています)
// Test.cpp : コンソール アプリケーションのエントリ ポイントを定義します。
//
#include stdafx.h
#include <tchar.h>
#include <afx.h>
#include <afxwin.h>
int _tmain(int argc, _TCHAR* argv[])
{
CStdioFile fileWrite;
CStdioFile fileRead;
CString strTmpData, strData;
TCHAR* szBuf(NULL);
TCHAR* szTmpBuf(NULL);
int n(0), m(0), nIndex(0);
ULONGLONG nSize(0), nReadSize(0), nBufSize(0);
const long ROWS(1050);// サイズがこれ以上だと現象が出ます。
const long LENGTH(1000);
//const long ROWS(1049);// サイズがこれ以下では現象は出ません。
//const long LENGTH(1000);
//CString strToken(_T(\r\n));// 区切りが改行の場合、現象が出ます。
CString strToken(_T(\t\t));// 区切りが改行以外の場合、現象が出ません。
TCHAR* szFile = _T(Test.txt);
// 書き込み
if(fileWrite.Open(szFile, CFile::typeBinary | CFile::modeCreate |
CFile::modeWrite))
{
for(m = 0; m < ROWS; m++)
{
strData.Empty();
for(n = 0; n < LENGTH; n++)
{
strTmpData.Format(_T(%d), n % 10);
strData += strTmpData;
}
strData += strToken;
fileWrite.WriteString(strData.GetBuffer(0));
}
fileWrite.Close();
}
// 読み込み
if(fileRead.Open(szFile, CFile::modeRead))// テキストモード
{
nSize = fileRead.GetLength();
szBuf = (TCHAR*)malloc((size_t)(nSize + 1));
ZeroMemory(szBuf, (size_t)(nSize + 1));
nReadSize = fileRead.Read(szBuf, (UINT)nSize);//
nReadSize:1051050
nBufSize = strnlen(szBuf, ULONG_MAX);// nBufSize:1051054(なぜ
か、nReadSizeと相違)
szTmpBuf = strtok(szBuf, strToken.GetBuffer(0));
while(szTmpBuf != NULL)
{
if(nIndex == ROWS)
AfxMessageBox(余分なデータ);// なぜかここに入
ります。
szTmpBuf = strtok(NULL, strToken.GetBuffer(0));
nIndex++;
}
free(szBuf);
fileRead.Close();
}
return 0;
}
上記のプログラムは、改行を含むファイルをテキストモードでオープンし、Readした場
合、データの末尾に余計なデータが入り込んでしまっていることを示すものです。
また、下記については確認しています。
・C言語(read関数, fread関数)を用いても同様の現象が出る
・バイナリモードでオープンすると、正しいサイズを取得できる
よろしくお願いいたします。
提示したプログラムを一点、訂正いたします。
//CString strToken(_T(\r\n));// 区切りが改行の場合、現象が出ます。
CString strToken(_T(\t\t));// 区切りが改行以外の場合、現象が出ません。
↓↓↓
CString strToken(_T(\r\n));// 区切りが改行の場合、現象が出ます。
//CString strToken(_T(\t\t));// 区切りが改行以外の場合、現象が出ません。
提示したそのままのソースでは現象が出ず、改行のコメントアウトを復活させると現象が出ま
す。(いろいろテストした結果、変更箇所がそのままになっていました。失礼いたしました。)
また、補足ですが、プロジェクトの設定は「共有 DLL で MFC を使う」としています。
>ある程度のサイズ(私の環境では約1MB)を超えるファイルだと現象が出ます。
サイズは関係ないはずなんですけどねぇ…。
バッファに収まるかどうかで挙動変わるんでしょうか?
バイナリモードで書き込んで、テキストモードで読み出す。
という処理をしているので、改行コードの変換部分でサイズの変化があるのでしょう。
# テキストモードで書き込んで、バイナリモードで読み出す。でも似たような問題が発生
するでしょうけど。
CStdioFile::Read()ではなく、
CStdioFile::ReadString()で読み込むべきかと思います。
# CStdioFile::Read()は継承元のCFile::Read()になるでしょうけど。
瀬戸っぷさん
返信いただきありがとうございます。
試しに、テキストモードで書き込みテキストモードで読み出すよう変更しましたが、同様の現象
が出ました。
(下記の部位について、変更を行いました。)
if(fileWrite.Open(szFile, CFile::typeBinary | CFile::modeCreate |
CFile::modeWrite))
↓↓↓
if(fileWrite.Open(szFile, CFile::typeText | CFile::modeCreate |
CFile::modeWrite))
CStdioFile::ReadString() は速度が遅いため使用を躊躇しています。
バイナリモードでWrite、Readともに行うことで、一応の現象の回避はできていますが、根本的
な原因の解明にはなっていないので、引き続きよろしくお願いいたします。
Windows7 Pro 64Bit + VS2017で確認。
>nSize = fileRead.GetLength();
>nReadSize = fileRead.Read(szBuf, (UINT)nSize);
で nSize > nReadSizeとなっていました。
テキストモードで開いていた為、0Dh 0Ahを0Ahに変換しているようです。
# 1050行で、nReadSizeが1050バイト分ほど少ない。
で、書き込み時のバイナリモードをテキストモードにして書き出しを行い、出力された
ファイルをバイナリエディタで覗くと…
\r\n(0Dh 0Ah)が\r\r\n(0Dh 0Dh 0Ah)に変換されます。
内部的には改行は\nで扱うべき…となります。
# CStdioFile::Read()でも変換は生きるんですねぇ……。
サイズで動作に差が出る原因は不明…なままです。
バイナリモードで書き込んで、テキストモードで読み込んだ状態で…
メモリウィンドウで終端部分をダンプしてみました。
30 31 32 33 34 35 36 37 38 39 0a 38 39 0d 0a 00 00……
先に出てくる0Ahまでが、本来読み込み完了した位置(szBuf[nReadSize])になります。
CStdioFile::Read()としては、「読み込んだと報告した位置以降」なので、知ったこっ
ちゃないよ。
ということでしょう。
読み込みできるバッファサイズを越えていたりもしませんし。
で、「文字列として読み込んだ」わけではないので、「読み込んだと報告した位置」の後
に終端マーク('\0')の付与はしていない。
# プログラマが責任もって付けろ。
ということでしょう。
で、テキストモードの為、0Dh 0Ahを0Ahに切り詰めるという処理を指定されたバッファ内
で行っていて、その時のデータが「読み込んだと報告した位置以降」に残っている。
ということかも知れません。
# なので、ダンプすると0Dh 0Ahで終わっている。
CStdioFile::Read()の後でszBuf[nReadSize] = _T('\0');として、明示的に「終端マー
ク」を設定することで正しく動作するかと。
>先に出てくる0Ahまでが、本来読み込み完了した位置(szBuf[nReadSize])
ちょっと間違えたか。
szBuf[(nReadSize - 1)]かな。
szBuf[nReadSize] = _T('\0');
を行った後のメモリダンプは、
30 31 32 33 34 35 36 37 38 39 0a 00 39 0d 0a 00 00……
となりました。
もうひとつ…
http://www.c-tipsref.com/reference/string/strtok.html
strtok()では、第2引数の文字列内の「いずれかの文字」で区切るので、
今回の場合'\r'か'\n'を発見すると区切ります。
「\r\nと連続した場合」ではありませんのでご注意を。
回答については、瀬戸っぷさんのご指摘の通りでしょう。
ところで、CStdioFileクラスなのですが、コンパイルオプションで文字コードを
(1)MBCSにしている場合は入出力がMBCS(ASCII/Shift-JIS)
(2)Unicodeにしている場合は入出力がUnicode
の専用になってしまい。
UnicodeアプリとしてコンパイルするとMBCSの入出力ができなくなってしまうようなクラス
です。
何か裏道があるのかもしれませんけどみあたりませんでした。
現状、
(A)MSの公式見解として「MBCS用のMFCは非推奨」。UnicodeのMFCはOK。
(B)なので、UnicodeのMFCからは、CStdioFileを使ってASCII文字の入出力ができない。
という事態になっているようです(やれやれ)。
瀬戸っぷさん
丁寧に調査していただきありがとうございます。
大変参考になりました。
>テキストモードで開いていた為、0Dh 0Ahを0Ahに変換しているようです。
># 1050行で、nReadSizeが1050バイト分ほど少ない。
GetLength()で得た値とReadの戻り値の相違は、ちょうど改行文字が1バイトに変換された分
ですね。こちらは想定どおりの結果です。
>で、書き込み時のバイナリモードをテキストモードにして書き出しを行い、出力された
>ファイルをバイナリエディタで覗くと…
>\r\n(0Dh 0Ah)が\r\r\n(0Dh 0Dh 0Ah)に変換されます。
>内部的には改行は\nで扱うべき…となります。
># CStdioFile::Read()でも変換は生きるんですねぇ……。
プログラムで改行を\r\nで扱うと、テキストモードで書き込むと、改行変換の結果、余計な\r
が付与されてしまう、ということですね。
気を付けなければならないですね。
>で、テキストモードの為、0Dh 0Ahを0Ahに切り詰めるという処理を指定されたバッファ内
>で行っていて、その時のデータが「読み込んだと報告した位置以降」に残っている。
>ということかも知れません。
># なので、ダンプすると0Dh 0Ahで終わっている。
なるほど。いったん、メモリにバイナリデータを読み込んだのち、クリアせずに切り詰め処理
を行っているということでしょうかね。
切り詰められた差分が完全に残っていないのは、適当なブロック単位に読み込んでいるため中
途半端に残っているのかもしれませんね。(あくまでも推測ですが)
>明示的に「終端マーク」を設定することで正しく動作するかと。
その手がありましたね。
私の環境でもこの方法で正しく動作することを確認しました。
わざわざバイナリモードに変更するより、こちらの方法が簡潔でよさそうですね。
>strtok()では、第2引数の文字列内の「いずれかの文字」で区切るので、
>今回の場合'\r'か'\n'を発見すると区切ります。
>「\r\nと連続した場合」ではありませんのでご注意を。
こちらは誤解しておりました。指摘していただきありがとうございます。
仲澤@失業者さん
貴重な情報をいただきありがとうございます。
>(A)MSの公式見解として「MBCS用のMFCは非推奨」。UnicodeのMFCはOK。
>(B)なので、UnicodeのMFCからは、CStdioFileを使ってASCII文字の入出力ができない。
ネットで情報を調べて、公式見解はまだ確認していないのですが、ひとまず、Visual Studio
C++ 2013 では、MFCのMBCS版が非推奨ということを確認しました。
既存のアプリでUnicode対応を迫られることになり、なかなか面倒な事態ですね。
>MFCのMBCS版が非推奨ということを確認しました。
MBCS版の追加パッケージがダウンロ-ドできました。
今もできるかな?
MBCSでプロジェクト設定すると可能になります。
Unicodeは使えなくなるのかな?
_T()は使えたはずですね。