テキストモードでファイルオープン

[戻る]
ARA 2018/11/17(土) 17:56:48
お世話になっています。

テキストモードでのファイル読み込みの挙動について、質問がございます。

テキストモードでオープンしたファイルをReadすると、なぜかファイルサイズよりも余分
にバッファが確保されてしまいます。
ある程度のサイズ(私の環境では約1MB)を超えるファイルだと現象が出ます。
また、改行を含まないファイルでは現象が出ないことを確認しています。

なぜ、このような現象が出るのか、教えていただけないでしょうか?

OS:Windows10 64bit
開発環境:VisualStudio2010(コンソールアプリケーション。また、プロジェクトの文字セ
ットを「マルチバイト文字セットを使用する」としています)

ARA 2018/11/17(土) 17:57:16
// 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;
}

ARA 2018/11/17(土) 18:43:34
上記のプログラムは、改行を含むファイルをテキストモードでオープンし、Readした場
合、データの末尾に余計なデータが入り込んでしまっていることを示すものです。

また、下記については確認しています。

・C言語(read関数, fread関数)を用いても同様の現象が出る
・バイナリモードでオープンすると、正しいサイズを取得できる

よろしくお願いいたします。

ARA 2018/11/19(月) 09:33:16
提示したプログラムを一点、訂正いたします。

    //CString strToken(_T("\r\n"));// 区切りが改行の場合、現象が出ます。
    CString strToken(_T("\t\t"));// 区切りが改行以外の場合、現象が出ません。

 ↓↓↓

    CString strToken(_T("\r\n"));// 区切りが改行の場合、現象が出ます。
    //CString strToken(_T("\t\t"));// 区切りが改行以外の場合、現象が出ません。

提示したそのままのソースでは現象が出ず、改行のコメントアウトを復活させると現象が出ま
す。(いろいろテストした結果、変更箇所がそのままになっていました。失礼いたしました。)

また、補足ですが、プロジェクトの設定は「共有 DLL で MFC を使う」としています。

瀬戸っぷ [E-Mail] 2018/11/19(月) 16:08:38
>ある程度のサイズ(私の環境では約1MB)を超えるファイルだと現象が出ます。

サイズは関係ないはずなんですけどねぇ…。
バッファに収まるかどうかで挙動変わるんでしょうか?

バイナリモードで書き込んで、テキストモードで読み出す。
という処理をしているので、改行コードの変換部分でサイズの変化があるのでしょう。
# テキストモードで書き込んで、バイナリモードで読み出す。でも似たような問題が発生
するでしょうけど。

CStdioFile::Read()ではなく、
CStdioFile::ReadString()で読み込むべきかと思います。
# CStdioFile::Read()は継承元のCFile::Read()になるでしょうけど。

ARA 2018/11/19(月) 16:24:46
瀬戸っぷさん

返信いただきありがとうございます。

試しに、テキストモードで書き込みテキストモードで読み出すよう変更しましたが、同様の現象
が出ました。

(下記の部位について、変更を行いました。)

    if(fileWrite.Open(szFile, CFile::typeBinary | CFile::modeCreate | 
CFile::modeWrite))

 ↓↓↓

    if(fileWrite.Open(szFile, CFile::typeText | CFile::modeCreate | 
CFile::modeWrite))

CStdioFile::ReadString() は速度が遅いため使用を躊躇しています。
バイナリモードでWrite、Readともに行うことで、一応の現象の回避はできていますが、根本的
な原因の解明にはなっていないので、引き続きよろしくお願いいたします。

瀬戸っぷ [E-Mail] 2018/11/21(水) 11:00:57
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()でも変換は生きるんですねぇ……。


サイズで動作に差が出る原因は不明…なままです。

瀬戸っぷ [E-Mail] 2018/11/21(水) 11:12:37
バイナリモードで書き込んで、テキストモードで読み込んだ状態で…
メモリウィンドウで終端部分をダンプしてみました。

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');として、明示的に「終端マー
ク」を設定することで正しく動作するかと。

瀬戸っぷ [E-Mail] 2018/11/21(水) 11:16:26
>先に出てくる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……
となりました。

瀬戸っぷ [E-Mail] 2018/11/21(水) 11:29:27
もうひとつ…

http://www.c-tipsref.com/reference/string/strtok.html
strtok()では、第2引数の文字列内の「いずれかの文字」で区切るので、
今回の場合'\r'か'\n'を発見すると区切ります。
「\r\nと連続した場合」ではありませんのでご注意を。

仲澤@失業者 2018/11/21(水) 14:37:02
回答については、瀬戸っぷさんのご指摘の通りでしょう。

ところで、CStdioFileクラスなのですが、コンパイルオプションで文字コードを

(1)MBCSにしている場合は入出力がMBCS(ASCII/Shift-JIS)
(2)Unicodeにしている場合は入出力がUnicode

の専用になってしまい。
UnicodeアプリとしてコンパイルするとMBCSの入出力ができなくなってしまうようなクラス
です。
何か裏道があるのかもしれませんけどみあたりませんでした。
現状、

(A)MSの公式見解として「MBCS用のMFCは非推奨」。UnicodeのMFCはOK。
(B)なので、UnicodeのMFCからは、CStdioFileを使ってASCII文字の入出力ができない。

という事態になっているようです(やれやれ)。

ARA 2018/11/22(木) 09:12:12
瀬戸っぷさん

丁寧に調査していただきありがとうございます。
大変参考になりました。

>テキストモードで開いていた為、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と連続した場合」ではありませんのでご注意を。

こちらは誤解しておりました。指摘していただきありがとうございます。

ARA 2018/11/22(木) 09:13:44
[[解決]]
仲澤@失業者さん

貴重な情報をいただきありがとうございます。

>(A)MSの公式見解として「MBCS用のMFCは非推奨」。UnicodeのMFCはOK。
>(B)なので、UnicodeのMFCからは、CStdioFileを使ってASCII文字の入出力ができない。

ネットで情報を調べて、公式見解はまだ確認していないのですが、ひとまず、Visual Studio 
C++ 2013 では、MFCのMBCS版が非推奨ということを確認しました。

既存のアプリでUnicode対応を迫られることになり、なかなか面倒な事態ですね。

ITO 2018/11/22(木) 14:35:25
>MFCのMBCS版が非推奨ということを確認しました。
MBCS版の追加パッケージがダウンロ−ドできました。
今もできるかな?
MBCSでプロジェクト設定すると可能になります。
Unicodeは使えなくなるのかな?
"_T()"は使えたはずですね。

古いスレッドには追加発言できません。