010、构造函数和析构函数
机器码里没有“构造”关键字
构造函数和析构函数最终仍然是函数。它们的特殊性来自编译器自动插入调用,并按照 C++ 对象生命周期规则组织执行顺序。
识别构造函数常见证据:
- 对象首次使用前被调用。
- 首参数是对象地址。
- 初始化多个固定偏移字段。
- 调用基类或成员对象构造函数。
- 含虚函数时向对象写入虚表地址。
识别析构函数常见证据:
- 对象作用域结束、异常清理或
delete前被调用。 - 释放对象持有的堆、句柄、锁等资源。
- 调用成员对象和基类析构函数。
- 某些实现中会改写虚表指针或进入删除析构包装函数。
“第一次成员调用就是构造、最后一次成员调用就是析构”只能作为线索,不能单独作为结论。
初始化顺序
派生对象的构造顺序通常是:
1 | 虚基类(如有) |
析构顺序与之相反。成员初始化列表的书写顺序不能改变成员对象按声明顺序初始化的规则。
不同对象的调用时机
| 对象类型 | 构造时机 | 析构时机 |
|---|---|---|
| 局部对象 | 执行到定义位置 | 离开作用域或展开栈 |
| 堆对象 | new 分配成功后 |
delete 释放前 |
| 参数副本 | 调用前或进入函数时 | 参数生命周期结束 |
| 返回对象 | 结果位置构造 | 接收对象生命周期结束 |
| 全局对象 | main 前的运行库初始化阶段 |
正常进程退出阶段 |
| 函数内静态对象 | 首次执行到定义处 | 正常进程退出阶段 |
进程被强制终止时,正常析构链可能不会执行。
new 与 delete
单个堆对象的典型过程:
1 | operator new -> 检查分配结果 -> 调用构造函数 |
对象数组还要保存元素数量或其他管理信息,以便 delete[] 逆序析构每个元素。具体 cookie 布局属于编译器实现细节。
new[] 必须与 delete[] 配对。反汇编中看到批量构造、逆序循环析构和隐藏计数信息时,应优先考虑对象数组。
拷贝与移动
浅拷贝会逐字段复制。如果对象字段保存独占资源地址,两个对象可能指向同一资源,随后造成重复释放或悬空引用。
逆向时关注:
- 是否调用拷贝构造函数或赋值运算符。
- 是否重新分配并复制资源内容。
- 是否转移资源后把源对象置空。
- 是否存在引用计数增减。
现代编译器可能应用拷贝消除,因此源码中看似存在的拷贝调用不一定出现在机器码中。
异常清理中的析构
如果构造到一半发生异常,已经完成构造的基类和成员必须被析构。反汇编中可能出现:
- 正常路径之外的清理基本块。
- 展开元数据指向析构函数。
- 多条路径汇合到统一资源释放代码。
这类清理代码不是普通业务分支,恢复控制流时要与第 13 章异常处理结合理解。
RAII 视角
RAII 把资源生命周期绑定到对象生命周期。常见资源包括:
- 堆内存。
- 文件与内核对象句柄。
- 临界区、互斥量和锁。
- 网络连接或图形对象。
看见构造中获取资源、析构中释放资源,就可以把相关函数归到同一个资源所有者对象中。
动态验证方法
- 在候选构造函数入口断点。
- 记录
this地址和对象初始字节。 - 单步观察各成员被写入的顺序。
- 对同一地址设置数据断点,观察后续成员修改。
- 在候选析构函数验证释放对象是否一致。
学习检查清单
| 检查项 | 状态 |
|---|---|
| 能列出基类、成员和函数体的构造顺序 | 待复盘 |
| 能识别局部、堆、全局和静态对象的生命周期 | 待复盘 |
| 能区分 new/delete 与 new[]/delete[] 的行为 | 待复盘 |
| 能说明浅拷贝为什么可能导致重复释放 | 待复盘 |
| 能在异常清理路径中识别析构调用 | 待复盘 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ruiqy~!





















