- 开发工具
- 静态分析
- 代码质量
- 质量保障
【免费下载链接】cppcheck
static analysis of C/C++ code
IOWithoutPositioning 是 cppcheck 内置的 I/O 类检查器之一,专门检测在同时以读写模式打开的FILE*流上,读操作与写操作之间缺少fseek()/fsetpos()/rewind()/fflush()等定位或刷新调用的情况——按 C 标准这属于未定义行为(Undefined Behaviour)。读完本文,你将掌握该检查器的触发条件、修复方式、cppcheck 的底层跟踪算法与能力边界,以及如何在真实项目中启用和验证它。
检查器基本信息
| 属性 | 值 |
|---|---|
| 消息 ID | IOWithoutPositioning |
| 消息文本 | Read and write operations without a call to a positioning function (fseek, fsetpos or rewind) or fflush in between result in undefined behaviour. |
| 类别 | Undefined Behaviour(未定义行为) |
| 严重级别 | Error |
| 适用语言 | C / C++ |
该检查器的核心实现在 lib/checkio.cpp:
void CheckIOImpl::ioWithoutPositioningError(const Token *tok) { reportError(tok, Severity::error, "IOWithoutPositioning", "Read and write operations without a call to a positioning function (fseek, fsetpos or rewind) or fflush in between result in undefined behaviour.", CWE664, Certainty::normal); }从源码可见,它按Severity::error级别报告,关联 CWE-664(Improper Control of a Resource Through its Lifetime),置信度为Certainty::normal。
问题背景:C 标准对读写切换的要求
C 标准规定:在同一个以读写模式打开的文件流上,若读取操作之后紧跟写入操作(或反之),且中间没有调用定位函数(fseek、fsetpos、rewind)或fflush(),则该行为是未定义的。尽管许多实现(如 glibc 的某些版本)在特定缓冲条件下恰好会产生可预期结果,但依赖这种“巧合”的代码在不同平台、不同缓冲模式下可能表现出截然不同的行为——例如读取位置与写入位置不一致、数据丢失或缓冲区内容被破坏。
常见容易出错的场景包括:用fopen("a.txt", "r+")打开文件后,先fread读取再fwrite覆盖,中间忘记用fseek(fp, 0, SEEK_CUR)之类的“空定位”强制刷新内部状态;或者先写后读时忘记在两者之间重新定位。
触发条件与典型误用示例
文档给出的“Before”示例(出错代码):
#include <cstdio> void f() { FILE *fp = fopen("a.txt", "r+"); if (!fp) return; char buf[10]; fread(buf, 1, 10, fp); fwrite(buf, 1, 10, fp); // <- no seek/rewind/fflush since the read above fclose(fp); }fopen("a.txt", "r+")以读写模式打开文件,随后fread之后直接fwrite,中间没有任何定位或刷新调用,因此触发IOWithoutPositioning。
官方“After”示例(正确代码):
#include <cstdio> void f() { FILE *fp = fopen("a.txt", "r+"); if (!fp) return; char buf[10]; fread(buf, 1, 10, fp); fseek(fp, 0, SEEK_CUR); // 空定位:不移动文件位置,但满足标准对读写切换的定位要求 fwrite(buf, 1, 10, fp); fclose(fp); }fseek(fp, 0, SEEK_CUR)将文件位置指针移动到当前位置——相当于“什么都不做”的定位操作,但它足以满足标准要求,是 read/write 模式文件在读写之间切换时的惯用写法。同理,fflush(fp)也能达到同样效果。
正确的修复方式小结
在同一个读写流上,读 → 写或写 → 读之间,任选其一:
fseek(fp, 0, SEEK_CUR):零位移定位,最常用;fsetpos(fp, &pos):定位到先前保存的位置;rewind(fp):将位置重置到文件开头(注意同时会清除错误标志);fflush(fp):刷新缓冲区,同样被标准视为满足读写切换的要求。
底层实现:cppcheck 如何跟踪 FILE*
该检查器并非简单地在同一函数内找两个相邻调用,而是基于一个跨语句的状态机持续跟踪每个FILE*变量的最近一次操作。
Filepointer 状态结构
在 lib/checkio.cpp 中,每个被跟踪的FILE*变量对应一个Filepointer结构,记录:
mode:文件的打开模式(只读 / 只写 / 读写 / 已关闭 / 未知);lastOperation:上一次操作类型,枚举值为NONE、UNIMPORTANT、READ、WRITE、POSITIONING、OPEN、CLOSE、UNKNOWN_OP;append_mode/read_mode:追加模式与文本/二进制读取模式(供同类检查器如seekOnAppendedFile、ftellFileError使用);mode_indent/op_indent:记录模式与操作所在的代码块缩进层级,用于处理分支与循环作用域。
操作分类与判定逻辑
CheckIOImpl::checkFilepointerUsage会对函数调用逐一分类(lib/checkio.cpp):
fseek、fsetpos、rewind、fflush(Windows 下还包括_fseeki64)→POSITIONING;fgetc、fgets、fread、fscanf、getc(及 Windows 的*_s变体)→READ;fputc、fputs、fwrite、fprintf、putc(及 Windows 变体)→WRITE;fclose→CLOSE;fopen/freopen/tmpfile(Windows 下还有_wfopen/_wfreopen)→OPEN;ungetc、ftell等 →UNIMPORTANT(不影响读写切换判定)。
判定规则集中在 lib/checkio.cpp:
case Filepointer::Operation::READ: if (f.mode == OpenMode::CLOSED) useClosedFileError(tok); else if (f.mode == OpenMode::WRITE_MODE) readWriteOnlyFileError(tok); else if (f.lastOperation == Filepointer::Operation::WRITE) ioWithoutPositioningError(tok); break; case Filepointer::Operation::WRITE: if (f.mode == OpenMode::CLOSED) useClosedFileError(tok); else if (f.mode == OpenMode::READ_MODE) writeReadOnlyFileError(tok); else if (f.lastOperation == Filepointer::Operation::READ) ioWithoutPositioningError(tok); break;即:当执行读操作时,若上一次操作是WRITE且中间没有POSITIONING/UNIMPORTANT重置,就报告;写操作同理,若上一次是READ则报告。一次POSITIONING操作会把lastOperation重置,从而“打断”读写连续序列。
值得注意的是:POSITIONING分支中若文件处于追加模式还会联动触发seekOnAppendedFile警告(因为追加模式下定位无效);fflush则特别处理了对输入流调用(fflushOnInputStream,仅按可移植性 warning 级别报告)。
能力边界:只检查能证明的代码
文档明确说明:cppcheck 只沿函数内直行代码(straight-line code)跟踪局部FILE*变量。从源码结构看,这一保守策略体现在多处:
- 当文件句柄被传给其他函数时,无法判断该函数内部是否发生了定位或关闭操作,cppcheck 不会猜测,而是把状态置为
UNKNOWN_OP(未知操作); - 全局或成员变量形式的文件句柄,若其被非静态成员函数使用,同样会因不确定性被重置为未知状态;
- 遇到
return、continue、break或noreturn函数调用时,会清空所有跟踪状态(lib/checkio.cpp); FILE*变量被重新赋值(非fopen/freopen来源)时,也会重置为未知。
这种“宁可不报,也不误报”的设计意味着:只有那些在同一函数内、被局部变量持有、且读写切换路径完全可证明的代码,才会命中IOWithoutPositioning。例如在 if/else 分支中分别执行读写(test/testio.cpp 的测试用例if(read) fread(...); else fwrite(...);)就不会被报告,因为两条路径互斥,cppcheck 无法证明它们在执行流上连续发生。
测试用例验证
该检查器在 test/testio.cpp 的fileIOwithoutPositioning()测试组中有大量覆盖,可以当作“行为规范”来阅读:
- 直行代码
fwrite → fread与fread → fwrite均会报告(error); - 分支路径(if/else 各执行一种操作)不报告;
fread → fflush → fwrite、fread → rewind → fwrite、fread → fsetpos → fwrite、fread → fseek → fwrite均不报告,验证四种合法“间隔操作”都被识别;- 关键反例:
fread → ftell → fwrite会报告——ftell只是查询当前位置,不是定位函数,不能打断读写切换序列; - 成员函数测试(issue #6452):中间夹着已知的
seek(writePosition)成员调用时不报告,而夹着未定义的unknown(writePosition)调用时报告,说明 cppcheck 能区分“已知会定位的封装”与“未知行为”。
启用方式与相关检查器
默认启用与 CERT 映射
该检查器属于io(CheckIO)模块的一部分,是 cppcheck 默认启用的检查之一,通常无需额外配置即可生效。它同时被映射到 CERT 规则编号:
- lib/checkersidmapping.cpp:
FIO39与FIO50均映射到IOWithoutPositioning; - lib/settings.cpp:
IOWithoutPositioning同时被列入certCCheckers与certCppCheckers集合,即使用--std=cert-c或--std=cert-c++认证标准运行时也会被强制纳入检查范围。
若需要在命令行显式指定检查器,可以使用:
cppcheck --enable=warning,style,performance,portability --checkers-report=checkers.txt 文件或目录然后在生成的checkers-report.txt中确认IOWithoutPositioning的状态;也可以使用--checkers相关选项查看该检查器是否被启用。检查器的完整清单与开关方式参见 man/checkers 目录及 checkers 报告机制 的实现。
同类 FILE* 跟踪检查器
IOWithoutPositioning 只是 CheckIO 中同一套FILE*跟踪机制下的一个成员。文档“Related checkers”中列出的其他检查器均共享Filepointer状态机,只是针对不同的非法序列:
- useClosedFile:在
fclose之后继续读写已关闭文件; - readWriteOnlyFile:对只写模式打开的文件执行读操作;
- writeReadOnlyFile:对只读模式打开的文件执行写操作;
- seekOnAppendedFile:对追加模式文件执行定位(追加模式下定位无效果);
- incompatibleFileOpen:对同一文件名以冲突模式重复打开。
这些检查器从 CheckIO::getErrorMessages 中可以看到完整集合,它们共同构成了 cppcheck 对 C 标准库文件 I/O 未定义行为与常见误用的系统性防护。
实战建议
- 修复优先采用“空定位”:
fseek(fp, 0, SEEK_CUR)是不移动位置的定位调用,语义清晰、可移植,是读写模式流切换的标准做法;rewind会同时清除错误标志并回到文件开头,仅在你确实想从头开始时使用。 - 不要用
ftell充当定位:ftell只查询位置不改变流状态,cppcheck 的测试用例(test/testio.cpp)明确证明它不能打断读写切换序列。 - 封装文件操作时让定位函数可见:如果文件句柄被封装在类或函数中,cppcheck 只有在能确认中间调用为已知定位操作时才不报告(参见 issue #6452 的成员函数测试);若无法静态证明,宁可显式在读写之间调用定位函数,既消除未定义行为,也让静态分析结果更干净。
- 结合同一套检查器一起修复:该检查器报告的错误往往与
readWriteOnlyFile、useClosedFile等同时出现,建议一次处理,避免遗漏同一条代码路径上的其他 I/O 未定义行为。
- 开发工具
- 静态分析
- 代码质量
- 质量保障
【免费下载链接】cppcheck
static analysis of C/C++ code
相关推荐
cppcheck 的 deallocret 检查器:捕获“释放后返回指针”的未定义行为
cppcheck 的 deallocret 检查器:捕获“释放后返回指针”的未定义行为 导读 deallocret 是 cppcheck 内置的 C/C++ 静
开发工具静态分析代码质量质量保障cppcheck invalidContainerLoop 检查器:在迭代中修改容器导致的未定义行为
cppcheck invalidContainerLoop 检查器:在迭代中修改容器导致的未定义行为 导读 invalidContainerLoop 是 cpp
开发工具静态分析代码质量质量保障Hydra 输出工作目录定制实战:run/sweep 目录模式与 override_dirname 深度解析
Hydra 输出工作目录定制实战:run/sweep 目录模式与 override_dirname 深度解析 本篇文章围绕 Hydra 框架的输出目录(work
开发工具静态分析代码质量质量保障
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考