移动构造函数中必须手动将源对象指针置为nullptr,否则析构时会导致二次释放;所有裸指针成员均需逐一置空,且置空操作须在无异常风险的最后一步完成。

移动构造函数里必须手动置空原始指针
不置空会导致源对象析构时二次释放——这是最常见、最危险的移动语义错误。标准只要求移动后对象“可析构”,但没规定它内部指针是否为 nullptr,你得自己保证。
典型错误是只转移指针值,却忘了清零:
class Buffer {
char* data;
public:
Buffer(Buffer&& other) noexcept : data(other.data) {
// ❌ 缺少 other.data = nullptr;
}
~Buffer() { delete[] data; } // other.data 仍指向已转移内存 → 重复 delete
};- 所有裸指针成员(
char*、int*、MyType*)都必须在移动后显式设为nullptr - 即使成员是
void*或句柄类型(如HANDLE),也要按等效逻辑置为无效值(如INVALID_HANDLE_VALUE) - 如果类有多个资源指针,每个都要单独处理,不能只清一个
为什么不能依赖智能指针自动处理
用 std::unique_ptr 确实能避免手动置空,但它不是万能解药:一旦你混合使用裸指针和智能指针,或需要定制资源释放逻辑(比如 CloseHandle、munmap),裸指针就绕不开。
更关键的是:智能指针的移动操作本身也依赖底层置空逻辑——std::unique_ptr 的移动构造函数内部正是把原对象的 ptr_ 设为 nullptr,你写的裸指针类要模仿这个行为。
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
-
std::unique_ptr移动后原对象的get()返回nullptr,这是它的契约,不是魔法 - 如果你封装了非堆内存(如 mmap 映射区、GPU buffer handle),
std::unique_ptr默认删除器不适用,必须手写移动逻辑并置空 - 性能敏感场景(如高频小对象池)有时会回避智能指针的间接调用开销,此时裸指针 + 正确置空是唯一选择
noexcept 和置空顺序必须严格匹配
置空操作必须放在所有可能抛异常的代码之后、且在构造函数体末尾前完成。否则一旦中途异常,源对象指针仍残留有效值,析构时就会崩。
正确顺序是:先完成资源接管(无异常风险),再置空源指针(无异常风险),最后收尾(如有):
class FileWrapper {
FILE* fp;
public:
FileWrapper(FileWrapper&& other) noexcept : fp(other.fp) {
other.fp = nullptr; // ✅ 必须在此处立即置空
// 不要在这一行之前做任何可能失败的操作(比如 fopen 失败重试)
}
};- 所有移动操作标记为
noexcept,是为了让std::vector等容器敢用移动而非降级为拷贝;但noexcept不代表你可以省略置空 - 如果移动构造函数里需要做非平凡操作(如 dup fd、setsockopt),应确保这些操作本身不抛异常,或提前做完再置空
- 移动赋值运算符同理:
delete data;后必须立刻other.data = nullptr;,否则自赋值检查失效
调试时怎么验证指针是否真被置空
靠肉眼看代码容易漏,运行时验证更可靠。最直接的办法是在析构函数里加断言或日志:
~Buffer() {
if (data != nullptr) {
// 在 debug 模式下触发断点或报错
assert(!"Buffer destroyed with non-null data — likely missing move-nulling");
// 或输出日志:fprintf(stderr, "WARNING: Buffer::~Buffer called with data=%p\n", data);
}
delete[] data;
}- 启用 AddressSanitizer(
-fsanitize=address)能捕获双重释放,但它不告诉你哪次移动漏了置空——所以断言仍是第一道防线 - 单元测试中显式构造右值并移动,然后检查源对象的指针值:
EXPECT_EQ(other.data, nullptr); - 注意:不要在 release 构建中依赖
assert,应改用日志+崩溃转储,或静态分析工具(如 clang-tidy 的cppcoreguidelines-owning-memory)
移动语义的“安全”不来自语言自动保证,而来自你对每一个裸指针的亲手归零。哪怕只漏一个,整个类的移动行为就不可靠——这比拷贝慢更糟,它会让程序在某个特定扩容路径上静默崩溃。

















