函数参数抛出异常:代码是否内存泄漏?两种写法有何差异?
一、原代码是否会产生内存泄漏?
会存在内存泄漏的风险,原因如下:
在main函数的Foo foo(new A(), new B());调用中,函数实参的求值顺序在C++17及之前的标准中是未指定的。可能出现的执行顺序:
- 先执行
new A()分配内存并构造A对象,得到A*指针; - 接着执行
new B(),此时B的构造函数抛出std::runtime_error异常; - 由于异常抛出时,
A*指针还未被MyPtr<A>临时对象接管(MyPtr<A>的构造还未执行),这块内存没有任何智能指针负责释放,最终导致内存泄漏。
当然也存在另一种顺序:先执行new B()抛出异常,此时new A()还未执行,不会产生泄漏。但标准不保证这种顺序,因此代码存在明确的泄漏风险。
另外,MyPtr类本身的实现是基本正确的:移动构造会转移指针所有权并置空原指针,析构函数会释放持有的指针,只要指针被MyPtr接管,就不会泄漏。问题出在参数传递过程中,未被智能指针接管的裸指针因异常丢失。
二、两种写法的差异
Foo foo(new A(), new B()); 和 Foo foo(MyPtr<A>(new A()), MyPtr<B>(new B())); 主要有以下几点差异:
语法层面
- 前者依赖隐式转换:编译器自动将
A*和B*转换为MyPtr<A>和MyPtr<B>临时对象; - 后者是显式构造:直接调用
MyPtr的构造函数创建临时对象,意图更清晰,可读性更强。
- 前者依赖隐式转换:编译器自动将
内存泄漏风险
两者的核心风险一致:在C++17之前,都可能因实参求值顺序的不确定性导致内存泄漏。即使是显式写法,编译器仍可能先执行new A(),再执行new B()(抛异常),此时MyPtr<A>的临时对象还未构造,同样会丢失A的内存。只有在C17及之后的标准中,函数实参的初始化顺序被规定为从左到右:先完成第一个实参的完整构造(
new A()+MyPtr<A>构造),再处理第二个实参。此时如果第二个实参抛出异常,第一个MyPtr<A>临时对象会被析构,释放A的内存,不会泄漏。但这种安全仅针对C17及以后版本。
修复建议
要彻底避免内存泄漏,应该分步骤构造智能指针,确保每一步的裸指针都被立即接管:
try { MyPtr<A> a(new A()); MyPtr<B> b(new B()); Foo foo(std::move(a), std::move(b)); } catch (const std::exception&) { }
或者使用标准库的std::unique_ptr替代自定义的MyPtr,配合std::make_unique(C++14及以上),能更安全地避免此类问题:
#include <memory> // ... try { auto a = std::make_unique<A>(); auto b = std::make_unique<B>(); Foo foo(std::move(a), std::move(b)); } catch (const std::exception&) { }
内容的提问来源于stack exchange,提问作者Alexey Starinsky

