C++智能指针一定能避免内存泄漏吗?
尽管智能指针很强大,但以下几种情况依然会导致内存泄漏:
1. 循环引用(Cyclic Reference)
这是 std::shared_ptr 最经典的陷阱。
#include <iostream>
#include <memory>
struct B;
struct A
{
A() { std::cout << "A()\n"; }
std::shared_ptr<B> b_ptr;
~A() { std::cout << "~A()\n"; }
};
struct B
{
B() { std::cout << "B()\n"; }
std::shared_ptr<A> a_prt;
//std::weak_ptr<A> a_prt;
~B() { std::cout << "~B()\n"; }
};
int main()
{
{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
std::cout << "a_use_count:" << a.use_count() << std::endl;
std::cout << "b_use_count:" << b.use_count() << std::endl;
std::cout << "a->b_ptr = b" << std::endl;
a->b_ptr = b;//a的b_ptr指向b,b的引用计数为2;
std::cout << "b_use_count:" << b.use_count() << std::endl;
std::cout << "b->a_prt = a" << std::endl;
b->a_prt = a; // b的a_ptr指向a, a的引用计数为2;
std::cout << "a_use_count:" << a.use_count() << std::endl;
}
// 出作用域,a和b离开作用域,引用计数都减1,但都还剩下1(因为互相指着)
// 所以a和b都不会被销毁,导致内存泄漏!
return 0;
}
输出结果如下:
A()
B()
a_use_count:1
b_use_count:1
a->b_ptr = b
b_use_count:2
b->a_prt = a
a_use_count:2
从上面结果可以看出A对象和B对象的析构函数都没有调用,证明A,B开辟的内存都没销毁,整个程序发生内存泄漏。
解决方案:在可能形成循环引用的地方,将一方改为 std::weak_ptr。weak_ptr 不会增加引用计数。
#include <iostream>
#include <memory>
struct B;
struct A
{
A() { std::cout << "A()\n"; }
std::shared_ptr<B> b_ptr;
~A() { std::cout << "~A()\n"; }
};
struct B
{
B() { std::cout << "B()\n"; }
//std::shared_ptr<A> a_prt;
std::weak_ptr<A> a_prt;
~B() { std::cout << "~B()\n"; }
};
int main()
{
{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
std::cout << "a_use_count:" << a.use_count() << std::endl;
std::cout << "b_use_count:" << b.use_count() << std::endl;
std::cout << "a->b_ptr = b" << std::endl;
a->b_ptr = b;//a的b_ptr指向b,b的引用计数为;
std::cout << "b_use_count:" << b.use_count() << std::endl;
std::cout << "b->a_prt = a" << std::endl;
b->a_prt = a; // b的a_ptr指向a, a的引用计数为;
std::cout << "a_use_count:" << a.use_count() << std::endl;
}
// 出作用域,a和b离开作用域,引用计数都减1,但都还剩下1(因为互相指着)
// 所以a和b都不会被销毁,导致内存泄漏!
return 0;
}
输出结果如下:
A()
B()
a_use_count:1
b_use_count:1
a->b_ptr = b
b_use_count:2
b->a_prt = a
a_use_count:1
~A()
~B()
从上面结果可以看出A对象和B对象的析构函数都调用了,证明A,B开辟的内存都销毁,不存在内存泄漏的问题。
2. 静态变量或全局变量持有智能指针
如果静态或全局的 shared_ptr 持有某个对象的引用,那么该对象的引用计数永远不会降为0,直到程序结束。
std::shared_ptr<MyClass> global_ptr;
void leak() {
auto local_ptr = std::make_shared<MyClass>();
global_ptr = local_ptr; // 现在引用计数至少为2
} // local_ptr离开作用域,引用计数减1,但global_ptr还持有,所以对象不会被释放
// 除非显式地 global_ptr.reset(),否则对象会一直存在到程序结束。
3. 使用 std::shared_ptr 管理非动态内存或第三方资源
如果你错误地将 shared_ptr 或 unique_ptr 指向一个栈上的对象或者通过 malloc 分配的内存,会导致未定义行为(通常是双重释放)。
void bad_idea() {
int x = 10;
std::shared_ptr<int> ptr(&x); // 错误!x在栈上,shared_ptr会在其生命周期结束时尝试delete它。
} // 灾难发生!
正确做法:始终使用 std::make_shared 和 std::make_unique 来创建智能指针。
4. 在容器中存放智能指针的原始指针
如果你从一个智能指针中获取了原始指针,并用另一个智能指针来管理它,会导致同一块内存被多个“所有者”管理,从而被重复释放。
void double_delete() {
auto ptr1 = std::make_unique<int>(100);
int* raw_ptr = ptr1.get(); // 获取原始指针,但不释放所有权
// 错误!ptr2不知道ptr1的存在,两者都会尝试删除同一块内存
std::unique_ptr<int> ptr2(raw_ptr);
} // 运行时错误:双重释放
5. 由裸指针构造智能指针的陷阱
如果一个对象已经由裸指针 new 出来,并且在构造智能指针之前抛出了异常,那么就会发生内存泄漏。
void potential_leak() {
MyClass* raw_ptr = new MyClass; // 1. 分配内存
some_function_that_might_throw(); // 2. 如果这里抛出异常...
std::unique_ptr<MyClass> smart_ptr(raw_ptr); // 3. ...这行不会执行,内存泄漏!
}
解决方案:同样,使用 std::make_unique 或 std::make_shared,它们是一次性操作,是异常安全的。
void safe() {
auto smart_ptr = std::make_unique<MyClass>(); // 分配和构造智能指针是原子的
some_function_that_might_throw();
}
总结
| 情况 | 描述 | 解决方案 |
|---|---|---|
| 循环引用 | shared_ptr 互相指向,引用计数永不为0 |
使用 std::weak_ptr 打破循环 |
| 静态/全局持有 | 生命周期长的对象持有引用,阻止释放 | 谨慎设计,必要时手动 reset() |
| 管理错误资源 | 用智能指针管理栈内存或 malloc 内存 |
只管理由 new 分配的堆内存,并用 make_xxx |
| 原始指针交叉使用 | 多个智能指针从同一个裸指针创建 | 绝不从一个已有所有者的裸指针创建新智能指针 |
| 异常不安全构造 | 在 new 和智能指针构造之间发生异常 |
使用 make_unique / make_shared |
结论:C++智能指针是避免内存泄漏的极其强大和有效的工具,应该作为现代C++开发的首选。但它们要求开发者理解其所有权语义和使用规则。正确地使用智能指针可以消除绝大部分的内存泄漏问题,但如果你误用了它们,依然会陷入麻烦。 它们是你武器库中的利器,但并不能让你对内存管理高枕无忧。
浙公网安备 33010602011771号