0%

C++相关的若干面试问题记录

一次技术沟通中,发现与我做不同项目的同行,在 C++ 机制和操作系统 API 上特别有心得。交流后对我启发很大,把当时聊到的问题按”面试题 + 答案”的方式整理如下,方便速查和背诵。

一、语言机制

问题1:Windows 怎么收集 dump?单进程和多进程分别怎么做?

一句话答案: 进程内注册 SetUnhandledExceptionFilter,崩溃回调里调 MiniDumpWriteDump 写文件;多进程让每个子进程自收集,父进程只监控退出码;不想写代码就配 WER 的 LocalDumps 注册表。

单进程(代码方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <windows.h>
#include <dbghelp.h>
#pragma comment(lib, "dbghelp.lib")

LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep) {
HANDLE hFile = CreateFileW(L"crash.dmp", GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION mei{ GetCurrentThreadId(), ep, FALSE };
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile,
MiniDumpWithFullMemory, &mei, nullptr, nullptr);
CloseHandle(hFile);
}
ExitProcess(1); // 写完 dump 立刻退出,不要尝试继续运行.
}

int main() {
SetUnhandledExceptionFilter(CrashHandler); // 传自己的处理函数.
}

要点:

  • dump 类型:MiniDumpNormal 只有栈和模块信息(几十 KB);MiniDumpWithFullMemory 含完整内存(体积大但 WinDbg 里能看堆数据)。
  • UEF 的盲区:进程被调试器附加时不触发;abort()、__fastfail 等 CRT 主动终止不走 UEF。
  • 回调里不要再返回 EXCEPTION_EXECUTE_HANDLER 继续跑,崩溃后的进程状态不可信,写完就退。

单进程(免代码方式:WER)

注册表 HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps 下建三个值:

值 类型 含义
DumpFolder REG_EXPAND_SZ dump 保存目录
DumpCount DWORD 最多保留几个
DumpType DWORD 1=mini dump,2=full dump

崩溃时系统自动落盘,适合生产环境兜底。

多进程

  • 推荐:每个子进程自己注册 UEF 自收集。父进程用 WaitForSingleObject 等子进程句柄,看退出码判断是否正常结束。
  • 备选:父进程以 DEBUG_PROCESS 标志 CreateProcess 子进程,循环 WaitForDebugEvent 收 EXCEPTION_DEBUG_EVENT 时跨进程写 dump。缺点:父进程独占调试端口,子进程的 UEF 会失效(异常先给调试器),实现也复杂。

工程选型

场景 方案
客户端 UEF 自收集,自定义路径和上报
服务/守护 子进程自收集 + 父进程监控拉起
生产兜底 WER LocalDumps
跨平台 Crashpad / Breakpad 开源库

问题2:函数调用约定是什么?

一句话答案: 调用约定规定三件事——参数怎么传、栈谁清理、名字怎么修饰。x86 默认 __cdecl,x64 Windows 只有一种约定(关键字全被忽略)。

x86 常见约定

约定 参数传递 栈清理 名称修饰 用途
__cdecl 右→左压栈 调用方 _func C/C++ 默认;调用方清栈才能支持 printf 这类可变参数
__stdcall 右→左压栈 被调方(ret N) _func@N Win32 API
__fastcall 前两个参数 ECX/EDX,其余压栈 被调方 @func@N 求快
__thiscall this 走 ECX,其余压栈 被调方 C++ 修饰 x86 成员函数默认,不可显式指定

x64

  • Windows 只有一种 Microsoft x64 calling convention:前 4 个整数参数用 RCX/RDX/R8/R9,调用方预留 32 字节 shadow space,调用方清栈。
  • __cdecl/__stdcall/__fastcall 写在 x64 代码里被编译器忽略,写了也白写。
  • __vectorcall 是独立的显式约定(SIMD 参数走 XMM),不是 x64 默认约定的别名。

不匹配的后果与对策

  • 调用方和被调方约定不一致 → 栈不平衡 → 返回地址错位崩溃。hook/注入时代理函数必须与原函数约定一致。
  • DLL 导出函数双方必须声明一致;extern "C" 去掉 C++ 名称修饰,配 .def 文件可再去掉 _、@N 修饰,让 GetProcAddress 用干净的名字找函数。

问题3:Qt 信号槽怎么传递自定义数据结构?跨线程呢?

一句话答案: 跨线程用 Qt::QueuedConnection,参数会被拷贝进目标线程的事件队列;自定义类型必须 Q_DECLARE_METATYPE + qRegisterMetaType,否则运行时报 “Cannot queue arguments of type”。

连接类型决定行为

类型 行为
Qt::DirectConnection 信号发出时立即在发送方线程同步调用槽
Qt::QueuedConnection 参数拷贝后入事件队列,在接收方线程的事件循环里异步执行
Qt::AutoConnection(默认) 收发同线程 = Direct,跨线程 = Queued

跨线程传自定义类型的完整步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 头文件里声明可拷贝的类型.
struct MyData {
int id;
QString name;
QVector<double> values;
};
Q_DECLARE_METATYPE(MyData)

// 2. 程序启动时注册到元对象系统(QueuedConnection 必需).
qRegisterMetaType<MyData>("MyData");

// 3. 先 connect,再 emit(顺序反了信号直接丢).
connect(this, &MainWindow::startWork,
m_worker, &Worker::doWork, Qt::QueuedConnection);

MyData data{ 1, "test", {1.0, 2.0} };
emit startWork(data); // data 被拷贝进事件队列,发送方可以继续改自己的.

三个坑

  1. 先 emit 后 connect:信号发出时还没有连接,直接丢失。连接必须在发射之前建立。
  2. 跨线程传指针/引用:QueuedConnection 异步执行,等槽函数跑起来,发送方的局部变量可能已析构。跨线程一律传值或 QSharedPointer。
  3. 接收方线程没有事件循环:QThread 子类如果没调 exec(),队列里的槽永远不会执行。

问题4:怎么加载不同路径下的 DLL?

一句话答案: 知道绝对路径直接 LoadLibrary;要加搜索目录用 AddDllDirectory 配合 LoadLibraryEx 的 LOAD_LIBRARY_SEARCH_USER_DIRS;SetDllDirectory 会替换默认搜索顺序,慎用。

方法速查

1
2
3
4
5
6
7
8
9
10
11
// 方法一:绝对路径,最明确.
HMODULE h = LoadLibraryW(L"C:\\mylib\\mydll.dll");

// 方法二:加搜索目录(Win8+ 推荐做法).
AddDllDirectory(L"C:\\mylib\\plugins");
// AddDllDirectory 单独调用不生效,必须在 LoadLibraryEx 里带上 USER_DIRS 标志.
HMODULE h2 = LoadLibraryExW(L"mydll.dll", nullptr,
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS |
LOAD_LIBRARY_SEARCH_USER_DIRS);

// 方法三:SetDllDirectory 会替换掉默认搜索顺序(当前目录等),容易引入坑,少用.

要点

  • LoadLibrary 的参数完整路径或仅文件名都可以:完整路径直接定位、绕过搜索顺序;只写文件名则走默认搜索顺序。相对路径依赖”当前目录”,可能被别的代码改掉,不推荐。
  • 不用把每个 DLL 都 LoadLibrary 一遍:你只加载自己直接调用的模块,它的依赖由加载器读导入表自动解析。同目录的多个 DLL 用 AddDllDirectory 加一次目录即可。
  • 完整路径加载的 DLL,它的依赖不会优先从该路径找,仍走默认搜索顺序,除非用 LOAD_WITH_ALTERED_SEARCH_PATH 标志或提前 AddDllDirectory。
  • 默认搜索顺序(SafeDllSearchMode 开启):应用程序目录 → System32 → Windows 目录 → 当前目录 → PATH。当前目录排在 PATH 前面,也是 DLL 劫持的经典攻击面。
  • 延迟加载(链接器 /delayload:mydll.dll):首次调用才加载 DLL,但链接时仍需要导入库 .lib 和 delayimp.lib,不是完全脱离 lib。
  • 不要用 SetCurrentDirectory 改变相对路径基准——它是进程全局状态,多线程下不安全。

实战场景:A/B 两套同名 DLL 目录的切换测试

场景: 路径 A 和路径 B 下各有 100 个同名 DLL(比如新旧两版实现),要多次启动程序分别验证两套 DLL 功能是否正常。

错误思路: 逐个 LoadLibrary 完整路径。100 个 DLL 互相有依赖,枚举不完;而且依赖解析不看你 LoadLibrary 的路径,仍走默认搜索顺序,结果加载出”A 目录的 a.dll + B 目录的 b.dll”的混搭,测了个寂寞。

正确思路:控制搜索顺序,而不是控制每次加载。 启动时把选定目录插进搜索路径,之后所有加载(包括依赖解析)自动命中同一目录:

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
#include <windows.h>
#include <iostream>

// ===== 切换测试路径:二选一,注释掉另一行,重新编译 =====
const wchar_t* kDllDir = L"D:\\test\\pathA"; // 测路径A.
// const wchar_t* kDllDir = L"D:\\test\\pathB"; // 测路径B.

int main() {
// 把选定目录插入搜索顺序(替代"当前目录"的位置),进程全局生效.
if (!SetDllDirectoryW(kDllDir)) {
std::cout << "SetDllDirectory 失败: " << GetLastError() << std::endl;
return 1;
}

// 只加载程序直接用的入口 DLL,只写文件名,依赖自动跟着从同目录解析.
HMODULE h = LoadLibraryW(L"mydll.dll");
if (!h) {
std::cout << "加载失败: " << GetLastError() << std::endl; // 126 = 找不到 DLL 或其依赖.
return 1;
}

// 验证实际加载来源,防止搜到意外位置.
wchar_t loaded[MAX_PATH];
GetModuleFileNameW(h, loaded, MAX_PATH);
std::wcout << L"实际加载: " << loaded << std::endl;

FreeLibrary(h);
return 0;
}

三个必须注意的前提:

  1. 隐式链接时 SetDllDirectory 来不及生效。 exe 如果链了 DLL 的 .lib(代码里直接调导出函数),DLL 在 main 运行之前就被加载器装好了。解决办法:链接器 → 输入 → 延迟加载的 DLL,把相关 DLL 设成 /delayload,首次调用才加载;或者放弃 .lib,全部显式 LoadLibrary + GetProcAddress。
  2. exe 所在目录不能放同名 DLL。 SetDllDirectory 插入的位置在”应用程序目录”之后,exe 目录下有同名 DLL 会永远先命中,A/B 测试全部失效。
  3. 同进程内无法先后加载两个同名 DLL。 系统按模块名判重,第二次 LoadLibrary 直接返回第一次的句柄。所以 A/B 切换必须是”改注释 → 重新编译 → 重新启动进程”,想在单进程内热切换是不行的(真要热切换得用隔离的 Ldr 私有命名空间,远超测试需求)。

替代方案对比:

方案 切换方式 适用
注释常量 + 重编译(上面方案) 改代码 DLL 数量多、有依赖关系,推荐
命令行参数传路径 运行时不改代码 自动化测试脚本
两个 .bat 各带参数启动 双击切换 手动反复验证

问题5:this 指针是怎么传递的?

一句话答案: this 是成员函数的隐藏首参:x86 走 ECX(__thiscall),x64 Windows 走 RCX,x64 Linux(System V ABI)走 RDI。static 成员函数没有 this。

汇编视角(x64 Windows)

1
2
3
4
5
; obj.add(a, b) 等价于 add(&obj, a, b).
mov rcx, [obj] ; this 作为第一个参数进 RCX.
mov edx, a
mov r8d, b
call Calculator::add

面试延伸点

  • 多重继承要调整 this:指向非首个基类的指针调虚函数时,编译器在虚表里插 thunk 先把 this 减去偏移,再跳到真正的函数。
  • this 可以为 nullptr 而不崩:成员函数体内只要不访问成员变量、不调虚函数,nullptr->func() 实际能跑通(但属于未定义行为,别依赖)。这是经典面试陷阱题。
  • 普通回调想拿到 this:用 lambda 捕获 [this]、std::bind,或写一个 static 函数接收对象指针再转发。

问题6:32 位、64 位下各数据类型多少字节?哪些变了?为什么?

一句话答案: 变的只有指针、size_t、long(仅 Linux/macOS);Windows 用 LLP64 模型(long 保持 4 字节),Linux/macOS 用 LP64 模型(long 变 8 字节)。

类型 32 位 Win64 (LLP64) Linux64 (LP64)
char / short / int 1 / 2 / 4 1 / 2 / 4 1 / 2 / 4
long 4 4 8
long long 8 8 8
float / double 4 / 8 4 / 8 4 / 8
指针 4 8 8
size_t / ptrdiff_t 4 8 8

为什么这样设计

  • 指针必须变 8 字节才能寻址 64 位空间;size_t 跟着指针走,否则表示不了大对象。
  • Windows 让 long 保持 4 字节是兼容性选择:大量 Win32 代码假设 long 和 int 同宽、DWORD 就是 long。Unix 世界没有这层包袱,long 直接升到 8。

移植守则

1
2
3
4
5
6
7
8
#include <cstdint>
#include <cinttypes>

int64_t a = 1; // 固定位数用 <cstdint>,别用 long.
intptr_t p = (intptr_t)ptr; // 指针转整数用 intptr_t/uintptr_t,long 存不下 64 位指针.

printf("%zu", size); // size_t 用 %zu.
printf("%" PRId64, a); // int64_t 跨平台打印用 PRId64.

二、Windows / Visual Studio

问题7:VS 的 /MD、/MDd、/MT、/MTd 有什么区别?

一句话答案: 这是 CRT(C 运行时库)链接方式:/MD = 动态链接 CRT(依赖 vcruntime140.dll,需装 VC++ Redist),/MT = 静态链接 CRT(无外部依赖、体积大),末尾 d = Debug 版。同进程所有模块必须统一。

命名规则与对照

M = Multithreaded;第二字母 D = 动态 CRT、T = 静态 CRT;末尾 d = Debug。

选项 链接方式 依赖 适用
/MD 动态 vcruntime140.dll、ucrtbase.dll、msvcp140.dll Release 主流选择
/MT 静态 无(CRT 编进二进制) 单文件分发、绿色软件
/MDd 动态 Debug 带 d 后缀的 CRT DLL Debug 对应 /MD
/MTd 静态 Debug 无 Debug 对应 /MT

必须统一的原因:跨模块堆崩溃

1
2
3
4
5
// DLL 里(/MT 编译,有自己的 CRT 堆).
void* AllocInDll() { return malloc(100); }

// EXE 里(/MD 编译,用 CRT DLL 的堆).
free(p); // 崩:两个堆互不认识对方的内存.
  • 每个静态链接 CRT 的模块都有自己独立的堆管理器,跨模块 malloc/free 直接崩。
  • 就算都 /MD,CRT 版本不同(如 VS2015 配 VS2019 编译的模块混用)也可能出问题。
  • 设计原则:谁分配谁释放,DLL 接口不透传内存所有权,导出对应的 FreeXxx 函数。

配置位置

项目属性 → C/C++ → 代码生成 → 运行时库。


参考资料

  • MSDN: MiniDumpWriteDump、SetUnhandledExceptionFilter、LocalDumps(WER)。
  • MSDN: x64 Calling Convention、Calling Conventions(x86)。
  • Qt 文档: Signals & Slots、QMetaType。
  • 《程序员的自我修养——链接、装载与库》。