日期
版本
作者
变更
2025-12-18
V1.0
Lex
初稿
文档背景 一次技术沟通中,发现与我做不同项目的同行,在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 ; }
方法二:使用 Windows Error Reporting(WER)
无需代码,系统自带。崩溃时自动收集,保存在:1 C:\ProgramData\Microsoft\Windows\WER\ReportQueue\
启用方法:注册表 HKLM\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps
多进程崩溃收集 多进程场景(如主进程+子进程)需要父进程监控子进程:
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 #include <windows.h> #include <tlhelp32.h> #include <dbghelp.h> 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; while (WaitForDebugEvent (&de, INFINITE)) { if (de.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) { HANDLE hProcess = OpenProcess (PROCESS_ALL_ACCESS, FALSE, de.dwProcessId); if (hProcess) { char dumpPath[MAX_PATH]; sprintf (dumpPath, "child_dump_%d.dmp" , de.dwProcessId); HANDLE hFile = CreateFileA (dumpPath, GENERIC_WRITE, 0 , NULL , CREATE_ALWAYS, 0 , NULL ); if (hFile != INVALID_HANDLE_VALUE) { MiniDumpWriteDump (hProcess, de.dwProcessId, hFile, MiniDumpNormal, NULL , NULL , NULL ); CloseHandle (hFile); } CloseHandle (hProcess); } } ContinueDebugEvent (de.dwProcessId, de.dwThreadId, DBG_CONTINUE); } CloseHandle (pi.hProcess); CloseHandle (pi.hThread); } }
实际应用建议
场景
推荐方案
说明
客户端应用
SetUnhandledExceptionFilter
用户体验好,可自定义保存路径
服务端/守护进程
父进程监控
子进程崩溃不影响主进程
生产环境
WER + 日志
系统级别,无需额外代码
1.2 函数调用约定是指什么? 基本概念 调用约定(Calling Convention)是函数调用时的一套规则,包括:
参数传递顺序(从左到右 vs 从右到左)
栈清理责任(调用方 vs 被调方)
名字修饰规则(编译器内部标识)
this 指针传递方式(C++ 成员函数)
常见调用约定
调用约定
参数传递
栈清理
名字修饰
备注
__cdecl
右→左
调用方
_functionName
C/C++ 默认
__stdcall
右→左
被调方
_functionName@N
Windows API
__fastcall
ECX, EDX → 栈
被调方
@functionName@N
快速调用
__thiscall
ECX + 栈
被调方
C++ 编译器决定
成员函数默认
__vectorcall
XMM 寄存器
被调方
functionName@@
SIMD 优化
代码示例 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 54 class MyData {public : int id; QString name; QVector<double > values; Q_OBJECT }; 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
自动处理线程切换
重要: 自定义类型需要注册:
1 2 3 4 5 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 #include <windows.h> int main () { SetDllDirectoryA ("C:\\mylib\\dlls" ); AddDllDirectory (L"C:\\mylib\\custom_dlls" ); AddDllDirectory (L"C:\\program files\\myapp\\plugins" ); HMODULE h = LoadLibraryA ("mydll.dll" ); return 0 ; }
方法三:修改可执行文件目录 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 #pragma comment(linker, "/delayload:mydll.dll" ) int result = Add (1 , 2 );
实际应用对比
方法
优点
缺点
绝对路径
明确
不灵活,迁移麻烦
SetDllDirectory
简单全局生效
需管理员权限修改
AddDllDirectory
安全,推荐
Windows 7+
延迟加载
开发简单
运行时才报错
环境变量
用户可配置
需设置环境变量
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 8 9 10 11 12 13 14 15 class Calculator {public : int add (int a, int b) { return a + b; } private : int m_value; };
虚函数的 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 p->vfunc (); void * adjusted_this = p; ((void (*)(void *))(*(void **)(*(void **)adjusted_this)[0 ])(adjusted_this);
实际应用 问题: 回调函数如何获取 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位
变化?
char
1
1
不变
short
2
2
不变
int
4
4
不变
long
4
8
变化
long long
8
8
不变
float
4
4
不变
double
8
8
不变
pointer
4
8
变化
size_t
4
8
变化
ptrdiff_t
4
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 22 23 24 long addr = (long )p; intptr_t addr = (intptr_t )p; printf ("%zu\n" , size); printf ("%td\n" , diff); printf ("%lld\n" , llval); #ifdef _WIN32 #define PRId64 "lld" #else #define PRId64 "ld" #endif printf ("%" PRId64 "\n" , value);struct Data { char c; void * p; };
2 Windows/Visual Studio 相关 2.1 VS 中的 MD、MTD 这些调用选项的区别是什么? 基本概念 MD / MT 是 Visual Studio 链接器选项,决定运行时库(Runtime Library)的链接方式:
M = M ultithreaded(多线程)
D = D ynamic linking(动态链接)
T = S tatic linking(静态链接)
D = D ebug(调试版本)
选项对照表
编译选项
链接方式
运行时库
宏定义
特点
/MD
动态链接
MSVCRxx.DLL
_MT, _DLL
运行时依赖 DLL
/MT
静态链接
LIBCMT.lib
_MT
静态链接,无外部依赖
/MDd
动态链接(调试)
MSVCRxxD.DLL
_MT, _DLL, _DEBUG
调试版本
/MTd
静态链接(调试)
LIBCMTD.lib
_MT, _DEBUG
调试版本
代码示例与影响 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 ; }
实际选择建议
场景
推荐选项
原因
分发可执行文件
/MT
无需用户安装 VC++ 运行库
插件/DLL
/MD
与主程序共享运行时
开发测试
/MTd
快速链接,独立调试
发布版
/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++ → 代码生成 → 运行时库
或通过命令行: