简介《精通Windows API——函数、接口、编程实例(源码)》配套源码包面向已具备C基础、希望深入Win32桌面开发与系统级编程的开发者。内容围绕窗口管理、消息循环、内存与文件操作、动态库加载及COM接口等核心主题展开通过156个可运行实例把API函数、接口规范与工程实践串联起来适合作为查阅手册和动手实验素材。压缩包为rar格式共1789个文件、约24.29MB包含c/cpp/h源码、vcproj与sln工程、exe示例程序以及pdb、ilk调试符号和manifest、rc、res等资源文件另有htm说明与dll、lib依赖目录按示例组织便于按主题检索和编译调试。已有376人学习。读者可对照源码理解CreateWindowEx、SendMessage、GetMessage、LoadLibrary/GetProcAddress等典型调用也能从GUI、文件操作、多线程与进程通信案例中积累排错思路提升Windows平台开发与调试能力。1. 一个卡死的老上位机让我重新翻起了 Win32 手册接手一个跑了七八年的工控上位机界面每隔几小时假死一次日志文件写到 4GB 就整个进程崩掉。把代码拉下来一看没有框架、没有运行时封装一个wWinMain加两千多行WndProc全是裸的 Windows API 调用。这类代码最难受的不是难懂而是没有参考物CreateFileMapping的大小参数传 0 到底代表什么、GetMessage返回 -1 要不要退出、WM_PAINT里能不能再触发一次重绘全靠猜和试。这份带源码的《精通Windows API-函数、接口、编程实例》补的恰好就是这块空白它把 Win32 C 里最容易含糊过去的函数原型、参数语义和典型调用序列摊开每个接口配一个能编译、能跑、能改的样例。适合已经会写 C、但一碰句柄、消息、内存映射就发怵的人也适合要长期维护存量 Win32 代码、需要一份“参数速查 可跑样例”的从业者。2. Win32 的调用契约先把 WinMain 到消息循环跑通Win32 编程的门槛从来不在语法而在一堆没写进函数签名的隐含规则哪个参数必须非空、哪个句柄必须成对释放、哪个返回值不是 0 就代表失败。源码包里的例子之所以能跑是因为这些规则被作者用正确的方式写进去了照着抄却跑不起来八成是某条规则被漏掉了。所以第一步不是急着翻目录看有哪些例子而是先把最小骨架跑通把规则摸清楚。2.1 最小可运行骨架注册窗口类、建窗口、跑消息泵任何 Win32 图形程序都是同一个三段式结构。下面这段六十行的代码是所有例子的公共底座先把它编译通过后面拆模块才有意义。// minimal_win32.cpp // 编译cl /EHsc /std:c17 /DUNICODE /D_UNICODE minimal_win32.cpp user32.lib gdi32.lib #include windows.h LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); // 投递 WM_QUIT消息泵收到 0 后退出 return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); // 必须与 EndPaint 成对否则重绘会无限循环 EndPaint(hWnd, ps); return 0; } default: return DefWindowProcW(hWnd, msg, wParam, lParam); // 未处理的消息交回系统 } } int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrev, PWSTR cmdLine, int nShow) { WNDCLASSEXW wc { sizeof(WNDCLASSEXW) }; // cbSize 必须是第一个赋值的字段 wc.lpfnWndProc WndProc; wc.hInstance hInst; wc.lpszClassName LMinimalWnd; wc.hCursor LoadCursorW(nullptr, IDC_ARROW); wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); // 系统色要用 1 偏移 if (!RegisterClassExW(wc)) return 1; HWND hWnd CreateWindowExW(0, wc.lpszClassName, Ldemo, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, nullptr, nullptr, hInst, nullptr); if (!hWnd) return 2; ShowWindow(hWnd, nShow); UpdateWindow(hWnd); MSG msg; while (GetMessageW(msg, nullptr, 0, 0) 0) { // 返回 0 退出-1 表示出错 TranslateMessage(msg); // 把 WM_KEYDOWN 翻译成 WM_CHAR DispatchMessageW(msg); // 分发到 WndProc } return (int)msg.wParam; }这段代码有三处值得单独说清楚。wc.cbSize如果不赋值RegisterClassExW会直接返回 0GetLastError()给出ERROR_INVALID_PARAMETER新手最容易在这里卡半天。hbrBackground用系统色时必须写成COLOR_WINDOW 1因为CreateSolidBrush之外的常量是索引加 1 才落在画刷句柄区间。GetMessageW的循环条件写成 0而不是! 0是因为它出错时返回 -1若用! 0判断会进入死循环——这条在长时间运行的服务型界面里是真会翻车的。参数层面CreateWindowExW第一个参数是扩展样式做无边框、置顶、透明时才有用常规窗口传 0CW_USEDEFAULT只对 overlapped 窗口的 x/y 有效width/height 传它没有意义最后那个lpParam会以WM_CREATE的lParam形式进入窗口过程多窗口共用一个WndProc时靠它区分实例。2.2 字符集TCHAR、_T() 与 W/A 后缀函数的真实关系源码包里几乎每个字符串都套着_T()每个函数名都能看到W或A后缀这不是风格问题是编译期开关。Win32 里凡是接收字符串的 API实际上都有两个版本CreateFile本身只是个宏展开成CreateFileW还是CreateFileA取决于有没有定义UNICODE。源码写法定义 UNICODE 后未定义 UNICODE 时说明CreateFile(TEXT(a.txt), ...)CreateFileWCreateFileA通用写法随宏切换CreateFileW(La.txt, ...)CreateFileWCreateFileW强制宽字符不受宏影响CreateFileA(a.txt, ...)CreateFileACreateFileA强制窄字符中文路径会乱码_T(text)/TEXT(text)Ltexttext字面量跟随宏结论很简单新写的代码一律显式用W版本和L字面量把UNICODE、_UNICODE两个宏一起定义死别依赖_T()的自动切换。理由不是“宽字符更先进”而是窄字符版本走的是系统 ANSI 代码页一旦路径里出现非当前代码页能表示的字符CreateFileA会失败得毫无提示——报错码可能还是ERROR_FILE_NOT_FOUND看上去像文件不存在实际是编码转不过去。源码包里如果有混用的情况改造顺序是先全局加-DUNICODE -D_UNICODE然后编译把所有LNK2019和C2664挨个修。别反向操作去掉宏去迁就老代码那样只是把问题推后。2.3 调用约定与句柄WINAPI、__stdcall 与 HANDLE 到底是什么WINAPI在 32 位下展开为__stdcall含义是参数从右往左压栈、由被调用方清理栈在 x64 下这个修饰被忽略因为统一走一套调用约定。它存在的意义是回调函数的二进制兼容窗口过程、线程入口、EnumWindows的回调都必须带这个修饰否则 32 位下栈会失衡程序在回调返回时崩掉报错位置看起来毫无关系。HANDLE则是一个不透明指针void*的 typedef。这里有个反直觉的细节空句柄有两种NULL值为 0和INVALID_HANDLE_VALUE值为(HANDLE)-1。不同 API 用不同的失败值——CreateFileW失败返回INVALID_HANDLE_VALUECreateWindowExW失败返回NULLCreateThread失败返回NULL。判断错的那一个失败分支永远进不去后面拿到一个 -1 的句柄去读写GetLastError()会给出ERROR_INVALID_HANDLE但你根本不知道是哪个调用出的问题。一个稳妥的写法是用返回值本身做判断而不是和某一个常量比较HANDLE hFile CreateFileW(Ldata.bin, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile INVALID_HANDLE_VALUE) { // CreateFile 系列特殊 DWORD err GetLastError(); // err ERROR_FILE_NOT_FOUND / ERROR_ACCESS_DENIED ... } HANDLE hThread CreateThread(nullptr, 0, Worker, ctx, 0, nullptr); if (hThread nullptr) { // CreateThread 系列用 NULL DWORD err GetLastError(); }线程入口函数的签名也要严格遵守DWORD WINAPI Worker(LPVOID)返回值和参数类型差一点在 x64 下可能侥幸编译通过、运行时栈错位。这类问题没有报错只有随机崩溃属于典型的玄学现场。3. 把源码包编译起来工程配置、链接顺序与产物验证拿到带源码的示例集第一件事永远不是读代码而是让它在你本机上编出一个能双击运行的 exe。编不出来之前代码读得再细也没法验证理解是否正确。这一章按“先看清目录结构、再统一构建脚本、最后处理链接错误”的顺序推。3.1 先摸清目录结构头文件、库和示例的分层这类书配套源码通常分成三层公共头文件、按章节组织的示例源文件、以及可选的第三方或系统库目录。花五分钟列个清单比后面到处找#include找不到要省事得多。目录典型内容处理方式include/公共结构体定义、宏、辅助函数声明加进头文件搜索路径src/ch02…src/ch15按章组织的.cpp多数含独立main每个 cpp 编成一个 exelib/预编译静态库若有加进链接器输入res/图标、对话框模板.rc资源脚本一并编进目标bin/运行时依赖的 dll、数据文件复制到输出目录资源脚本.rc容易被漏掉。漏了的话编译能过运行起来图标是默认白纸、对话框创建失败返回NULLGetLastError()给出ERROR_RESOURCE_TYPE_NOT_FOUND。看到这个码先回去检查.rc有没有参与编译。3.2 用 CMake 统一两套工具链别手点工程文件如果包里带的是 IDE 工程文件直接在 IDE 里打开也能编但几百个示例一个个手动配置不现实而且换人接手时环境又得重来一遍。写个CMakeLists.txt批量扫描一条命令出全部产物是更省事的路子。cmake_minimum_required(VERSION 3.20) project(win32_samples CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 统一字符集让源码里的 _T() / TEXT() 一律展开为宽字符版本 add_compile_definitions(UNICODE _UNICODE) # 关掉 min/max 宏否则和 std::min/std::max 冲突同时瘦身 windows.h add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN) # 老代码里 strcpy / fopen 一大片先关掉安全告警别一开始就被淹 add_compile_definitions(_CRT_SECURE_NO_WARNINGS) file(GLOB SAMPLE_SRC CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/src/ch*/*.cpp) foreach(src ${SAMPLE_SRC}) get_filename_component(name ${src} NAME_WE) add_executable(${name} ${src}) # 按需增减advapi32 是注册表shell32 是外壳接口comctl32 是公共控件 target_link_libraries(${name} PRIVATE user32 gdi32 advapi32 shell32 comctl32) endforeach()逻辑上就三步全局宏统一编译环境、file(GLOB)批量收集源文件、循环里给每个示例单独add_executable再挂库。CONFIGURE_DEPENDS让新增的 cpp 不用重新跑 cmake 也能被扫到改示例时很省事。参数部分要留意三点。WIN32_LEAN_AND_MEAN会把windows.h里一部分不常用的头比如 winsock剔出去体积小、编译快但如果某个示例用到了winsock2.h就必须在windows.h之前手动 include否则会撞重复定义。NOMINMAX是为了避免min/max宏把std::min替换掉报错信息会变成一堆看不懂的C2589。_CRT_SECURE_NO_WARNINGS只是把告警压下去不代表那些strcpy没问题后面做代码改造时要单独处理。不带 CMake 想单编一个文件命令行这样写# 在 x64 Native Tools 环境下 cl /EHsc /std:c17 /DUNICODE /D_UNICODE /DNOMINMAX /W3 ^ src\ch04\file_map_demo.cpp ^ /Fe:file_map_demo.exe ^ user32.lib gdi32.lib advapi32.lib/EHsc打开标准 C 异常模型Visual C 下不加它try/catch里 throw 出的异常不会触发析构句柄就漏了。/W3是告警级别调到/W4会冒出一堆老代码的类型转换告警第一次上手建议先留在/W3。3.3 链接失败的三类报错与 dumpbin 验证Win32 项目的链接错误来来回回就三类认清楚了基本能自己定位。第一类是LNK2019 unresolved external symbol。九成是漏了 lib用了RegOpenKeyExW没加advapi32.lib用了ShellExecuteW没加shell32.lib。剩下的可能是字符集不匹配——源码里定义了wWinMain编译时却带着/D_UNICODE以外的配置链接器会去找WinMain报的符号名和你写的不一样一眼能看出来是宽窄字符问题。第二类是LNK2005 已经在 xxx.obj 中定义。常见于混了静态 CRT一个库用/MT编的主程序用/MD编的malloc家族符号重复。解决方式统一运行时库选项不要试图用/FORCE:MULTIPLE硬压过去那个开关留下的是内存管理器的双重堆运行久了直接崩。第三类是LNK1104 cannot open file xxx.lib。路径不对或者库名拼错检查链接器附加库目录注意 x86 和 x64 的库目录是分开的混着引会在链接阶段报机器类型不匹配。编出来之后别急着跑先用dumpbin看一眼依赖确认没有意外引用dumpbin /dependents file_map_demo.exe输出里应该只有KERNEL32.dll、USER32.dll、GDI32.dll、ADVAPI32.dll这类系统组件。如果冒出来一个VCRUNTIME140.dll而目标机器上没装运行时双击就是“缺少 dll”弹窗。要发给别人用编译时改静态链接CMake 里设set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$$CONFIG:Debug:Debug)命令行加/MT。4. 四类高频接口拆开看窗口、文件映射、进程线程、注册表源码包覆盖面通常很广但真到工程里天天用的无非这四类。这一章每类挑一个能直接改吧改吧就用起来的片段重点讲“为什么这么写”而不是“函数有几个参数”。4.1 窗口与 GDIWM_PAINT 里做双缓冲的正确姿势直接在WM_PAINT里往窗口 DC 上画窗口一拖动就闪成一片。原因是系统先发WM_ERASEBKGND刷背景再让你画内容两个动作之间的时间差肉眼可见。标准解法是在内存 DC 里画完整帧再一次性BitBlt过去。case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); RECT rc; GetClientRect(hWnd, rc); // 1. 建一个和窗口 DC 兼容的内存 DC HDC memDC CreateCompatibleDC(hdc); // 2. 建一张和客户区等大的位图挂到内存 DC 上 HBITMAP bmp CreateCompatibleBitmap(hdc, rc.right, rc.bottom); HGDIOBJ oldBmp SelectObject(memDC, bmp); // 3. 所有绘制都在 memDC 上做 FillRect(memDC, rc, (HBRUSH)GetStockObject(WHITE_BRUSH)); SetBkMode(memDC, TRANSPARENT); TextOutW(memDC, 20, 20, Ldouble buffer, 13); // 4. 一次性拷贝到屏幕 BitBlt(hdc, 0, 0, rc.right, rc.bottom, memDC, 0, 0, SRCCOPY); // 5. 逆序回收顺序错了会 GDI 泄漏 SelectObject(memDC, oldBmp); DeleteObject(bmp); DeleteDC(memDC); EndPaint(hWnd, ps); return 0; }关键在最后三步的回收顺序必须先把原来的位图SelectObject回去才能DeleteObject新位图否则那张位图还被 DC 选着删除会失败句柄表里就一直挂着一个 GDI 对象。GDI 对象有进程级配额默认一万个泄漏几百个之后所有绘图 API 开始返回NULL界面变成一片灰任务管理器里 GDI 对象数会稳定上涨——这是判断 GDI 泄漏最直接的信号。CreateCompatibleBitmap(hdc, ...)里第一个参数必须传窗口 DC 而不是内存 DC。传内存 DC 的话建出来的是 1bpp 单色位图画上去全是黑白块这个坑我见过不止一次。按上面写法每次重绘都新建位图对低频重绘的场景够用如果窗口大、刷新频繁位图应该在窗口创建时建一次缓存在结构体里WM_SIZE时重建。源码包里的例子多半是每次新建那是为了代码短工程里别照抄。4.2 文件映射CreateFileMapping 的页对齐与生命周期要读一个几 GB 的日志文件用ReadFile分块读也行但解析逻辑会被缓冲区切分搞得很难写。文件映射把整个文件当成一块内存指针加偏移就能访问代码干净得多。HANDLE hFile CreateFileW(Lbig.log, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile INVALID_HANDLE_VALUE) return; LARGE_INTEGER fileSize{}; GetFileSizeEx(hFile, fileSize); // 第 4、5 个参数是 64 位大小的高低位传 0,0 表示与文件等长 HANDLE hMap CreateFileMappingW(hFile, nullptr, PAGE_READONLY, 0, 0, nullptr); if (!hMap) { CloseHandle(hFile); return; } // 偏移必须按 64KB 对齐一次映射全文件就传 0 const char* base (const char*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); if (!base) { CloseHandle(hMap); CloseHandle(hFile); return; } // ... 按 base offset 访问注意别越界到 fileSize.QuadPart 之外 UnmapViewOfFile(base); CloseHandle(hMap); CloseHandle(hFile); // hFile 要最后关映射视图还活着时关文件句柄会失败CreateFileMappingW的大小参数是“高 32 位 低 32 位”两段传0, 0表示跟随文件实际长度。如果只想映射前 1MB传0, 1024*1024但要注意这之后对文件的写入不能超过映射大小超了会触发EXCEPTION_IN_PAGE_ERROR不是返回值报错是直接异常。MapViewOfFile的偏移必须 64KB 对齐这是分配粒度决定的。想从第 100 字节开始看只能整块映射之后用base 100取指针不能直接传 100 当偏移——传了会得到ERROR_MAPPING_ALIGNMENT而这个错误在老文档里常常没写清楚。还有个容易忽略的点映射视图是对文件的共享内存视图不是快照。别的进程往文件里写你这边读到的内容会变。要保证一致性得自己加锁LockFileEx或者命名互斥体。做多进程日志收集时很多人以为是“读到了一个副本”结果解析到一半数据变了日志里出现半行记录回头查半天。4.3 进程与线程CreateProcess 的命令行缓冲与等待启动外部程序并等它结束看着简单但CreateProcessW有个反直觉要求lpCommandLine必须是可写缓冲区不能传字符串字面量。wchar_t cmdLine[] Ltool.exe --input data.bin; // 必须可写不能是 L... STARTUPINFOW si { sizeof(si) }; PROCESS_INFORMATION pi{}; BOOL ok CreateProcessW( nullptr, // 应用程序名为 nullptr 时从 cmdLine 里解析 cmdLine, // 会被就地修改所以不能是 const nullptr, nullptr, // 进程/线程安全属性 FALSE, // bInheritHandles是否需要继承句柄 0, // 创建标志CREATE_NO_WINDOW 可隐藏控制台 nullptr, // 环境块nullptr 表示继承 nullptr, // 工作目录 si, pi); if (!ok) { /* GetLastError() */ } // 等它结束超时 30 秒 DWORD wr WaitForSingleObject(pi.hProcess, 30000); if (wr WAIT_TIMEOUT) { TerminateProcess(pi.hProcess, 1); // 超时强杀注意这是最后手段 } DWORD exitCode 0; GetExitCodeProcess(pi.hProcess, exitCode); CloseHandle(pi.hThread); // 两个句柄都要关 CloseHandle(pi.hProcess);传字面量的后果是进程启动后系统在解析命令行时往只读页写数据触发访问违例崩溃点看起来在系统库内部非常难查。改成wchar_t数组就没事。bInheritHandles传 TRUE 时要特别小心所有可继承句柄都会被复制进子进程包括你不希望它拿到的。真要传管道给子进程正确做法是用STARTUPINFOEXW配合PROC_THREAD_ATTRIBUTE_HANDLE_LIST精确指定而不是图省事传 TRUE。做命令行工具封装时这个细节决定了子进程能不能拿到父进程的文件句柄。4.4 注册表与系统信息RegOpenKeyEx 的权限与 32/64 位视图读注册表本身不难难在两点权限和视图。前者让你ERROR_ACCESS_DENIED后者让你明明存在的东西读不到。HKEY hKey nullptr; // KEY_READ 是最小权限要写就换成 KEY_SET_VALUE // KEY_WOW64_64KEY 强制读 64 位视图避免被重定向到 Wow6432Node LONG ret RegOpenKeyExW(HKEY_LOCAL_MACHINE, LSOFTWARE\\SomeVendor\\SomeProduct, 0, KEY_READ | KEY_WOW64_64KEY, hKey); if (ret ! ERROR_SUCCESS) { // ERROR_FILE_NOT_FOUND键不存在或视图不对 // ERROR_ACCESS_DENIED权限不足 return; } wchar_t value[512]; DWORD cb sizeof(value); DWORD type 0; ret RegQueryValueExW(hKey, LInstallPath, nullptr, type, (LPBYTE)value, cb); if (ret ERROR_SUCCESS type REG_SZ) { // value 里是安装路径注意 cb 是字节数不是字符数 } RegCloseKey(hKey);权限方面HKEY_LOCAL_MACHINE下的写操作在普通用户令牌下必然失败读操作也有部分子键受限。程序启动时申请提权会让用户看到一个弹窗这在无人值守场景里是硬伤所以读多写少的配置项尽量放HKEY_CURRENT_USER。视图方面32 位程序默认会被重定向到Wow6432Node64 位程序默认读 64 位视图。加KEY_WOW64_64KEY或KEY_WOW64_32KEY可以强制指定这个标志在排查“明明装了这个软件却读不到注册表”时是第一步要确认的。RegQueryValueExW的cb是字节数REG_SZ要除以sizeof(wchar_t)才是字符数直接拿它当长度去wcslen会读到一堆尾部乱码。这类细节源码包里都有例子对照值得跑一遍看输出。5. 避坑与排查五条血泪记录示例代码在干净环境里跑通不代表工程里能用。下面这五条是实际维护 Win32 代码时反复撞上的每条都按“看到什么现象、根子在哪、怎么修”的顺序写。5.1 句柄数只涨不跌任务管理器里的长期指标现象程序跑八九个小时后任务管理器里“句柄”数从 300 涨到 8000 多然后CreateFile开始返回失败报ERROR_TOO_MANY_OPEN_FILES。原因某个错误分支提前return了跳过了CloseHandle。Win32 没有 RAII所有句柄都是手动生命周期if (err) return;这种写法用一次漏一次。解决给每类句柄包一层析构函数里CloseHandle的小结构体或者至少把清理逻辑提到单一出口。临时排查可以在CreateFile前后打日志配个计数器看哪个路径进来的多。struct ScopedHandle { HANDLE h{ INVALID_HANDLE_VALUE }; ~ScopedHandle() { if (h ! INVALID_HANDLE_VALUE h ! nullptr) CloseHandle(h); } operator HANDLE() const { return h; } };用的时候ScopedHandle fh{ CreateFileW(...) }函数任何地方return都会自动关。注意CreateFileW的失败值是INVALID_HANDLE_VALUE不是NULL析构里两个都要排掉。5.2 界面无响应在 WndProc 里等一个需要消息的调用现象点了按钮之后窗口整个变白标题栏显示“未响应”点关闭也没反应只能任务管理器结束进程。原因在WM_COMMAND的处理函数里直接调了WaitForSingleObject等一个工作线程而那个工作线程完成任务后需要用SendMessage通知主线程——SendMessage是跨线程同步的等主线程处理主线程又在等工作线程直接死锁。解决所有阻塞操作移出WndProc。要么起工作线程完成时PostMessage异步投递不等待要么在主线程里用MsgWaitForMultipleObjects边等边抽消息。判断标准很简单WndProc里的代码执行时间不能超过几十毫秒超了就该移出去。5.3 编译报 LNK2019 找不到 WinMain现象源码里明明写的是wWinMain链接器报unresolved external symbol WinMain或者反过来找wWinMain。原因字符集宏和入口函数不匹配。定义了UNICODE但没定义_UNICODE或者链接器子系统选项/SUBSYSTEM:CONSOLE与/SUBSYSTEM:WINDOWS搞反了。控制台子系统会去找main/wmain窗口子系统才找WinMain/wWinMain。解决UNICODE和_UNICODE永远成对定义/SUBSYSTEM:WINDOWS与wWinMain配对。用 CMake 的话add_executable(name WIN32 ...)里那个WIN32就是设子系统的别漏。5.4 文件映射跨进程读到半行数据现象多进程往同一个日志文件写另一个进程映射读偶尔解析出一行没写完的记录或者内容对不上。原因文件映射提供的是共享视图不是快照。写端还没写完读端已经看到了部分数据而且没有内存屏障保证顺序。解决要么用LockFileEx做区域锁读写前先锁字节范围要么改成“写临时文件 原子改名”的模式读端只认最终文件名。第二种在日志场景更省事代价是文件句柄数量多一些。5.5 提权后启动的程序读不到用户配置现象程序加了个requireAdministrator清单双击弹 UAC提权之后原来能读的HKEY_CURRENT_USER配置全没了界面变成默认值。原因提权后进程跑在管理员账户的安全上下文里HKEY_CURRENT_USER指向的是管理员那个用户配置不是当前登录用户。同样的问题也出现在映射网络盘、读取用户目录文件上。解决配置要么放HKEY_LOCAL_MACHINE的公共位置要么在提权前把当前用户的 SID 和配置路径读出来通过命令行或临时文件传给提权后的进程。不要指望提权后还能自动找到原来的用户环境。6. 进阶用一层薄封装把源码包里的例子串成可复用的调试脚手架把示例跑通只是第一步。真正在工程里用起来缺的是一层统一的错误处理和句柄管理否则每个示例都是孤岛出问题时只能靠printf猜。我的做法是给源码包里的每个例子加两个小工具一个统一错误信息输出一个 RAII 句柄包装然后把这层抽成一个公共头文件。// win32_util.h —— 放在公共头目录所有示例 include 它 #pragma once #include windows.h #include string // 把 GetLastError() 的数值翻译成可读描述 inline std::wstring LastErrorText(DWORD code GetLastError()) { wchar_t* buf nullptr; DWORD n FormatMessageW( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, code, 0, (LPWSTR)buf, 0, nullptr); std::wstring s (n buf) ? std::wstring(buf, n) : Lunknown error; if (buf) LocalFree(buf); while (!s.empty() (s.back() L\r || s.back() L\n)) s.pop_back(); return s; } // 只在失败时打印成功的路径不产生任何输出 #define WIN32_CHECK(expr, what) \ do { \ if (!(expr)) { \ OutputDebugStringW((std::wstring(L[FAIL] ) what \ L - LastErrorText()).c_str()); \ } \ } while (0)FormatMessageW的三面旗标各有作用ALLOCATE_BUFFER让系统自己分配字符串用完必须LocalFreeFROM_SYSTEM表示从系统消息表里查IGNORE_INSERTS防止消息体里的%1占位符没被替换时报警。去掉尾部换行是因为系统返回的描述自带\r\n拼进日志会多一个空行。OutputDebugStringW只在调试器挂载时可见用调试器或者一个简单的调试输出捕获工具就能看到。相比printf它的好处是不影响控制台窗口的显示节奏也不会在 Release 下留一堆空输出。宏包一层do { } while (0)是为了能在if后面直接当语句用不带这个包裹的宏在if...else里会展开错。把这层挂到示例上只要改一处所有 Win32 调用外面套WIN32_CHECK失败时立刻能看到函数名、错误码和系统描述。之前那个“窗口创建失败返回 2”的骨架改成打印具体错误之后ERROR_INVALID_PARAMETER和ERROR_NOT_ENOUGH_MEMORY就能一眼分开不用再靠改代码逐个试。验证这层封装有没有生效最快的办法是故意制造一次失败把CreateFileW的路径改成一个不存在的目录看调试输出里是不是准确打印出[FAIL] CreateFileW - 系统找不到指定的路径。。如果打出来的是unknown error说明FormatMessageW没拿到消息通常是错误码在调用之前被别的 API 覆盖了——GetLastError()必须在失败之后立刻取中间插一个OutputDebugString都会被清掉。从那次排查一个只在客户现场复现的句柄泄漏之后我每次拿到新的 Win32 示例包都强制先把这两件小工具加进公共头再开始读代码。不是因为严谨是因为吃过亏没有错误描述的时候GetLastError()返回的只是一个数字而人脑对数字是没有记忆的。希望帮到你。本文还有配套的精品资源点击获取