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++开发的首选。但它们要求开发者理解其所有权语义和使用规则。正确地使用智能指针可以消除绝大部分的内存泄漏问题,但如果你误用了它们,依然会陷入麻烦。 它们是你武器库中的利器,但并不能让你对内存管理高枕无忧。

posted @ 2025-10-23 17:10  焦涛  阅读(73)  评论(0)    收藏  举报