文档背景 一次技术沟通中,发现与我做不同项目的同行,在C++的机制和操作系统 API 上特别有心得,而这是我近两年的工作和学习中用不上,也没有深入学的内容,交流后对我有很大启发,很过瘾。
这些内容解释了很多原理,填补了我的认知空白,也间接指导了我后续的实践,非常开心有这样的交流,后面我拉了个微信群,只拉取了 C++ 开发在里面,把靠谱的 C++ 都拉进群。
术语解释
Dump(转储文件) :进程崩溃时保存的内存快照,可用于事后调试分析
调用约定 :函数参数传递顺序、栈清理责任、名字修饰方式的统一规范
信号槽 :Qt 的一种对象间通信机制,类似于观察者模式
DLL(动态链接库) :Windows 系统的共享库,可在运行时加载
this 指针 :指向当前对象的指针,是成员函数的隐含参数
MD/MT :Visual Studio 的运行时库链接方式
参考资料
《Windows 高级编程》
MSDN: MiniDumpWriteDump
《程序员的自我修养》
1 语言机制 1.1 Windows 怎么收集 dump,单进程崩溃怎么执行,多进程崩溃怎么收集? 基本概念 Dump 文件是进程崩溃时内存状态的快照,包含线程信息、堆栈信息、模块信息等。Windows 提供 DbgHelp.dll 来生成 Dump 文件。
单进程崩溃收集 方法一:使用 SetUnhandledExceptionFilter(代码方式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <windows.h> #include <dbghelp.h> #include <iostream> #include <ShlObj.h> #pragma comment(lib, "dbghelp.lib" ) LONG WINAPI UnhandledExceptionHandler (EXCEPTION_POINTERS* pExceptionInfo) { TCHAR szPath[MAX_PATH]; SHGetFolderPath (NULL , CSIDL_PERSONAL, NULL , 0 , szPath); SYSTEMTIME st; GetSystemTime (&st); TCHAR szFileName[MAX_PATH]; wsprintf (szFileName, TEXT ("%s\\crash_%04d%02d%02d_%02d%02d%02d.dmp" ), szPath, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); HANDLE hFile = CreateFile (szFileName, GENERIC_WRITE, 0 , NULL , CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); if (hFile != INVALID_HANDLE_VALUE) { MINIDUMP_EXCEPTION_INFORMATION mdei; mdei.ThreadId = GetCurrentThreadId (); mdei.ExceptionPointers = pExceptionInfo; mdei.ClientPointers = FALSE; MiniDumpWriteDump (GetCurrentProcess (), GetCurrentProcessId (), hFile, MiniDumpNormal, &mdei, NULL , NULL ); CloseHandle (hFile); std::cout << "Dump saved to: " << szFileName << std::endl; } return EXCEPTION_EXECUTE_HANDLER; } int main () { SetUnhandledExceptionFilter (UnhandledExceptionFilter); int * p = nullptr ; *p = 1 ; return 0 ; }
注意:
EXCEPTION_EXECUTE_HANDLER 表示异常已被处理。对于空指针访问、栈溢出等不可恢复异常,继续执行会导致未定义行为。
实际工程中,写完 dump 后应主动终止进程,例如调用 FatalAppExit(0, L"Application crashed") 或 ExitProcess(1)。
如果希望保留系统默认的崩溃处理(如 WER),可以在写完 dump 后返回 EXCEPTION_CONTINUE_SEARCH。
方法二:使用 Windows Error Reporting(WER)
无需代码,系统自带。崩溃时自动收集,保存在:1 C:\ProgramData\Microsoft\Windows\WER\ReportQueue\
启用方法:注册表 HKLM\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps
多进程崩溃收集 多进程场景(如主进程+子进程)可以由父进程作为调试器监控子进程,但更常见的工程实践是让子进程自己安装异常过滤函数 生成 dump,父进程只负责监控子进程是否异常退出。
方案一:子进程自收集(推荐) 每个子进程在启动时调用 SetUnhandledExceptionFilter,崩溃时自己写 dump。父进程通过 WaitForSingleObject 监控子进程句柄,根据退出码判断是否正常退出。
1 2 SetUnhandledExceptionFilter (UnhandledExceptionHandler);
方案二:父进程作为调试器监控 父进程使用 CreateProcess 配合 DEBUG_PROCESS 标志附加到子进程。收到 EXCEPTION_DEBUG_EVENT 时写 dump,收到 EXIT_PROCESS_DEBUG_EVENT 时退出监控循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <windows.h> #include <dbghelp.h> #include <string> #pragma comment(lib, "dbghelp.lib" ) void WriteMiniDumpForProcess (HANDLE hProcess, DWORD processId) { std::string dumpPath = "child_dump_" + std::to_string (processId) + ".dmp" ; HANDLE hFile = CreateFileA (dumpPath.c_str (), GENERIC_WRITE, 0 , NULL , CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); if (hFile == INVALID_HANDLE_VALUE) return ; MiniDumpWriteDump (hProcess, processId, hFile, MiniDumpNormal, NULL , NULL , NULL ); CloseHandle (hFile); } void CreateAndMonitorProcess (const char * exePath) { STARTUPINFOA si = {sizeof (si)}; PROCESS_INFORMATION pi; if (CreateProcessA (exePath, NULL , NULL , NULL , FALSE, DEBUG_PROCESS, NULL , NULL , &si, &pi)) { DEBUG_EVENT de; BOOL running = TRUE; while (running && WaitForDebugEvent (&de, INFINITE)) { DWORD continueStatus = DBG_CONTINUE; switch (de.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT: if (de.u.Exception.ExceptionRecord.ExceptionCode != EXCEPTION_BREAKPOINT) { WriteMiniDumpForProcess (pi.hProcess, de.dwProcessId); continueStatus = DBG_EXCEPTION_NOT_HANDLED; running = FALSE; } break ; case EXIT_PROCESS_DEBUG_EVENT: running = FALSE; break ; } ContinueDebugEvent (de.dwProcessId, de.dwThreadId, continueStatus); } CloseHandle (pi.hProcess); CloseHandle (pi.hThread); } }
说明: 父进程作为调试器会独占子进程的调试端口,子进程自己无法再使用 SetUnhandledExceptionFilter。实际工程中方案一(子进程自收集)更灵活、dump 信息也更完整。
实际应用建议
场景
推荐方案
说明
客户端应用
SetUnhandledExceptionFilter
用户体验好,可自定义保存路径
服务端/守护进程
父进程监控
子进程崩溃不影响主进程
生产环境
WER + 日志
系统级别,无需额外代码
1.2 函数调用约定是指什么? 基本概念 调用约定(Calling Convention)是函数调用时的一套规则,包括:
参数传递顺序(从左到右 vs 从右到左)
栈清理责任(调用方 vs 被调方)
名字修饰规则(编译器内部标识)
this 指针传递方式(C++ 成员函数)
常见调用约定
调用约定
参数传递
栈清理
名字修饰
备注
__cdecl
右→左压栈
调用方
_functionName
C/C++ 32位默认
__stdcall
右→左压栈
被调方
_functionName@N
Windows API
__fastcall
ECX, EDX → 栈
被调方
@functionName@N
仅 x86
__thiscall
ECX + 栈
被调方
C++ 成员函数修饰(如 ?method@Class@@QAEXXZ)
x86 成员函数默认,不可显式指定
__vectorcall
XMM/YMM 寄存器 + 栈
被调方
与 fastcall/thiscall 类似
x86/x64 SIMD 优化
注意: x64(AMD64)架构下,Windows 只使用一种统一的 Microsoft x64 calling convention ,__cdecl/__stdcall/__fastcall 等关键字在 x64 上会被忽略;GCC/Clang 在 x64 Linux/macOS 上遵循 System V AMD64 ABI 。
代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 int __cdecl cdeclFunc (int a, int b) ; int __stdcall stdcallFunc (int a, int b) ; int __fastcall fastcallFunc (int a) ; #ifdef _MSC_VER #define MYAPI __stdcall #else #define MYAPI __cdecl #endif extern "C" int MYAPI MyExportFunction (int param) ;
实际应用 问题: DLL 导出函数如果约定不匹配,会导致栈错误
1 2 3 4 5 6 7 8 9 extern "C" __declspec(dllexport) int __stdcall Add (int a, int b) { return a + b; } typedef int (__stdcall *AddFunc) (int , int ) ;HMODULE h = LoadLibrary ("mydll.dll" ); AddFunc fn = (AddFunc)GetProcAddress (h, "Add@8" );
1.3 Qt 的信号槽怎么传递数据结构?例如跨线程的时候如何传递? 基本原理 Qt 信号槽默认是同步的(直接调用),跨线程时通过事件队列 异步传递。信号发送时,参数会被拷贝到事件队列,由目标线程的事件循环处理。
传递方式 1 2 3 4 5 6 7 8 9 10 11 emit mySignal (someData) ; emit mySignal (&someData) ; emit mySignal (QSharedPointer<MyData>::create(args)) ;emit mySignal (args) ;
跨线程传递示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 class MyData {public : int id; QString name; QVector<double > values; }; class Worker : public QObject { Q_OBJECT public slots: void doWork (const MyData& data) { qDebug () << "Processing:" << data.name; } signals: void workDone (const MyData& result) ; }; class MainWindow : public QWidget { Q_OBJECT public : void startTask () { MyData data; data.id = 1 ; data.name = "Test" ; data.values = {1.0 , 2.0 , 3.0 }; Qt::ConnectionType type = Qt::QueuedConnection; emit startWorkSignal (data) ; connect (this , &MainWindow::startWorkSignal, m_worker, &Worker::doWork, Qt::QueuedConnection); QMetaObject::invokeMethod (m_worker, "doWork" , Qt::QueuedConnection, Q_ARG (MyData, data)); } signals: void startWorkSignal (const MyData&) ; private : Worker* m_worker; };
关键点总结
场景
处理方式
注意事项
基础类型(int/QString)
自动拷贝
默认即可
自定义类型
需要注册到 Qt 元对象系统
使用 Q_DECLARE_METATYPE
指针/引用
谨慎使用
考虑生命周期
大数据量
移动语义/QSharedPointer
减少拷贝开销
跨线程
Qt::QueuedConnection
自动处理线程切换
重要: 自定义普通结构体/类不需要继承 QObject,但必须注册到 Qt 元对象系统才能跨线程传递。
1 2 3 4 5 6 7 8 9 10 11 12 struct MyData { int id; QString name; QVector<double > values; }; Q_DECLARE_METATYPE (MyData);qRegisterMetaType <MyData>("MyData" );
1.4 怎么加载不同路径下的 dll? 方法一:LoadLibrary 显式加载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <windows.h> typedef int (*AddFunc) (int , int ) ;int main () { HMODULE h = LoadLibraryA ("C:\\mylib\\mydll.dll" ); h = LoadLibraryA (".\\mydll.dll" ); const char * envPath = getenv ("MYDLL_PATH" ); if (envPath) { std::string dllPath = std::string (envPath) + "\\mydll.dll" ; h = LoadLibraryA (dllPath.c_str ()); } if (h) { AddFunc fn = (AddFunc)GetProcAddress (h, "Add" ); if (fn) { int result = fn (1 , 2 ); } FreeLibrary (h); } return 0 ; }
方法二:SetDllDirectory / AddDllDirectory(推荐) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <windows.h> int main () { SetDllDirectoryA ("C:\\mylib\\dlls" ); SetDefaultDllDirectories (LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_USER_DIRS); AddDllDirectory (L"C:\\mylib\\custom_dlls" ); AddDllDirectory (L"C:\\program files\\myapp\\plugins" ); HMODULE h = LoadLibraryA ("mydll.dll" ); return 0 ; }
注意: AddDllDirectory 添加的路径默认不会生效,必须:
进程级调用 SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_USER_DIRS)(Windows 8+);或
在 LoadLibraryEx 中显式指定 LOAD_LIBRARY_SEARCH_USER_DIRS 标志(Windows 7+ 推荐这种方式)。
Windows 7 示例:1 2 DLL_DIRECTORY_COOKIE cookie = AddDllDirectory (L"C:\\mylib\\plugins" ); HMODULE h = LoadLibraryExW (L"mydll.dll" , NULL , LOAD_LIBRARY_SEARCH_USER_DIRS);
方法三:修改可执行文件目录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <windows.h> int main () { char path[MAX_PATH]; GetModuleFileNameA (NULL , path, MAX_PATH); std::string dir = path; size_t pos = dir.rfind ('\\' ); if (pos != std::string::npos) { dir = dir.substr (0 , pos); } std::string dllPath = dir + "\\dlls" ; SetDllDirectoryA (dllPath.c_str ()); return 0 ; }
方法四:延迟加载(Delay Load) Visual Studio 项目属性 → 链接器 → 输入 → 延迟加载的 DLL。
1 2 3 4 5 6 #pragma comment(lib, "delayimp.lib" ) #pragma comment(linker, "/delayload:mydll.dll" ) int result = Add (1 , 2 );
说明: 延迟加载不是不需要 .lib,而是不需要在运行初期就加载 DLL;链接时仍然需要 DLL 对应的导入库 .lib 和 delayimp.lib。
实际应用对比
方法
优点
缺点
注意
绝对路径
明确
不灵活,迁移麻烦
需自行处理路径
SetDllDirectory
简单全局生效
会替换默认 DLL 搜索顺序
不需要管理员权限
AddDllDirectory
安全,推荐
Windows 7+
需配合 SetDefaultDllDirectories 或 LoadLibraryEx 标志
延迟加载
开发简单,按需加载
运行时才报错
仍需要 DLL 的导入库 .lib 和 delayimp.lib
环境变量
用户可配置
需设置环境变量
影响整个进程
1.5 this 指针是怎么传递的? 基本原理 this 指针是成员函数的隐含参数,在 C++ 中通常通过寄存器或栈传递。
不同调用约定的传递方式 1 2 3 4 5 6 7 8 class MyClass {public : void method (int a, int b) ; static void staticMethod (int a) ; };
调用约定
this 传递位置
说明
__thiscall
ECX 寄存器
32位 MSVC 默认
x64 调用约定
RCX 寄存器
64位默认
GCC/Clang
RDI 寄存器
64位 System V ABI
32位 vs 64位 示例 1 2 3 4 5 6 7 class Calculator {public : int add (int a, int b) { return m_value + a + b; } private : int m_value; };
32 位 MSVC(__thiscall)伪汇编示意:1 2 3 4 5 ; this 通过 ECX 传递,参数从右到左压栈 mov ecx, [this] ; this 指针放入 ECX push b push a call Calculator::add ; 被调方清理栈(ret 8)
64 位 MSVC 伪汇编示意:1 2 3 4 5 6 ; this 作为第一个整型参数通过 RCX 传递 mov rcx, [this] ; this 指针放入 RCX mov edx, a ; 第二个参数 RDX mov r8d, b ; 第三个参数 R8 ; 调用者负责分配 32 字节 shadow space ; 调用者负责清理栈
虚函数的 this 调整 1 2 3 4 5 6 7 8 9 10 11 12 13 class Base {public : virtual void vfunc () {} }; class Derived : public Base {public : void vfunc () override {} }; Base* p = new Derived ();
编译器实际生成的调用流程可简化理解为:1 2 3 4 5 6 7 8 p->vfunc (); using VTable = void **; VTable vtable = *(VTable*)p; void (*func)(void *) = (void (*)(void *))vtable[0 ]; func (p);
注意: 多重继承时,this 指针可能需要根据基类子对象偏移进行调整,编译器会在虚表中插入调整 thunk(thunk)来完成。
实际应用 问题: 回调函数如何获取 this?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void callback () { } class MyClass {public : void init () { auto cb = [this ]() { handleCallback (); }; registerCallback (cb); auto cb2 = std::bind (&MyClass::handleCallback, this ); registerCallback (cb2); using Callback = void (*)(MyClass*); Callback fn = &MyClass::staticCallback; fn (this ); } void handleCallback () { } static void staticCallback (MyClass* p) { p->handleCallback (); } };
1.6 32位、64位系统,int、long 是多少字节,哪些数据类型的字节变了?哪些不变?为什么不变? 数据类型字节大小
类型
32位
64位 Windows (LLP64)
64位 Linux/macOS (LP64)
变化?
char
1
1
1
不变
short
2
2
2
不变
int
4
4
4
不变
long
4
4
8
平台相关
long long
8
8
8
不变
float
4
4
4
不变
double
8
8
8
不变
pointer
4
8
8
变化
size_t
4
8
8
变化
ptrdiff_t
4
8
8
变化
变化的根本原因
指针变为 8 字节 :为了支持 64 位地址空间(2^64 = 16EB 寻址能力)
long 变为 8 字节 (仅 Windows):Windows 采用 LLP64 模型,long 保持 4 字节;Linux/macOS 采用 LP64 模型,long 变为 8 字节
LP64 vs LLP64 模型 1 2 3 4 5 6 7 8 LP64 (Linux, macOS, BSD): - long (Long) = 64-bit - pointer = 64-bit LLP64 (Windows): - long long = 64-bit - long = 32-bit (保持兼容) - pointer = 64-bit
代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> #include <cstdint> int main () { std::cout << "sizeof(int): " << sizeof (int ) << std::endl; std::cout << "sizeof(long): " << sizeof (long ) << std::endl; std::cout << "sizeof(long long): " << sizeof (long long ) << std::endl; std::cout << "sizeof(void*): " << sizeof (void *) << std::endl; std::cout << "sizeof(size_t): " << sizeof (size_t ) << std::endl; std::cout << "sizeof(ptrdiff_t): " << sizeof (ptrdiff_t ) << std::endl; int64_t i64 = 123 ; int32_t i32 = 123 ; uint64_t u64 = 456 ; std::vector<int > vec (10 ) ; for (size_t i = 0 ; i < vec.size (); ++i) {} return 0 ; }
实际注意事项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 long addr = (long )p; intptr_t addr = (intptr_t )p; printf ("%zu\n" , size); printf ("%td\n" , diff); printf ("%lld\n" , llval); #include <cinttypes> int64_t value = 123456789 ;printf ("%" PRId64 "\n" , value);struct Data { char c; void * p; };
2 Windows/Visual Studio 相关 2.1 VS 中的 MD、MTD 这些调用选项的区别是什么? 基本概念 MD / MT 是 Visual Studio 的 C/C++ 运行时库(CRT)链接选项 ,命名规则如下:
首字母 M = Multithreaded(多线程版本,现代程序都是这个)。
第二个字母表示链接方式:
D = Dynamic,动态链接到 CRT DLL;
T = 使用静态 LIB 版本的 CRT。
末尾小写 d = Debug 版本(带调试堆、额外检查)。
因此四个常见选项是:
/MD:Release + 动态链接 CRT。
/MDd:Debug + 动态链接 CRT。
/MT:Release + 静态链接 CRT。
/MTd:Debug + 静态链接 CRT。
选项对照表
编译选项
链接方式
典型运行时库
宏定义
特点
/MD
动态链接
vcruntime140.dll、ucrtbase.dll、msvcp140.dll 等
_MT, _DLL
体积小,依赖 VC++ Redistributable
/MT
静态链接
libcmt.lib、libvcruntime.lib、libucrt.lib 等
_MT
体积大,无外部 DLL 依赖
/MDd
动态链接(调试)
vcruntime140d.dll、ucrtbased.dll、msvcp140d.dll 等
_MT, _DLL, _DEBUG
调试版本
/MTd
静态链接(调试)
libcmtd.lib、libvcruntimed.lib、libucrtd.lib 等
_MT, _DEBUG
调试版本,静态链接
注意: VS2015 之后,CRT 被拆分为 vcruntime(VC 运行时)、ucrt(通用 C 运行时)和 msvcp(C++ 标准库)等部分;旧版(VS2015 之前)通常统称为 MSVCRxx.DLL。
代码示例与影响 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #include <stdlib.h> int main () { printf ("Hello, World!\n" ); int * p = (int *)malloc (100 ); if (!p) { return 1 ; } free (p); return 0 ; }
实际选择建议
场景
推荐选项
原因
普通客户端应用
/MD
体积小,多个模块共享 CRT 堆,便于统一更新安全补丁
独立工具/单文件分发
/MT
无需用户安装 VC++ Redistributable
插件/DLL
与宿主程序一致
必须统一,否则跨模块 malloc/free 会崩溃
开发调试
/MDd
与发布版 /MD 运行时一致,避免库混用问题
发布版
/MD 或 /MT
/MD 更主流;/MT 适合对依赖敏感的场景
关键原则: 同一个进程内的所有模块(EXE + 全部 DLL)应使用相同的运行时库选项,尤其是不要混用 /MD 和 /MT。
常见问题 1. 运行时库不匹配
1 2 error LNK2005: "void * __cdecl operator new(unsigned int)" (??2@YAPAXI@Z) 已经在 LIBCMT.lib 中定义
解决:统一所有项目的链接选项
2. DLL 和 EXE 之间的内存分配
1 2 3 4 5 6 7 8 9 void * AllocMem () { return malloc (100 ); } void FreeMem (void * p) { free (p); }
解决:使用统一运行时库,或提供导出函数让 DLL 负责释放
3. 调试版本 vs 发布版本混用
1 MSVCR100D.dll (调试运行时) != MSVCR100.dll (发布运行时)
解决:确保加载的 DLL 与主程序使用相同的运行时库
项目配置位置 Visual Studio 中设置位置:
项目属性 → C/C++ → 代码生成 → 运行时库
或通过命令行: