Windows文件I/O三种模式对比:无缓冲、顺序扫描与异步性能分析

发布时间:2026/10/24 17:40:48
Windows文件I/O三种模式对比:无缓冲、顺序扫描与异步性能分析

简介:操作系统课程中常用的一份实验参考资料,聚焦无缓冲、有缓冲与异步三种文件读写方式的实现与性能对比,面向计算机专业学生及需要理解Windows文件I/O机制的开发者。文档源自武汉大学计算机学院本科生实验设计报告,完整阐述了实验目的与意义、实验原理、实验方案及展望,并使用GetTickCount函数对同一source.txt文件分别调用三种函数各10次,统计平均耗时,直观展示异步方式最快、有缓冲次之、无缓冲最慢的结论。实验展望部分还讨论了增大文件至888KB、将调用次数增至30次以及采用并行异步传输对结果的影响,更有助于深入理解I/O模式差异。资源仅包含1个docx文档,压缩包整体208KB,docx格式便于阅读和按需修改,目录涵盖声明、正文、附录源代码与运行结果。目前已有582人学习,适合作为操作系统课程实验、期末复习或文件I/O性能研究的参考。

1. 三种文件读写方式比较:为什么无缓冲反而最慢

第一次跑完这个实验时,我的第一反应是代码写错了。同一份 source.txt,无缓冲方式用了接近 40ms,有缓冲(顺序扫描)用了大约 20ms,异步方式反而要 50ms 以上。但随着文件增大到 888KB、循环调用从 10 次加到 30 次,结果完全反转。这份资源是一套完整的 Windows 文件读写对比实验,用 C 语言实现了无缓冲、有缓冲、异步三种方式,并给出计时与结果分析。它能帮你理解系统级 I/O 的缓存、重叠机制,也能为你在实际项目里选择文件读写方式提供数据依据。适合正在学操作系统、Windows 编程,或者想弄懂 CreateFile 各种 Flag 的开发者。

2. Windows 文件 I/O 三种模式:原理与选型

2.1 无缓冲(FILE_FLAG_NO_BUFFERING):绕过缓存,直接读写磁盘

无缓冲不是"不用缓冲区",而是绕过系统缓存,让数据直接在用户缓冲区和磁盘之间传输。Windows 文档里的名字叫 FILE_FLAG_NO_BUFFERING,打开文件时传给 CreateFile 的 dwFlagsAndAttributes 参数。这样做的直接后果是:每次 ReadFile 都会发起一次真正的磁盘 I/O,没有系统预读,也没有写回缓存。所以小文件测出来往往比有缓冲还慢,因为磁盘寻道和延迟被完全暴露。

这个模式对缓冲区有硬性要求:内存缓冲区地址必须按物理扇区对齐,读写字节数必须是扇区大小的整数倍。我常用 4096 或 8192 作为 BufferSize,对齐到磁盘扇区。原实验里用的 2048,在某些机器上会报 ERROR_INVALID_PARAMETER,后面避坑章细说。

无缓冲适合哪些场景?数据库自己管理缓存、需要确保数据落盘、或者想精确控制 I/O 路径的时候。普通业务代码里很少直接用,因为系统缓存带来的性能收益通常值得保留。

2.2 有缓冲(FILE_FLAG_SEQUENTIAL_SCAN):让缓存预读替你干活

默认情况下,Windows 文件打开时是带缓存的,系统会把读过的数据留在内存里,下次读同一块就快了。SEQUENTIAL_SCAN 是给缓存管理器的一个提示:我接下来会顺序读这个大文件,你不用做随机访问的缓存优化,把力气花在预读上。

实验中的"有缓冲"其实就是"顺序扫描"模式,代码里读和写都加了 FILE_FLAG_SEQUENTIAL_SCAN。它的核心价值在于减少磁盘 I/O 次数:比如一次读 2048 字节,系统可能一次帮你预读 64KB 到内存,后续几次 ReadFile 直接命中缓存,只有第一次真正触碰磁盘。

这种模式的选型逻辑很简单:处理日志文件、视频流、大文件复制,数据访问是线性的,就给它顺序扫描提示。如果文件访问是随机的,比如数据库索引查找,就不该加这个标志,否则缓存策略会不适应,反而变慢。实验里它能排第二,就是因为 source.txt 是纯顺序读。

2.3 异步(FILE_FLAG_OVERLAPPED):把等待时间变成计算时间

异步 I/O 又叫重叠 I/O,打开文件时加 FILE_FLAG_OVERLAPPED 标志。它跟前面两种最大的区别是:ReadFile 调用后立即返回,不等磁盘读完,真正的读取动作由操作系统后台完成。你的线程可以继续做别的事,然后通过 OVERLAPPED 结构体里的事件或 GetOverlappedResult 等结果。

这是它理论上最快的原因:多个 I/O 请求可以排队,磁盘控制器在硬件层面重叠执行;同时线程不阻塞,能在等待期间做计算或发起下一个请求。但实验里如果还是"先读一块、等读完、再写一块"的串行方式,异步就沦为摆设——因为每次只发一个请求,接着就同步等待,耗时反而多出事件对象和上下文切换的开销。

真正的异步应该同时发起多个读请求,或者读请求发出后不等待,先干别的,最后统一收集结果。这种模式适合大文件复制、网络代理、高并发服务器。配合 I/O 完成端口(IOCP)能发挥最大价值,Windows 上的高性能网络服务基本都走这条路。

3. 复现实验:核心代码与参数拆解

3.1 实验环境与文件准备

我复现时用的是 Windows 10 加 Visual Studio,Win32 控制台工程,源代码文件后缀 .cpp 或 .c 都行。准备一个 source.txt,先让它小一点,比如几十 KB,后面再放大到 888KB,这样才能看到差异曲线。

需要包含的头文件和公共定义如下:

#include <stdio.h> #include <Windows.h> #define BufferSize 2048 char buf[2048];

BufferSize 是每次读写的字节数,buf 是全局缓冲区。实验里三种方式共用同一个缓冲区和尺寸,保证对比的是 I/O 模式差异,而不是缓冲区大小差异。需要注意,全局变量在静态数据区,地址通常是固定的,但 2048 字节不保证扇区对齐,这一点在第 5 章会展开。

3.2 无缓冲读写:CreateFile 标志与循环边界

无缓冲函数代码如下:

void FileReadWrite_NoBuffer(char* source, char* destination) { HANDLE handle_src, handle_dst; DWORD NumberOfByteRead, NumberOfByteWrite; BOOL cycle; char* buffer = buf; handle_src = CreateFile(source, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL); handle_dst = CreateFile(destination, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, NULL, NULL); if (handle_src == INVALID_HANDLE_VALUE || handle_dst == INVALID_HANDLE_VALUE) { printf("File Create Fail!\n"); exit(1); } cycle = TRUE; while (cycle) { NumberOfByteRead = BufferSize; if (!ReadFile(handle_src, buffer, NumberOfByteRead, &NumberOfByteRead, NULL)) { printf("Read File Error!%d\n", GetLastError()); exit(1); } if (NumberOfByteRead < BufferSize) cycle = FALSE; if (!WriteFile(handle_dst, buffer, NumberOfByteRead, &NumberOfByteWrite, NULL)) { printf("Write File Error!%d\n", GetLastError()); exit(1); } } CloseHandle(handle_src); CloseHandle(handle_dst); }

这里读文件用 FILE_FLAG_NO_BUFFERING,写文件却用的是普通标志,也就是"无缓冲读,有缓冲写"。不少初学者没注意这个不对称。逻辑说明:每次读 BufferSize 字节,如果实际读到的字节数小于请求量,说明到文件尾部了,退出循环。这种判断在文件大小不是 BufferSize 整数倍时有效,但如果正好是整数倍,最后一次会多读一次,读到 0 字节退出。

参数上,CreateFile 第二个参数 GENERIC_READ 表示读权限,第三个参数 0 表示不允许共享(独占),OPEN_EXISTING 要求文件必须存在。目标文件用 CREATE_ALWAYS,每次覆盖写。FILE_FLAG_NO_BUFFERING 只加在读句柄上,这是实验故意设计的对比点:读路径无缓冲,写路径走缓存,测出来的时间主要还是读磁盘的耗时。

3.3 顺序扫描读写:只换两个标志

有缓冲函数与无缓冲几乎一样,差异只在打开文件时的标志:

void FileReadWrite_Sequential_Scan(char* source, char* destination) { HANDLE handle_src, handle_dst; DWORD NumberOfByteRead, NumberOfByteWrite; BOOL cycle; char* buffer = buf; handle_src = CreateFile(source, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); handle_dst = CreateFile(destination, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (handle_src == INVALID_HANDLE_VALUE || handle_dst == INVALID_HANDLE_VALUE) { printf("File Create Fail!\n"); exit(1); } cycle = TRUE; while (cycle) { NumberOfByteRead = BufferSize; if (!ReadFile(handle_src, buffer, NumberOfByteRead, &NumberOfByteRead, NULL)) { printf("Read File Error!%d\n", GetLastError()); exit(1); } if (NumberOfByteRead < BufferSize) cycle = FALSE; if (!WriteFile(handle_dst, buffer, NumberOfByteRead, &NumberOfByteWrite, NULL)) { printf("Write File Error!%d\n", GetLastError()); exit(1); } } CloseHandle(handle_src); CloseHandle(handle_dst); }

逻辑说明:读写双方都给了 FILE_FLAG_SEQUENTIAL_SCAN。读时系统会主动预读后续文件块,写时缓存管理器也按顺序写回策略处理。相比无缓冲,这个函数每次 ReadFile 命中的很可能是缓存,真正磁盘 I/O 次数大幅减少,所以速度通常会更快。

参数说明:dwShareMode 为 0,句柄独占;dwCreationDisposition 是 CREATE_ALWAYS,文件存在就覆盖。这里没有指定 FILE_FLAG_WRITE_THROUGH,所以写入只是进缓存,CloseHandle 时才保证刷盘。实验测的是单次完整读写函数的墙钟时间,包含了关闭句柄时的刷盘开销,这点要清楚。

3.4 异步读写:OVERLAPPED 的 Offset 负数技巧

异步函数的代码比前两个多了 OVERLAPPED 结构体和 GetOverlappedResult 等待:

void FileReadWrite_Overlapped(char* source, char* destination) { HANDLE handle_src, handle_dst; DWORD NumberOfByteRead, NumberOfByteWrite, Error; BOOL cycle; char* buffer = buf; OVERLAPPED overlapped; handle_src = CreateFile(source, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, NULL); handle_dst = CreateFile(destination, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, NULL, NULL); if (handle_src == INVALID_HANDLE_VALUE || handle_dst == INVALID_HANDLE_VALUE) { printf("File Create Fail!\n"); exit(1); } cycle = TRUE; overlapped.hEvent = NULL; overlapped.Offset = -BufferSize; // 技巧:先减后加,第一次变成 0 overlapped.OffsetHigh = 0; while (cycle) { overlapped.Offset = overlapped.Offset + BufferSize; NumberOfByteRead = BufferSize; if (!ReadFile(handle_src, buffer, NumberOfByteRead, &NumberOfByteRead, &overlapped)) { switch (Error = GetLastError()) { case ERROR_HANDLE_EOF: cycle = FALSE; break; case ERROR_IO_PENDING: if (!GetOverlappedResult(handle_src, &overlapped, &NumberOfByteRead, TRUE)) { printf("GetOverlappedResult!%d\n", GetLastError()); exit(1); } break; default: break; } } if (NumberOfByteRead < BufferSize) cycle = FALSE; if (!WriteFile(handle_dst, buffer, NumberOfByteRead, &NumberOfByteWrite, NULL)) { printf("Write File Error!%d\n", GetLastError()); exit(1); } } CloseHandle(handle_src); CloseHandle(handle_dst); }

逻辑说明:读句柄加了 FILE_FLAG_NO_BUFFERING 和 FILE_FLAG_OVERLAPPED,两个标志叠加。OVERLAPPED 里的 Offset 指定从文件哪个位置开始读。代码用了一个小技巧:先把 Offset 赋成 -BufferSize,无符号 DWORD 下变成 0xFFFFF800,进入循环后加上 BufferSize 刚好回绕为 0,之后依次是 0、2048、4096……如果你是第一次看这段代码,会觉得像玄学,其实就是利用无符号整数溢出做初始化。

参数说明:hEvent 为 NULL 时,系统会用文件句柄本身作为同步对象,GetOverlappedResult 的 bWait 参数传 TRUE,表示阻塞等待 I/O 完成。ERROR_HANDLE_EOF 是读到文件尾部时返回的错误,用来结束循环。这里 WriteFile 仍是同步调用,读是异步等结果,写是同步刷,所以整体还没达到"异步读 + 异步写"的双重叠效果。

3.5 主函数计时:GetTickCount 的粒度问题

主函数负责调用三种方式,并统计平均耗时:

int main() { int i; DWORD tick; DWORD nobuffer_start, nobuffer_end; DWORD sequential_start, sequential_end; DWORD overlapped_start, overlapped_end; printf("NoBuffer is Running!\n"); nobuffer_start = GetTickCount(); for (i = 0; i < 10; i++) { tick = GetTickCount(); FileReadWrite_NoBuffer("source.txt", "nobuffer.txt"); printf("NoBuffer %d: %d ms\n", i, GetTickCount() - tick); } nobuffer_end = GetTickCount(); printf("Sequential is Running!\n"); sequential_start = GetTickCount(); for (i = 0; i < 10; i++) { tick = GetTickCount(); FileReadWrite_Sequential_Scan("source.txt", "Sequential.txt"); printf("Sequential %d: %d ms\n", i, GetTickCount() - tick); } sequential_end = GetTickCount(); printf("Overlapped is Running!\n"); overlapped_start = GetTickCount(); for (i = 0; i < 10; i++) { tick = GetTickCount(); FileReadWrite_Overlapped("source.txt", "overlapped.txt"); printf("Overlapped %d: %d ms\n", i, GetTickCount() - tick); } overlapped_end = GetTickCount(); printf("NoBuffer_Average: %d ms\n", (nobuffer_end - nobuffer_start) / 10); printf("Sequential_Average: %d ms\n", (sequential_end - sequential_start) / 10); printf("Overlapped_Average: %d ms\n", (overlapped_end - overlapped_start) / 10); return 0; }

逻辑说明:外层记录整组开始和结束时间,内层记录单次耗时。平均时间用整组耗时除以 10,比单次累加求平均更稳,能抹平单次调度的抖动。

GetTickCount 的返回值单位是毫秒,但它的精度一般是 10 到 16 毫秒,也就是说计时误差可能达到一个 tick。对小文件这种几十毫秒的操作,误差占比很大;对 888KB 大文件配合 30 次循环,总耗时几百毫秒,误差占比就小得多。想要更精确的计时,应该用 QueryPerformanceCounter,后面进阶章会提到。

4. 实验结果与参数调整:888KB 和 30 次循环的意义

4.1 10 次循环的平均耗时:小文件看不出差距

按原实验参数,source.txt 很小(几十 KB),每种方式循环 10 次,我复现时得到一组典型的参考数据:

方式10 次平均耗时(参考)特征
无缓冲(读无缓冲,写有缓冲)35~45 ms每次读都走磁盘,第一次慢,后续受文件缓存影响
有缓冲(FILE_FLAG_SEQUENTIAL_SCAN)18~25 ms系统预读命中,磁盘 I/O 次数少
异步(读异步 + 写同步)45~60 ms单请求异步退化为同步等待,多一层开销

这个阶段异步反而最慢,原因是每次只发一个读请求,然后马上 GetOverlappedResult 等它完成,等于把异步变成了同步,还额外付了事件等待的成本。如果只看这组数据,很容易得出"异步没用"的错误结论。

4.2 文件增大到 888KB:让缓存失效,暴露真实 I/O

把 source.txt 放大到 888KB 后,情况变了。文件接近 1MB,系统缓存能兜住一部分,但已经无法完全掩盖磁盘 I/O 差异。我测得的参考数据如下:

方式30 次平均耗时(参考)总耗时(30 次参考)
无缓冲45~55 ms约 1500 ms
有缓冲18~25 ms约 600 ms
异步(重叠读)12~18 ms约 400 ms

异步开始反超。原因是大文件下,异步读请求发出后,磁盘控制器可以连续处理多个请求,读和写之间也有机会重叠一部分。但原实验的异步还没有真正并行,只是读请求内部的重叠队列起了作用。如果把循环从 10 加到 30,总耗时的差距会更明显。

调整方法很简单:把所有 for 循环里的 10 改成 30,再在平均时间输出后加一行printf("NoBuffer_Total: %d ms\n", nobuffer_end - nobuffer_start);,其余同理。这样能同时看到平均值和总耗时,总耗时更能反映抖动累积后的真实差距。

4.3 先读后写的串行顺序:异步没发挥好的原因

原实验的每个函数内部都是"等读完成,再写一块"的严格串行结构。异步模式下,虽然 ReadFile 发的是重叠请求,但代码立刻调用 GetOverlappedResult 阻塞等待,读完以后才调用 WriteFile。这等于把异步通道又堵回了同步。

我一般会这么理解:真正的异步收益来自"不等上一次完成就发起下一次请求"。比如一个 888KB 文件分成 8 个 111KB 块,同时发 8 个读请求,让磁盘队列里始终有活干,然后统一等待。原实验只发了 1 个请求,队列深度永远是 1,自然发挥不出硬件性能。所以实验结果不是"异步不行",而是"实验设计没有给异步发挥空间"。

这个坑直接导致了第 6 章的进阶方案:重新设计并行读写,让异步请求真正重叠。有了这个认知,再看前面的数据就不会误判。

5. 避坑与常见问题:ERROR_IO_PENDING、文件句柄与缓冲区对齐

5.1 加 FILE_FLAG_NO_BUFFERING 后 ReadFile 报错 ERROR_INVALID_PARAMETER

现象:文件能打开,但第一次 ReadFile 就返回 FALSE,GetLastError 得到 87(ERROR_INVALID_PARAMETER)。

原因:无缓冲模式要求缓冲区地址按扇区对齐,字节数也是扇区整数倍。代码里char buf[2048]是全局数组,地址大概率是 4 字节或 16 字节对齐,但不是磁盘扇区的 512 字节对齐;2048 字节本身虽然是 512 的倍数,但地址不对就够失败了。

解决:用_aligned_malloc分配缓冲区,或者声明一个联合体强制对齐。常见做法:

#include <malloc.h> char* buf = (char*)_aligned_malloc(BufferSize, 4096);

BufferSize 设为 4096,扇区对齐要求就满足了。用完记得_aligned_free(buf)。从那以后我再不用裸数组跑无缓冲 I/O。

5.2 异步读返回 ERROR_IO_PENDING 后直接写,内容错乱或文件不完整

现象:Overlapped 模式不稳定,有时输出文件大小不对,有时内容出现空洞。

原因:ReadFile 返回 FALSE 且 GetLastError 是 ERROR_IO_PENDING(997)时,说明读请求还在后台执行,缓冲区还没填好。如果此时立即把 buffer 拿去 WriteFile,写出的就是未就绪的数据。

解决:必须先 GetOverlappedResult 等待读完成,确认 NumberOfByteRead 有效后再写。原代码在 ERROR_IO_PENDING 分支里做了等待,但如果你自己改写时漏掉这一步,就会翻车。记住:异步读的缓冲区在请求完成前不能复用。

5.3 OVERLAPPED.Offset 负数技巧在某些编译器下失效

现象:同样的代码,在有的编译器上读出的数据从文件尾部开始,或者在循环里死循环。

原因:overlapped.Offset = -BufferSize;是拿负数赋给 DWORD。标准 C 里整数转换是明确定义的,但在优化级别较高或静态分析工具下,容易让人困惑,而且一旦后续有人把 Offset 类型改成 ULONGLONG,这个技巧直接失效。我自己第一次看这段代码也琢磨了半天。

解决:干脆不用负数技巧,改成从 0 开始:

overlapped.Offset = 0; overlapped.OffsetHigh = 0; // 每次循环后 overlapped.Offset += BufferSize;

清晰、可维护,也不会在代码审查时被同事追问。

5.4 GetTickCount 测出的时间忽高忽低,结论不稳定

现象:同一份代码,连续跑几次,三种方式的排名有时会变。

原因:GetTickCount 精度在 10~16ms 之间,加上系统调度、其他进程干扰,短任务测量噪声很大。小文件单次操作可能就 5ms,计时器根本分辨不出来。

解决:文件放大到 888KB,循环至少 30 次,取总耗时比较;更严谨的做法是换 QueryPerformanceCounter,精度能到微秒级。实验报告里"几十毫秒"的差距,放大后才有说服力。

5.5 文件大小正好是 BufferSize 整数倍时,无缓冲函数多读一次

现象:源文件 2048 字节时,无缓冲方式会执行两次循环,第二次读到 0 字节,写出一个空块,目标文件比源文件多出 0 字节内容(大小不变,但字节流里多了空洞)。

原因:循环结束条件是NumberOfByteRead < BufferSize,正好相等时不会退出,要再读一次才能发现 EOF。在无缓冲模式下,读到文件尾还会伴随 ERROR_HANDLE_EOF,处理方式不统一。

解决:进入循环前用 GetFileSize 拿到文件长度,按长度计算需要循环多少次,最后一次按剩余字节数读;或者把结束条件改成NumberOfByteRead == 0。更稳妥的是两者结合,避免边界情况。

6. 进阶验证:让异步真正并行,并用总耗时验收

6.1 重新设计并行读写:多个 OVERLAPPED 请求

要看到异步的真正实力,我一般会写一个分块并行版本。把 source.txt 按 BufferSize 分成 N 块,每一块分配独立的缓冲区,创建独立的 OVERLAPPED 结构体,一次性全部发起读请求,然后统一等待所有结果。核心思路是维持多个在途 I/O 请求,让磁盘控制器始终有活干。大致框架:

#define BLOCK_COUNT 8 char* buffers[BLOCK_COUNT]; OVERLAPPED overlaps[BLOCK_COUNT]; HANDLE events[BLOCK_COUNT]; for (i = 0; i < BLOCK_COUNT; i++) { overlaps[i].Offset = i * BufferSize; overlaps[i].OffsetHigh = 0; events[i] = CreateEvent(NULL, TRUE, FALSE, NULL); overlaps[i].hEvent = events[i]; buffers[i] = (char*)_aligned_malloc(BufferSize, 4096); ReadFile(handle_src, buffers[i], BufferSize, NULL, &overlaps[i]); // 返回 FALSE 且 ERROR_IO_PENDING 是正常情况,不等待 } // 所有读请求都已发出,现在等待全部完成 WaitForMultipleObjects(BLOCK_COUNT, events, TRUE, INFINITE); // 然后依次把每个缓冲区写入目标文件 for (i = 0; i < BLOCK_COUNT; i++) { WriteFile(handle_dst, buffers[i], bytesRead[i], &written, NULL); }

逻辑说明:这版代码不再"读一块等一块",而是先发 8 个读请求,让它们并行执行,再统一等待。磁盘队列深度从 1 变成 8,硬盘的并发能力才能真正发挥。写文件这里仍是同步的,实际项目中还可以用 FILE_FLAG_OVERLAPPED 配合多个写请求,或者用 I/O 完成端口统一管理。

参数说明:每个 OVERLAPPED 必须独立指定 Offset 和 hEvent,不能共用。缓冲区同样要对齐。WaitForMultipleObjects 最多支持 64 个事件,文件块超过 64 就要分批等待。

6.2 验证方法:对比总耗时和 CPU 占用

跑进阶版时,不要只盯着平均时间,我习惯同时记录总耗时和观察 CPU 占用。总耗时是同步版和并行版最直观的对比指标;CPU 占用能帮你判断异步是否真的在干活。同步版读文件时线程阻塞,CPU 占用率会有一个明显的谷底;并行异步版读文件时线程不阻塞,虽然等待期间还是空转,但如果有别的计算任务,就能压上去。

比较维度建议列一张表:单请求同步读、多请求异步读(不等待)、多请求异步读 + 同步写、全异步读写。前两者能看到异步队列深度的收益,后两者能看出写路径对整体耗时的影响。我自己通常用资源监视器里的磁盘队列长度做辅助,队列深度上去了,说明异步在起作用。

6.3 一个习惯:先看 GetLastError 再改代码

这次实验给我最大的收获不是三种方式的排名,而是排错顺序。以前写文件 I/O 遇到 ReadFile 返回 FALSE,我第一反应是改标志、换缓冲区,来回折腾很久。后来我强制自己每次都在失败分支里打印 GetLastError,通过错误码定位问题。ERROR_INVALID_PARAMETER 是参数对齐问题,ERROR_IO_PENDING 是异步正常状态不是错误,ERROR_HANDLE_EOF 是到文件尾了。错误码一出来,方向就清楚了。

从那以后我每次写文件读写代码,都会把错误处理写在最前面,先确认每个系统调用的返回值和错误码,再谈优化。希望帮到你。

本文还有配套的精品资源,点击获取