我正在打开一个文件并将其内容放入字符串缓冲区中,以按字符进行一些词法分析。这样做可以使解析比使用后续数量的 fread(( 调用更快地完成,并且由于源文件始终不会大于几 MB,我可以放心,文件的全部内容将始终被读取。
但是,在检测何时没有更多要解析的数据时似乎存在一些麻烦,因为 ftell(( 通常会给我一个高于文件中实际字符数的整数值。如果使用 EOF (-1( 宏,如果尾随字符始终为 -1,这将不是问题......但情况并非总是如此...
以下是我打开文件并将其读取到字符串缓冲区中的方式:
FILE *fp = NULL;
errno_t err = _wfopen_s(&fp, m_sourceFile, L"rb, ccs=UNICODE");
if(fp == NULL || err != 0) return FALSE;
if(fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
fp = NULL;
return FALSE;
}
LONG fileSize = ftell(fp);
if(fileSize == -1L) {
fclose(fp);
fp = NULL;
return FALSE;
}
rewind(fp);
LPSTR s = new char[fileSize];
RtlZeroMemory(s, sizeof(char) * fileSize);
DWORD dwBytesRead = 0;
if(fread(s, sizeof(char), fileSize, fp) != fileSize) {
fclose(fp);
fp = NULL;
return FALSE;
}
这似乎总是工作得很好。下面是一个简单的循环,它一次检查一个字符的字符串缓冲区的内容,如下所示:
char c = 0;
LONG nPos = 0;
while(c != EOF && nPos <= fileSize)
{
c = s[nPos];
// do something with 'c' here...
nPos++;
}
文件的尾随字节通常是一系列 ý (-3( 和 « (-85( 字符,因此永远不会检测到 EOF。相反,循环只是继续前进,直到nPos最终具有比fileSize更高的值 - 这对于正确的词法分析来说是不可取的,因为您通常最终会跳过流中的最后一个标记,该流在末尾省略了换行符。
在基本拉丁字符集中,假设 EOF 字符是具有负值的任何字符是否安全?或者也许有更好的方法来解决这个问题?
#EDIT:我刚刚尝试在我的循环中实现 feof(( 函数,尽管如此,它似乎也没有检测到 EOF。
将注释组合成答案...
-
无法读取时会泄漏内存(可能占用大量内存(。
-
不允许在读取的字符串末尾使用 null 终止符。
当内存即将 被文件中的数据覆盖时,将内存归零是没有意义的。
您的测试循环正在越界访问内存;
。nPos == fileSize
是超出您分配的内存末尾的内存char c = 0; LONG nPos = 0; while(c != EOF && nPos <= fileSize) { c = s[nPos]; // do something with 'c' here... nPos++; }
这还有其他问题,前面没有提到。您确实问过"假设 EOF 字符是任何具有负值的字符是安全的",我回答是否定的。 这里有几个问题,会影响 C 和C++代码。 首先,纯
char
可以是有符号类型或无符号类型。 如果类型是无符号的,则永远不能在其中存储负值(或者,更准确地说,如果您尝试将负整数存储到无符号字符中,它将被截断为最低有效 8* 位,并将被视为正值。在上面的循环中,可能会出现以下两个问题之一。 如果
char
是有符号类型,则有一个字符(ÿ,y-变音符号,U+00FF,拉丁小写字母Y,拉丁语-1代码集中0xFF(具有与EOF相同的值(始终为负数,通常为-1(。 因此,您可能会过早地检测到 EOF。 如果char
是无符号类型,则永远不会有任何字符等于 EOF。 但是对字符串的EOF测试从根本上是有缺陷的;EOF 是 I/O 操作的状态指示器,而不是字符。在 I/O 操作期间,只有在尝试读取不存在的数据时,才会检测 EOF。
fread()
不会报告 EOF;您要求阅读文件中的内容。如果您在fread()
后尝试getc(fp)
,您将获得EOF,除非文件在您测量它有多长后已经增长。由于_wfopen_s()
是一个非标准函数,它可能会影响ftell()
的行为方式和它报告的值。(但你后来确定事实并非如此。请注意,
fgetc()
或getchar()
等函数定义为将字符返回为正整数,将 EOF 返回为不同的负值。如果未设置
stream
指向的输入流的文件结束指示器,并且 下一个字符存在,fgetc
函数获取该字符作为转换为int
的unsigned char
。如果设置了流的文件结束指示器,或者流位于文件末尾,则 设置流的文件指示器,
fgetc
函数返回 EOF。否则,fgetc
函数返回输入流中由stream
指向的下一个字符。 如果发生读取错误,则设置流的错误指示器和fgetc
函数 返回 EOF。289(289( 文件结束和读取错误可以通过使用
feof
和ferror
函数来区分。这指示 EOF 如何与 I/O 操作上下文中的任何有效字符分开。
您评论:
至于任何潜在的内存泄漏...在我的项目的这个阶段,内存泄漏是我的代码的众多问题之一,到目前为止,这些问题对我来说还无关紧要。即使它没有泄漏内存,它甚至一开始都不起作用,那么有什么意义呢?功能是第一位的。
在初始编码阶段阻止错误路径中的内存泄漏比稍后返回并修复它们更容易 - 因为您可能不会发现它们,因为您可能不会触发错误条件。 但是,这在多大程度上取决于节目的目标受众。 如果这是编码课程的一次性课程,您可能会没问题。 如果你是唯一会使用它的人,你可能没事。 但是,如果它将被数百万人安装,您将在任何地方改装检查时遇到问题。
我已经将_wfopen_s((换成了fopen((,ftell((的结果是相同的。但是,将相应的行更改为 LPSTR s = new char[fileSize + 1], RtlZeroMemory(s, sizeof(char( * fileSize + 1(;(这也应该空终止它,顺便说一句(,并将 if(nPos == 文件大小(添加到循环的顶部,它现在干净利落地出来了。
还行。 您也可以只使用 s[fileSize] = ' ';
来 null 终止数据,但使用 RtlZeroMemory()
可以实现相同的效果(但如果文件大小为数兆字节,则会更慢(。 但我很高兴各种意见和建议帮助你回到正轨。
* 理论上,CHAR_BITS可能大于 8;实际上它几乎总是 8,为了简单起见,我假设它是 8 位。 如果CHAR_BITS是 9 或更多,讨论必须更加细致入微,但净效果大致相同。