Windows API 文件操作超级指南------从入门到内核
前言
在上一篇文章中,我们系统性地对比了 C/C++ 在 Linux、Windows、macOS 三平台的文件操作。那篇文章的评论区有位读者问了一个很刁钻的问题:"既然 fopen 和 fstream 都能跨平台,我干嘛还要学 Windows API 那一大坨参数?"
这个问题问到点子上了。在 Windows 平台上,标准库(CRT)的 fopen 最终也是调用 CreateFile 实现的,但经过 CRT 这一层封装,你失去了对文件句柄的精细控制------比如你想独占文件不让别人读、想开启异步 I/O、想直接在内存中映射文件,这些 fopen 都做不到。
这篇文章我们不讲虚的,直接从 Windows 内核对象 的视角切入,把 CreateFile 的十几个参数拆开揉碎了讲,再配合异步 I/O、内存映射、目录遍历、稀疏文件等高级特性,给你一份最硬核的 Windows 文件操作指南。
全文代码基于 Windows 10/11 + Visual Studio 2022,使用 Unicode (UTF-16) 字符集,这是 Windows 内核原生编码。
一、文件句柄(HANDLE)到底是什么?
Windows 内核把文件抽象成一种 内核对象(Kernel Object) 。当我们调用 CreateFile 时,内核会创建一个 FILE_OBJECT 结构体,并返回一个指向它的句柄(HANDLE)。
句柄本质上是一个进程句柄表(Handle Table) 的索引值(通常是指针的整数形式)。CRT 的 FILE* 是在用户态对句柄的一层缓冲封装,而 Windows API 直接操作句柄,绕过了用户态缓冲区。
核心思想 :Windows 中几乎所有资源(文件、进程、线程、注册表键、事件、互斥体、管道)都通过句柄访问,操作范式高度统一:Create* → Read*/Write* → CloseHandle。
二、CreateFile:拆解每一个参数
CreateFile 是 Windows 文件操作的入口点。它的函数原型(Unicode 版本)如下:
cpp
HANDLE CreateFileW(
LPCWSTR lpFileName, // 文件路径(UTF-16)
DWORD dwDesiredAccess, // 访问权限
DWORD dwShareMode, // 共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全描述符
DWORD dwCreationDisposition, // 创建/打开行为
DWORD dwFlagsAndAttributes, // 属性 & 标志
HANDLE hTemplateFile // 模板句柄
);
如果你在代码里写 CreateFile(不带后缀),在 VS2022 默认 Unicode 环境下实际上被宏替换为 CreateFileW。强烈建议显式使用 CreateFileW,避免 ANSI/Unicode 混淆。
2.1 dwDesiredAccess(访问权限)
这个参数不是简单的"读"或"写",而是 位掩码(Bitmask),可以组合。
| 常量 | 值 | 说明 |
|---|---|---|
GENERIC_READ |
0x80000000 |
允许读取数据、读取属性、读取扩展属性 |
GENERIC_WRITE |
0x40000000 |
允许写入数据、写入属性、写入扩展属性、追加数据 |
GENERIC_EXECUTE |
0x20000000 |
允许执行(对文件而言就是遍历目录) |
GENERIC_ALL |
0x10000000 |
所有权限 |
更精细的特定权限(NTFS 文件系统支持):
| 常量 | 说明 |
|---|---|
FILE_READ_DATA |
读文件数据(等同于 GENERIC_READ 的子集) |
FILE_WRITE_DATA |
写文件数据 |
FILE_APPEND_DATA |
在文件末尾追加(不能修改已有数据) |
FILE_READ_ATTRIBUTES |
读文件属性(如隐藏、只读) |
FILE_WRITE_ATTRIBUTES |
写文件属性 |
FILE_READ_EA |
读扩展属性(Extended Attributes) |
FILE_WRITE_EA |
写扩展属性 |
DELETE |
删除文件 |
实战建议 :大多数场景直接用
GENERIC_READ或GENERIC_WRITE。但如果要打开日志文件且只允许追加,用FILE_APPEND_DATA而不是GENERIC_WRITE,更安全(防止覆盖历史数据)。
2.2 dwShareMode(共享模式)
Windows 的共享控制非常严格:打开文件时必须声明你允许别人怎么访问 。如果违背了已有句柄的共享限制,打开就会失败(ERROR_SHARING_VIOLATION)。
| 常量 | 含义 |
|---|---|
0 |
独占:任何其他进程(包括本进程其他线程)都不能打开该文件,除非通过继承的句柄。 |
FILE_SHARE_READ |
允许其他进程读取该文件。 |
FILE_SHARE_WRITE |
允许其他进程写入该文件。 |
FILE_SHARE_DELETE |
允许其他进程删除 或移动该文件。 |
经典组合:
- 日志文件:
FILE_SHARE_READ(允许多个进程同时读日志) - 数据库文件:
0(独占,防止并发写损坏数据) - 临时文件:
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE(完全共享)
⚠️ 注意:
FILE_SHARE_DELETE意味着其他进程可以删除你正在使用的文件,删除后该文件的目录项消失,但你的句柄仍然有效,直到你CloseHandle后文件才会真正释放磁盘空间。
2.3 dwCreationDisposition(创建/打开行为)
这个参数控制文件不存在或已存在时的行为,是原子操作。
| 常量 | 文件不存在 | 文件已存在 |
|---|---|---|
CREATE_NEW |
创建新文件,返回句柄 | 失败 (ERROR_FILE_EXISTS) |
CREATE_ALWAYS |
创建新文件 | 覆盖现有文件(截断为 0) |
OPEN_EXISTING |
失败 (ERROR_FILE_NOT_FOUND) |
打开现有文件 |
OPEN_ALWAYS |
创建新文件 | 打开现有文件(不截断) |
TRUNCATE_EXISTING |
失败 (ERROR_FILE_NOT_FOUND) |
打开并截断为 0(需写权限) |
安全漏洞提醒 :CREATE_ALWAYS 在覆盖文件时如果文件存在且被其他进程以 FILE_SHARE_DELETE 方式打开,虽然能成功覆盖,但可能会触发文件系统重解析点(Reparse Point)的问题。如果要安全地替换文件内容,建议用 CREATE_NEW 写到临时文件,然后用 ReplaceFile 或 MoveFileEx 原子替换。
2.4 dwFlagsAndAttributes(文件属性和 I/O 标志)
这个参数是属性 (低 16 位)和标志 (高 16 位)的组合,用按位或 | 连接。
常用文件属性(FILE_ATTRIBUTE_*):
| 常量 | 说明 |
|---|---|
FILE_ATTRIBUTE_NORMAL |
默认普通文件(不能与其他属性组合) |
FILE_ATTRIBUTE_READONLY |
只读 |
FILE_ATTRIBUTE_HIDDEN |
隐藏文件 |
FILE_ATTRIBUTE_ARCHIVE |
归档(备份软件常用) |
FILE_ATTRIBUTE_TEMPORARY |
临时文件(系统尽量缓存内存中,延迟写回磁盘) |
FILE_ATTRIBUTE_ENCRYPTED |
EFS 加密 |
FILE_ATTRIBUTE_COMPRESSED |
NTFS 压缩(文件级) |
关键 I/O 标志(FILE_FLAG_*):
| 常量 | 深度解析 |
|---|---|
FILE_FLAG_OVERLAPPED |
开启异步 I/O 。句柄变为可重叠(Overlapped),读写函数需要传入 OVERLAPPED 结构。 |
FILE_FLAG_NO_BUFFERING |
绕过系统缓存 (也称为"裸 I/O")。读写必须按卷扇区大小对齐(通常 512 或 4096 字节),用于数据库等需要直接控制 I/O 的场景。极大提升可控性,但代码复杂度剧增。 |
FILE_FLAG_SEQUENTIAL_SCAN |
告诉缓存管理器:你会顺序访问文件。系统会预读(Prefetch)并优化缓存策略。 |
FILE_FLAG_RANDOM_ACCESS |
告诉缓存管理器:你会随机跳转访问。系统禁用预读,避免浪费缓存空间。 |
FILE_FLAG_DELETE_ON_CLOSE |
临时文件专用:句柄关闭时自动删除文件(所有其他句柄关闭后才真正删除)。常用于安全存储临时数据。 |
FILE_FLAG_WRITE_THROUGH |
写入操作直接穿透到磁盘 ,不经过延迟写入(Lazy Writer)。相当于每次 WriteFile 都自动 FlushFileBuffers,牺牲性能换取数据可靠性。 |
FILE_FLAG_POSIX_SEMANTICS |
使用 POSIX 风格的命名规则(大小写敏感),非 Windows 默认行为。 |
FILE_FLAG_OPEN_REPARSE_POINT |
打开重解析点(符号链接/挂载点)本身,而不是指向的目标文件。 |
FILE_FLAG_OPEN_NO_RECALL |
用在远程存储(H SM)中,不调回离线文件。 |
经典组合配方:
- 普通读写:
FILE_ATTRIBUTE_NORMAL - 高性能顺序读大文件:
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN - 高性能随机读数据库:
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_RANDOM_ACCESS - 高可靠性写日志:
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH - 极高性能裸 I/O:
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING(需对齐)
2.5 hTemplateFile(模板句柄)
当指定模板句柄时,新创建的文件会继承模板文件的属性和扩展属性(EAs) 。通常传 NULL。
三、文件指针操作:SetFilePointerEx(64 位安全版)
老旧的 SetFilePointer 只能支持 32 位偏移(最大 2GB),在大文件时代必须用 SetFilePointerEx。
cpp
BOOL SetFilePointerEx(
HANDLE hFile,
LARGE_INTEGER liDistanceToMove, // 64 位偏移
PLARGE_INTEGER lpNewFilePointer, // 返回新位置
DWORD dwMoveMethod // FILE_BEGIN, FILE_CURRENT, FILE_END
);
示例:定位到文件第 10GB 处(10 * 1024^3)
cpp
LARGE_INTEGER dist;
dist.QuadPart = 10LL * 1024 * 1024 * 1024;
SetFilePointerEx(hFile, dist, NULL, FILE_BEGIN);
四、读写文件:ReadFile / WriteFile 的隐藏细节
cpp
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
关键细节:
- 返回值
TRUEvsFALSE:返回FALSE时调用GetLastError查看具体错误。如果返回TRUE,表示操作同步完成 ,lpNumberOfBytesRead/Written 即为实际数量。 - 异步模式(
FILE_FLAG_OVERLAPPED) :返回FALSE且GetLastError() == ERROR_IO_PENDING表示操作在后台进行。需要等待OVERLAPPED.hEvent或通过GetOverlappedResult获取结果。 - 读取文件末尾 :
ReadFile返回TRUE但lpNumberOfBytesRead == 0,表示已到 EOF。 - 缓冲区大小限制 :一次
ReadFile/WriteFile最大传输量受限于驱动和系统,一般建议 64KB~1MB 之间,太小会导致系统调用开销大,太大可能导致内存分配失败或触发ERROR_NOT_ENOUGH_MEMORY。
手动刷新缓冲区:
cpp
BOOL FlushFileBuffers(HANDLE hFile);
强制将所有缓存数据写入磁盘。注意:如果开启了 FILE_FLAG_NO_BUFFERING,这个调用是空操作(因为没有缓存)。
五、目录操作与文件遍历
5.1 创建目录
cpp
// 创建单层目录
BOOL CreateDirectoryW(LPCWSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes);
// 创建多层目录(需要手动递归实现,系统不提供 mkdir -p)
// 实战中可以用 SHCreateDirectoryEx 或手动循环
递归创建多层目录示例(自己造轮子):
cpp
BOOL CreateDirectoryRecursive(LPCWSTR path) {
WCHAR dir[MAX_PATH];
wcscpy_s(dir, path);
for (WCHAR* p = dir + 1; *p; p++) {
if (*p == L'\\' || *p == L'/') {
*p = L'\0';
if (!CreateDirectoryW(dir, NULL)) {
DWORD err = GetLastError();
if (err != ERROR_ALREADY_EXISTS) {
return FALSE;
}
}
*p = L'\\';
}
}
return CreateDirectoryW(dir, NULL) || GetLastError() == ERROR_ALREADY_EXISTS;
}
5.2 遍历目录:FindFirstFile / FindNextFile
cpp
WIN32_FIND_DATAW fd;
HANDLE hFind = FindFirstFileW(L"C:\\temp\\*.*", &fd);
if (hFind != INVALID_HANDLE_VALUE) {
do {
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
// 跳过 "." 和 ".."
if (wcscmp(fd.cFileName, L".") != 0 && wcscmp(fd.cFileName, L"..") != 0) {
wprintf(L"[DIR] %s\n", fd.cFileName);
}
} else {
// 显示文件大小(高32位 + 低32位)
ULONGLONG fileSize = ((ULONGLONG)fd.nFileSizeHigh << 32) | fd.nFileSizeLow;
wprintf(L"[FILE] %s (%llu bytes)\n", fd.cFileName, fileSize);
}
} while (FindNextFileW(hFind, &fd));
FindClose(hFind);
}
六、获取与设置文件信息(元数据)
6.1 获取文件属性(快速检查)
cpp
DWORD attrs = GetFileAttributesW(L"C:\\file.txt");
if (attrs == INVALID_FILE_ATTRIBUTES) {
// 文件不存在
} else {
if (attrs & FILE_ATTRIBUTE_DIRECTORY) { /* 是目录 */ }
if (attrs & FILE_ATTRIBUTE_READONLY) { /* 只读 */ }
}
6.2 获取文件大小(64 位安全)
cpp
LARGE_INTEGER size;
if (GetFileSizeEx(hFile, &size)) {
printf("文件大小: %lld 字节\n", size.QuadPart);
}
6.3 获取文件时间(创建/访问/修改)
cpp
FILETIME ftCreate, ftAccess, ftWrite;
GetFileTime(hFile, &ftCreate, &ftAccess, &ftWrite);
// FILETIME 转 SYSTEMTIME(可读格式)
SYSTEMTIME st;
FileTimeToSystemTime(&ftWrite, &st);
wprintf(L"最后修改: %04d-%02d-%02d %02d:%02d:%02d\n",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
注意 :
FILETIME是从 1601-01-01 开始的 100 纳秒间隔计数。FileTimeToSystemTime可以转换。
七、异步 I/O(Overlapped I/O)深度剖析
异步 I/O 是 Windows 高性能 I/O 的基石。它允许线程发起读写请求后立即返回,系统完成后再通知应用程序。
7.1 两种异步通知机制
| 机制 | 适用场景 |
|---|---|
| 事件通知(Event) | 简单的异步操作,用 OVERLAPPED.hEvent 配合 WaitForSingleObject |
| I/O 完成端口(IOCP) | 高并发服务器(如 Web 服务器、数据库),可同时管理数千个异步 I/O |
7.2 事件通知模式
cpp
HANDLE hFile = CreateFileW(L"data.bin", GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, NULL);
OVERLAPPED ov = {0};
ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // 手动重置事件
char buffer[4096];
DWORD dwRead = 0;
// 发起异步读取
if (!ReadFile(hFile, buffer, sizeof(buffer), &dwRead, &ov)) {
if (GetLastError() == ERROR_IO_PENDING) {
// 等待 I/O 完成(最长 5 秒超时)
DWORD wait = WaitForSingleObject(ov.hEvent, 5000);
if (wait == WAIT_OBJECT_0) {
// I/O 完成,获取结果
GetOverlappedResult(hFile, &ov, &dwRead, FALSE);
printf("读取了 %d 字节\n", dwRead);
} else if (wait == WAIT_TIMEOUT) {
// 超时处理:可以取消 I/O
CancelIoEx(hFile, &ov);
}
}
}
CloseHandle(ov.hEvent);
CloseHandle(hFile);
7.3 I/O 完成端口(IOCP)简例
IOCP 是 Windows 上最强大的异步 I/O 模型,通常配合线程池使用。
cpp
// 1. 创建完成端口
HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 2. 将文件句柄绑定到完成端口
CreateIoCompletionPort(hFile, hIOCP, (ULONG_PTR)contextPtr, 0);
// 3. 发起异步读(始终传入 OVERLAPPED)
ReadFile(hFile, buffer, size, NULL, &ov);
// 4. 在工作线程中等待完成
DWORD dwBytes;
ULONG_PTR key;
LPOVERLAPPED pOv;
GetQueuedCompletionStatus(hIOCP, &dwBytes, &key, &pOv, INFINITE);
// 处理完成事件...
八、内存映射文件(File Mapping)
内存映射文件将磁盘文件直接映射到进程虚拟地址空间,适用于大文件随机访问和进程间共享数据。
8.1 核心流程
cpp
HANDLE hFile = CreateFileW(L"bigfile.bin", GENERIC_READ | GENERIC_WRITE, ...);
// 1. 创建文件映射对象
HANDLE hMapping = CreateFileMappingW(
hFile, // 文件句柄
NULL, // 默认安全属性
PAGE_READWRITE, // 保护模式(读/写)
0, 0, // 映射整个文件(高32位/低32位)
NULL // 名称(匿名)
);
// 2. 映射视图到进程地址空间
LPVOID pView = MapViewOfFile(
hMapping,
FILE_MAP_ALL_ACCESS, // 权限
0, 0, // 偏移(必须对齐到系统分配粒度,通常 64KB)
0 // 映射整个文件
);
// 3. 直接像访问内存一样操作
char* pData = (char*)pView;
pData[100] = 'A'; // 修改文件第 100 字节
// 4. 刷新视图到磁盘(可选)
FlushViewOfFile(pView, 0);
// 5. 清理
UnmapViewOfFile(pView);
CloseHandle(hMapping);
CloseHandle(hFile);
8.2 映射超大型文件(> 4GB)
MapViewOfFile 一次最大能映射多少取决于系统虚拟地址空间(32位程序约 2GB,64位程序极大)。对于超大文件,可以分段映射:
cpp
LARGE_INTEGER offset;
offset.QuadPart = 10LL * 1024 * 1024 * 1024; // 10GB 处
// 注意:偏移高32位和低32位必须按系统分配粒度对齐(通常 64KB)
// 实际编码中可能需要先计算对齐
LPVOID pView = MapViewOfFile(hMapping, FILE_MAP_READ,
offset.HighPart, offset.LowPart,
1024 * 1024); // 映射 1MB
九、复制、移动、删除文件
9.1 复制文件
cpp
// 带进度回调的复制(极简版)
BOOL CopyFileW(
LPCWSTR lpExistingFileName,
LPCWSTR lpNewFileName,
BOOL bFailIfExists // TRUE: 目标存在则失败; FALSE: 覆盖
);
// 高级版:CopyFileEx 支持进度回调
9.2 移动/重命名文件
cpp
BOOL MoveFileW(LPCWSTR lpExisting, LPCWSTR lpNew);
// 高级版:MoveFileEx 支持跨卷移动、延迟直到重启
BOOL MoveFileExW(
LPCWSTR lpExisting,
LPCWSTR lpNew,
DWORD dwFlags // MOVEFILE_COPY_ALLOWED, MOVEFILE_REPLACE_EXISTING, MOVEFILE_DELAY_UNTIL_REBOOT
);
跨卷移动 :如果源和目标在不同盘符,必须指定 MOVEFILE_COPY_ALLOWED,否则失败。
9.3 删除文件
cpp
BOOL DeleteFileW(LPCWSTR lpFileName);
注意:如果文件被其他进程以 FILE_SHARE_DELETE 打开,删除操作会将文件标记为"待删除"(删除标记),所有句柄关闭后才真正释放空间。
安全删除目录(递归) :Windows 不提供 rm -rf,需要手动遍历递归删除所有子项,或使用 SHFileOperationW。
十、高级特性:稀疏文件、压缩与重解析点
10.1 稀疏文件(Sparse File)
稀疏文件允许文件有"空洞"(未写入的区域不占用磁盘空间)。常用于虚拟机磁盘文件、数据库。
cpp
// 标记文件为稀疏
DWORD bytes;
DeviceIoControl(hFile, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &bytes, NULL);
// 标记某个范围为稀疏(释放磁盘空间)
FILE_ZERO_DATA_INFORMATION zero = {0};
zero.FileOffset.QuadPart = 1024 * 1024; // 从 1MB 开始
zero.BeyondFinalZero.QuadPart = 10 * 1024 * 1024; // 到 10MB 结束
DeviceIoControl(hFile, FSCTL_SET_ZERO_DATA, &zero, sizeof(zero), NULL, 0, &bytes, NULL);
10.2 文件压缩(NTFS 压缩)
cpp
USHORT compress = COMPRESSION_FORMAT_DEFAULT;
DWORD bytesReturned;
DeviceIoControl(hFile, FSCTL_SET_COMPRESSION,
&compress, sizeof(compress), NULL, 0, &bytesReturned, NULL);
压缩会牺牲 CPU 性能换取磁盘空间。启用后文件在读写时自动解压/压缩。
10.3 符号链接与重解析点(Reparse Point)
cpp
// 创建符号链接(需管理员权限或启用 SeCreateSymbolicLinkPrivilege)
BOOL CreateSymbolicLinkW(
LPCWSTR lpSymlinkFileName,
LPCWSTR lpTargetFileName,
DWORD dwFlags // SYMBOLIC_LINK_FLAG_FILE 或 SYMBOLIC_LINK_FLAG_DIRECTORY
);
检测文件是否为重解析点:
cpp
DWORD attrs = GetFileAttributesW(path);
if (attrs & FILE_ATTRIBUTE_REPARSE_POINT) {
// 是重解析点(符号链接、挂载点、Junction)
}
十一、异常安全与错误处理框架
11.1 结构化异常处理(SEH)
Windows 文件操作可能引发内存访问违规(尤其是裸 I/O 或内存映射文件访问越界)。SEH 可以在 C/C++ 中捕获这些异常。
cpp
__try {
// 操作内存映射文件
pData[1000000000] = 'x'; // 可能越界
}
__except(EXCEPTION_EXECUTE_HANDLER) {
printf("访问违规!错误码: 0x%X\n", GetExceptionCode());
}
11.2 RAII 封装句柄(C++ 风格)
cpp
class WinHandle {
private:
HANDLE h;
public:
WinHandle(HANDLE h = INVALID_HANDLE_VALUE) : h(h) {}
~WinHandle() { if (h != INVALID_HANDLE_VALUE && h != NULL) CloseHandle(h); }
HANDLE get() const { return h; }
operator HANDLE() const { return h; }
// 禁用拷贝,启用移动语义...
};
十二、完整实战示例:高性能日志写入器
cpp
/**
* winapi_advanced_logger.cpp - 使用 Windows API 实现高性能异步日志
* 特性:
* - 异步 I/O(不阻塞主线程)
* - Write-Through(即使崩溃也能保住已提交日志)
* - 文件大小限制,自动滚动
*/
#include <windows.h>
#include <stdio.h>
#include <time.h>
class AsyncLogger {
private:
HANDLE hFile;
HANDLE hIOCP;
OVERLAPPED ov;
char buffer[4096];
DWORD currentSize;
DWORD maxSize;
WCHAR fileName[256];
public:
AsyncLogger(LPCWSTR baseName, DWORD maxSizeMB) : hFile(INVALID_HANDLE_VALUE),
hIOCP(NULL), currentSize(0) {
maxSize = maxSizeMB * 1024 * 1024;
wsprintfW(fileName, L"%s.log", baseName);
// 打开文件:写入、追加、Write-Through
hFile = CreateFileW(fileName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH | FILE_FLAG_OVERLAPPED,
NULL
);
if (hFile == INVALID_HANDLE_VALUE) {
printf("打开日志文件失败: %d\n", GetLastError());
return;
}
// 移动到文件末尾
SetFilePointerEx(hFile, {0}, NULL, FILE_END);
// 创建完成端口
hIOCP = CreateIoCompletionPort(hFile, NULL, 0, 0);
ZeroMemory(&ov, sizeof(ov));
ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
}
~AsyncLogger() {
if (hFile != INVALID_HANDLE_VALUE) {
// 等待所有异步 I/O 完成
FlushFileBuffers(hFile);
CloseHandle(ov.hEvent);
CloseHandle(hFile);
}
if (hIOCP) CloseHandle(hIOCP);
}
void AppendLog(const char* msg) {
if (hFile == INVALID_HANDLE_VALUE) return;
// 获取当前时间
time_t now = time(NULL);
struct tm tm_now;
localtime_s(&tm_now, &now);
int written = sprintf_s(buffer, sizeof(buffer),
"[%04d-%02d-%02d %02d:%02d:%02d] %s\r\n",
tm_now.tm_year + 1900, tm_now.tm_mon + 1, tm_now.tm_mday,
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec,
msg
);
// 异步写入(不等待完成,性能极高)
DWORD dwWritten = 0;
if (!WriteFile(hFile, buffer, written, &dwWritten, &ov)) {
if (GetLastError() != ERROR_IO_PENDING) {
printf("写入失败: %d\n", GetLastError());
}
}
// 检查文件大小,决定是否滚动(实际应用中应在完成回调中处理)
LARGE_INTEGER size;
GetFileSizeEx(hFile, &size);
if (size.QuadPart > maxSize) {
// 简单处理:关闭当前文件,重命名备份,重新创建
// 此处省略详细逻辑...
}
}
// 同步等待所有 I/O 完成(析构前调用)
void Flush() {
FlushFileBuffers(hFile);
}
};
int main() {
AsyncLogger logger(L"app", 10); // 最大 10MB
for (int i = 0; i < 1000; i++) {
char msg[64];
sprintf_s(msg, "日志消息 #%d", i);
logger.AppendLog(msg);
Sleep(10); // 模拟业务逻辑
}
logger.Flush();
printf("日志写入完成,请查看 app.log\n");
return 0;
}
十三、Windows API vs 标准库:深度对比表格
| 对比维度 | Windows API | C 标准库 (fopen) | C++ fstream |
|---|---|---|---|
| 跨平台性 | ❌ 仅 Windows | ✅ 完全跨平台 | ✅ 完全跨平台 |
| Unicode 原生支持 | ✅ UTF-16 原生 | ❌ 依赖 ANSI/UTF-8 转码 | ⚠️ 依赖路径转码 |
| 异步 I/O | ✅ 原生重叠 I/O | ❌ 不支持 | ❌ 不支持 |
| 内存映射文件 | ✅ CreateFileMapping | ❌ 不支持 | ❌ 不支持 |
| 文件共享控制 | ✅ 精细位掩码 | ❌ 不支持 | ❌ 不支持 |
| 字节范围锁定 | ✅ LockFile | ❌ 不支持 | ❌ 不支持 |
| 无缓冲 I/O | ✅ FILE_FLAG_NO_BUFFERING | ❌ 不支持 | ❌ 不支持 |
| 稀疏文件/压缩 | ✅ DeviceIoControl | ❌ 不支持 | ❌ 不支持 |
| 符号链接 | ✅ CreateSymbolicLink | ❌ 不支持 | ❌ 不支持 |
| 错误信息 | 极精确 (GetLastError + FormatMessage) | 模糊 (errno/perror) | 中等 (异常) |
| 性能 (本地磁盘) | 最高 (绕过 CRT 缓冲) | 中等 (带用户态缓冲) | 中等 (带流缓冲) |
| 性能 (网络共享) | 最优 (SMB 直通) | 差 (3-13 倍慢) | 差 |
| 句柄安全性 | 需手动 RAII | 自动 (fclose) | 自动 (析构) |
| 学习曲线 | 极高 | 低 | 低 |
| 代码量 | 大(参数繁多) | 小 | 小 |
| 适用场景 | Windows 高性能/底层/特殊功能开发 | 跨平台轻量级应用 | 现代跨平台 C++ 项目 |
十四、总结:什么时候该用 Windows API?
必须用 Windows API 的场景:
- 需要异步 I/O 或 I/O 完成端口(高并发服务器)。
- 需要内存映射文件(大文件随机访问或进程间共享内存)。
- 需要精细控制共享模式 和字节范围锁。
- 需要无缓冲 I/O 直接控制磁盘(数据库引擎)。
- 操作管道、邮件槽、物理磁盘、控制台等特殊设备。
- 需要稀疏文件、NTFS 压缩、重解析点等 NTFS 高级特性。
坚决用标准库的场景:
- 代码必须跨平台(Linux/macOS/Windows)。
- 简单的配置文件或日志读写。
- 快速原型开发,不想扣底层细节。
一句话结论 :Windows API 是 C/C++ 在 Windows 平台上的"降维打击"工具。用好了,你写的程序可以在同等硬件上压榨出标准库 2~5 倍的 I/O 性能;用坏了,代码会变得晦涩难懂且充满句柄泄漏。我的建议是:普通应用用 fstream,性能瓶颈处用 WinAPI 做局部优化,大型系统从一开始就设计基于 WinAPI 的 I/O 抽象层。