Windows API 文件操作超级指南——从入门到内核

Windows API 文件操作超级指南------从入门到内核

前言

在上一篇文章中,我们系统性地对比了 C/C++ 在 Linux、Windows、macOS 三平台的文件操作。那篇文章的评论区有位读者问了一个很刁钻的问题:"既然 fopenfstream 都能跨平台,我干嘛还要学 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_READGENERIC_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 写到临时文件,然后用 ReplaceFileMoveFileEx 原子替换。

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
);

关键细节

  1. 返回值 TRUE vs FALSE :返回 FALSE 时调用 GetLastError 查看具体错误。如果返回 TRUE,表示操作同步完成lpNumberOfBytesRead/Written 即为实际数量。
  2. 异步模式(FILE_FLAG_OVERLAPPED :返回 FALSEGetLastError() == ERROR_IO_PENDING 表示操作在后台进行。需要等待 OVERLAPPED.hEvent 或通过 GetOverlappedResult 获取结果。
  3. 读取文件末尾ReadFile 返回 TRUElpNumberOfBytesRead == 0,表示已到 EOF。
  4. 缓冲区大小限制 :一次 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 的场景

  1. 需要异步 I/OI/O 完成端口(高并发服务器)。
  2. 需要内存映射文件(大文件随机访问或进程间共享内存)。
  3. 需要精细控制共享模式字节范围锁
  4. 需要无缓冲 I/O 直接控制磁盘(数据库引擎)。
  5. 操作管道、邮件槽、物理磁盘、控制台等特殊设备。
  6. 需要稀疏文件、NTFS 压缩、重解析点等 NTFS 高级特性。

坚决用标准库的场景

  1. 代码必须跨平台(Linux/macOS/Windows)。
  2. 简单的配置文件或日志读写。
  3. 快速原型开发,不想扣底层细节。

一句话结论 :Windows API 是 C/C++ 在 Windows 平台上的"降维打击"工具。用好了,你写的程序可以在同等硬件上压榨出标准库 2~5 倍的 I/O 性能;用坏了,代码会变得晦涩难懂且充满句柄泄漏。我的建议是:普通应用用 fstream,性能瓶颈处用 WinAPI 做局部优化,大型系统从一开始就设计基于 WinAPI 的 I/O 抽象层。

相关推荐
七牛云行业应用3 小时前
Ollama 本地部署 DeepSeek 完全指南:macOS / Windows / Linux 三端安装 + GPU 配置 + API 调用
linux·windows·macos
Code Man3 小时前
Windows 下使用 Appium
android·windows·appium
zhangfeng11333 小时前
Ascendc 大语言模型框架 AscendCraft 和pypto 的区别 pypto生成算子c++源码吗
c++·人工智能·语言模型·算子开发
一只小灿灿3 小时前
C++ 修饰符全面详解
开发语言·c++
WangWei_CM4 小时前
Active Setup:按用户首次登录执行的组件初始化
windows
code_pgf4 小时前
C/C++ 中 `typedef` 关键字详解
c语言·c++
会周易的程序员4 小时前
js-shm: 高性能 Node.js 共享内存模块
开发语言·javascript·c++·node.js·共享内存·shm
2023自学中5 小时前
imx6ull 开发板 贪吃蛇, C++11 SDL2 无硬件GPU优化版
linux·c++
anscos5 小时前
为CUDA 代码引入静态分析
c++·工业软件·功能检测