我一个小工具,vs2015 MFC对话框程序,任务是读取很多个小的文本文件,每个文件的内容大概几十到1000多字节。
这些文件的内容主要是中文文字,有换行,也可能会有数字、字母、标点符号等,就是一个普通的文本文件。
这些文件的编码不确定,但目前为止发现了三种编码,分别是unicode、ansi、utf-8,其中大多数为ansi编码。本文说的有问题,就是在读取有的ansi编码的文本文件时出现问题。
mfc程序采用unicode编码,读取文件的部分大致如下:
CFile file;
file.Open(strFileName, CFile::modeRead);
int fenc = 0; // 文件的编码类型
CString strFileConten = TEXT("");
CString strTmp;
file.Read(&fenc, 2);
CArchive ar(&file, CArchive::load);
ar.GetFile()->SeekToBegin();
while (ar.ReadString(strTmp)
{
// 如果文件是unicode编码,则每次只读取一行,而且行结尾没有换行符,所以这里就要给它加上换行符,否则后面在取一行时,就会一次性全部取出来而导致异常
if (strFileConten.GetLength())
strFileConten += TEXT("\r\n");
strFileConten += strTmp;
}
这段代码就是打开文件并读取它。全部按照unicode来读取,在读完之后,根据fenc以及取strFileConten的部分内容进行判断实际的编码,然后再按照对应的编码进行处理,这个流程是没有问题的,上面说的三种编码都可以成功处理,现在问题出在读取文件的while上了。
对于有的ansi文件,成功读取了fenc,但是while循环一次都没有进去,明明文件是有内容的,而且内容也没有特别的情况,为啥就读取失败了呢?
于是就跟踪进ReadString()函数中,这个函数如下所示:
BOOL CArchive::ReadString(CString& rString)
{
rString = _T(""); // empty string without deallocating
const int nMaxSize = 128;
LPTSTR lpsz = rString.GetBuffer(nMaxSize);
LPTSTR lpszResult;
int nLen;
for (;;)
{
lpszResult = ReadString(lpsz, (UINT)-nMaxSize); // store the newline
rString.ReleaseBuffer();
// if string is read completely or EOF
if (lpszResult == NULL ||
(nLen = AtlStrLen(lpsz)) < nMaxSize ||
lpsz[nLen-1] == '\n')
{
break;
}
nLen = rString.GetLength();
lpsz = rString.GetBuffer(nMaxSize + nLen) + nLen;
}
// remove '\n' from end of string if present
lpsz = rString.GetBuffer(0);
nLen = rString.GetLength();
if (nLen != 0 && lpsz[nLen-1] == '\n')
rString.GetBufferSetLength(nLen-1);
return lpszResult != NULL;
}
根据我的理解,这个函数是每次读取128个字符,由于是按照unicode读取的,所以每个字符就是2个字节,每次最多读取256个字节,如果中间遇到了换行符,就停止读取,然后返回,否则就一直读取下,直到把整个文件读取完毕。
对于有问题的一个文件,我跟踪进来查看,发现for中的ReadString是没有问题的,它返回的lpszResult是有值的,但这仅是第一次循环,在第二次循环时,它返回了NULL,于是,在函数的结尾处,就变成了 return NULL != NULL,即返回了FALSE,于是,我的代码就是读取错误了,但此时,strTmp里是有内容的,而且是正常的。从这里看,这个函数是有问题的,其实问题不止一个,目前发现了两个。
通过以上的跟踪,说明了这个函数采用 lpszResult是否为空来判断返回值是不正确的,它只表示了最后一次的状态,而不能说明前面的状态。再仔细看读取失败的文件,它的长度是257个字节,后来我删掉了两个字节(一个回车换行),变成255个字节后,再去读取,发现就没有报错了,我把另外一个文件的内容长度也弄成257字节,结果读它也是失败。这样问题就比较明了了,当文件的内容长度刚好是257时,它就失败。仔细检查发现,在ReadString()中,for第一遍刚好读取256字节,就剩下一个字节了,for的第二遍时候,其调用的ReadString()函数内,直接导致了文件结尾,由于还没有读出一个字节,所以它就直接返回了NULL,结果这个正常的ReadString()也返回了FALSE。
知道了问题,就对它进行改造,采用类继承的办法解决。类声明:
class CMyArchive :
public CArchive
{
public:
CMyArchive(CFile* pFile, UINT nMode, int nBufSize = 4096, void* lpBuf = NULL);
virtual ~CMyArchive();
BOOL ReadString(CString& rString);
};
类定义:
CMyArchive::CMyArchive(CFile* pFile, UINT nMode, int nBufSize, void* lpBuf):
CArchive(pFile, nMode, nBufSize, lpBuf)
{
}
CMyArchive::~CMyArchive()
{
}
BOOL CMyArchive::ReadString(CString& rString)
{
rString = _T(""); // empty string without deallocating
const int nMaxSize = 128;
LPTSTR lpsz = rString.GetBuffer(nMaxSize);
LPTSTR lpszResult = NULL; // 这里必需初始化为NULL
LPTSTR lpResTmp;
int nLen;
for (;;)
{
lpResTmp = CArchive::ReadString(lpsz, (UINT)-nMaxSize); // store the newline
rString.ReleaseBuffer();
// 这里增加一个判断附值
if (lpResTmp)
lpszResult = lpResTmp;
// if string is read completely or EOF
if (lpszResult == NULL ||
(nLen = AtlStrLen(lpsz)) < nMaxSize ||
lpsz[nLen - 1] == '\n')
{
break;
}
nLen = rString.GetLength();
lpsz = rString.GetBuffer(nMaxSize + nLen) + nLen;
}
// remove '\n' from end of string if present
lpsz = rString.GetBuffer(0);
nLen = rString.GetLength();
if (nLen != 0 && lpsz[nLen - 1] == '\n')
rString.GetBufferSetLength(nLen - 1);
return lpszResult != NULL;
}
使用的时候,仅仅把原来的 CArchive ar(&file, CArchive::load); 改成 CMyArchive ar(&file, CArchive::load); 即可。
这样改造之后,再测试,问题解决。
后记:
上面说有两个问题,今天解决了一个问题,这里面的另外一个问题,就是在上面描述的情况下,如果文件的长度是寄数,则会导致少读取最后一个字节。这个问题后面有时间的时候再来解决,现在是没有时间了,这个问题针对我的这个工具,影响不大,所以做了一些处理就暂时避过了由其导致的问题。