首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++内存列传之RAII宇宙:智能指针

C++内存列传之RAII宇宙:智能指针

作者头像
澪贰
发布于 2025-06-11 20:19:51
发布于 2025-06-11 20:19:51
4610
举报
文章被收录于专栏:学习学习
智能指针是 C++ 中用于自动管理动态内存的类模板,它通过 RAII(资源获取即初始化)技术避免手动 new / delete 操作,从而显著减少内存泄漏和悬空指针的风险

1.为什么需要智能指针?

代码语言:javascript
复制
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

如果 p1 这里 new 抛异常会如何?

p1 未成功分配,值为 nullptr 函数直接跳转到 catch 块,p2 未分配,无内存泄漏

如果 p2 这里 new 抛异常会如何?

p1 已分配但未释放,导致内存泄漏 函数跳转到 catch 块,p2 未分配,delete p1 和 delete p2 均未执行

如果 div 调用这里又会抛异常会如何?

p1 和 p2 均已分配但未释放,导致双重内存泄漏 函数跳转到 catch 块,打印错误信息(如 “除 0 错误”)

C++ 不像 java 具有垃圾回收机制,能够自动回收开辟的空间,需要自行手动管理,但是自己管理有时又太麻烦了,况且这里只是两个指针就产生了这么多问题,因此在 C++11 就推出了智能指针用于自动管理内存

2.智能指针原理

2.1 RAll

代码语言:javascript
复制
template<class T>
class SmartPtr 
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}

private:
	T* _ptr;
};

int main()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<string> sp2(new string("xxx"));
	return 0;
}

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术

简单来说,就是把创建的对象给到 SmartPtr 类来管理,当对象的生命周期结束的时候,刚好类也会自动调用析构函数进行内存释放

这种做法有两大好处:

  • 不需要显式地释放资源
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效

2.2 像指针一样使用

都叫做智能指针了,那肯定是可以当作指针一样使用了,指针可以解引用,也可 以通过 -> 去访问所指空间中的内容,因此类中还得需要将 * 、-> 重载下,才可让其像指针一样去使用

代码语言:javascript
复制
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

* 重载返回对象,-> 重载返回地址,这部分的知识点在迭代器底层分析已经讲过很多遍了,就不过多叙述了,可自行翻阅前文

3.C++11的智能指针

智能指针一般放在 <memery> 文件里,C++11 也参考了第三方库 boost

  1. C++ 98 中产生了第一个智能指针 auto_ptr
  2. C++ boost 给出了更实用的 scoped_ptr 和 shared_ptr 和 weak_ptr
  3. C++ TR1,引入了 shared_ptr 等。不过注意的是 TR1 并不是标准版
  4. C++ 11,引入了 unique_ptr 和 shared_ptr 和 weak_ptr。需要注意的是 unique_ptr对应 boost 的 scoped_ptr。并且这些智能指针的实现原理是参考 boost 中的实现的

3.1 auto_ptr

代码语言:javascript
复制
template<class T>
class auto_ptr
{
public:
	// RAII
	// 像指针一样
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~auto_ptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	
	// ap3(ap1)
	// 管理权转移
	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}

	auto_ptr<T>& operator=(auto_ptr<T>& ap) 
	{
		if (this != &ap) 
		{
			_ptr = ap._ptr;       // 转移所有权
			ap._ptr = nullptr;    // 原指针置空
		}
		return *this;
	}
private:
	T* _ptr;
};

auto_ptr 在 C++98 就已经被引入,实现了智能指针如上面所讲的最基础的功能,同时他还额外对拷贝构造、= 重载进行了显式调用,但是这种拷贝虽然能解决新对象的初始化,但是对于被拷贝的对象,造成了指针资源所有权被转移走,跟移动构造有些类似

因此,auto_ptr 会导致管理权转移,拷贝对象被悬空,auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr

3.2 unique_ptr

代码语言:javascript
复制
template<class T>
class unique_ptr
{
public:
	// RAII
	// 像指针一样
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~unique_ptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	// ap3(ap1)
	// 管理权转移
	// 防拷贝
	unique_ptr(unique_ptr<T>& ap) = delete;
	unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
private:
	T* _ptr;
};

unique_ptr 很简单粗暴,直接禁止了拷贝机制

因此,建议在不需要拷贝的场景使用该智能指针

3.3 shared_ptr

代码语言:javascript
复制
template<class T>
class shared_ptr
{
public:
	// RAII
	// 像指针一样
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	// sp3(sp1)
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		++(*_pcount);
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr == sp._ptr)
			return *this;

		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}

		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);

		return *this;
	}

	int use_count() const
	{
		return *_pcount;
	}

	T* get() const
	{
		return _ptr;
	}

private:
	T* _ptr;
	int* _pcount;
};

C++11 中的智能指针就属 shared_ptr 使用的最多,因为它解决了赋值造成的资源被转移可能会被错误访问的问题

类中增加一个新的指针 _pcount 用于计数,即计数有多少个 _ptr 指向同一片空间,多个 shared_ptr 可以同时指向同一个对象,每次创建新的 shared_ptr 指向该对象,引用计数加 1;每次 shared_ptr 析构或者被赋值为指向其他对象,引用计数减 1。当最后一个指向该对象的 shared_ptr 析构时,对象会被自动删除,从而避免内存泄漏

🔥值得注意的是: shared_ptr 同时也支持了无法自己给自己赋值,这里还涉及一些关于线程安全的知识点,待 Linux 学习过后再来补充

3.4 weak_ptr

看似完美的 shared_ptr 其实也会有疏漏,比如:引用循环

代码语言:javascript
复制
struct ListNode
{
	int _data;
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

当执行 node1->next = node2 和 node2->prev = node1 时,node1 内部的 _next 指针指向 node2 ,node2 内部的 _prev 指针指向 node1 。这就导致两个节点之间形成了循环引用关系。此时,由于互相引用,每个节点的引用计数都变为 2 ,因为除了外部的智能指针引用,还多了来自另一个节点内部指针的引用

当 node1 和 node2 智能指针对象离开作用域开始析构时,它们首先会将所指向节点的引用计数减 1 。此时,每个节点的引用计数变为 1 ,而不是预期的 0 。这是因为 node1 的 _next 还指向 node2 ,node2 的 _prev 还指向 node1 ,使得它们的引用计数无法归零

对于 shared_ptr 来说,只有当引用计数变为 0 时才会释放所管理的资源。由于这种循环引用的存在,node1 等待 node2 先释放(因为 node2 的 _prev 引用着 node1 ),而 node2 又等待 node1 先释放(因为 node1 的 _next 引用着 node2 ),最终导致这两个节点所占用的资源都无法被释放,造成内存泄漏

代码语言:javascript
复制
class ListNode 
{
public:
    weak_ptr<ListNode> _next; 
    weak_ptr<ListNode> _prev;
};

为了解决 shared_ptr 的循环引用问题,通常可以使用 weak_ptr 。weak_ptr 是一种弱引用智能指针,它不会增加所指向对象的引用计数。将循环引用中的某一个引用(比如 ListNode 类中的 _prev 或 _next 其中之一)改为 weak_ptr 类型,就可以打破循环引用

因此,weak_ptr 是一种专门解决循环引用问题的指针

4.删除器

代码语言:javascript
复制
#include <iostream>
#include <memory>
#include <string>

using namespace std;

class A 
{
public:
    ~A() 
    { 
        cout << "A::~A()" << endl; 
    }
};

// 仿函数删除器:用于释放malloc分配的内存
template<class T>
struct FreeFunc 
{
    void operator()(T* ptr) const 
    {
        cout << "FreeFunc: free memory at " << ptr << endl;
        free(ptr);
    }
};

// 仿函数删除器:用于释放数组
template<class T>
struct DeleteArrayFunc 
{
    void operator()(T* ptr) const 
    {
        cout << "DeleteArrayFunc: delete[] memory at " << ptr << endl;
        delete[] ptr;
    }
};

int main() 
{
    // 使用FreeFunc删除器的shared_ptr
    shared_ptr<int> sp1((int*)malloc(sizeof(int)), FreeFunc<int>());
    *sp1 = 100;
    cout << "sp1: " << *sp1 << " at " << sp1.get() << endl;
    // 离开作用域时调用FreeFunc删除器

    // 使用DeleteArrayFunc删除器的shared_ptr
    shared_ptr<int> sp2(new int[5], DeleteArrayFunc<int>());
    for (int i = 0; i < 5; ++i) {
        sp2.get()[i] = i;
    }
    cout << "sp2 array:";
    for (int i = 0; i < 5; ++i) {
        cout << " " << sp2.get()[i];
    }
    cout << endl;
    // 离开作用域时调用DeleteArrayFunc删除器

    // 使用lambda删除器管理A对象数组
    shared_ptr<A> sp4(new A[3], [](A* p) {
        cout << "Lambda: deleting array at " << p << endl;
        delete[] p;
        });
    cout << "sp4 array of A objects created" << endl;
    // 离开作用域时调用lambda删除器

    // 使用lambda删除器管理文件句柄
    shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p) {
    if (p) 
    {
        cout << "Lambda: closing file" << endl;
        fclose(p);
    }
    });
    if (sp5) 
    {
        fprintf(sp5.get(), "Hello, shared_ptr with deleter!\n");
        cout << "File written" << endl;
    }
    // 离开作用域时调用lambda删除器关闭文件

    return 0;
}

对于所有的指针不一定是 new 出来的对象,因此利用仿函数设置了删除器,这样就可以调用对应的删除

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-06-04,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 智能指针是 C++ 中用于自动管理动态内存的类模板,它通过 RAII(资源获取即初始化)技术避免手动 new / delete 操作,从而显著减少内存泄漏和悬空指针的风险
  • 1.为什么需要智能指针?
  • 2.智能指针原理
    • 2.1 RAll
    • 2.2 像指针一样使用
  • 3.C++11的智能指针
    • 3.1 auto_ptr
    • 3.2 unique_ptr
    • 3.3 shared_ptr
    • 3.4 weak_ptr
  • 4.删除器
  • 希望读者们多多三连支持
  • 小编会继续更新
  • 你们的鼓励就是我前进的动力
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档