C++篇:深入理解 C++ 智能指针:从裸指针到 RAII 的蜕变

前面我们聊了 C++ 异常的抛出、捕获、栈展开,也聊了异常安全的痛点 ------ 一旦异常抛出,try 块中未释放的内存、锁、文件句柄就可能泄漏,哪怕手动 catch 后再释放,也容易因逻辑疏漏留下隐患。

而智能指针,正是解决这类问题的「神器」。它利用 RAII 思想,把资源的生命周期和对象绑定 ,哪怕中途抛出异常,栈展开时也会自动调用析构函数释放资源,让你彻底告别手动管理资源的烦恼。接下来,我们就聊聊智能指针是如何和异常安全完美配合的。

1.智能指针应用的场景

下面程序中我们可以看到,new 了以后,我们也 delete 了,但是因为抛异常导致后面的

delete 没有得到执行 ,所以就内存泄漏了,所以我们需要 new 以后捕获异常,捕获到异常后

delete 内存,再把异常抛出,但是因为 new 本身也可能抛异常,连续的两个 new 和下面的

Divide 都可能会抛异常,让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简

单多了。

cpp 复制代码
#include<iostream>
using namespace std;
double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}
void Func()
{
	// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
	// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
	// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
	// 是智能指针,否则代码太戳了
	int* array1 = new int[10];
	int* array2 = new int[10]; // 抛异常呢
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array1 << endl;
		cout << "delete []" << array2 << endl;
		
		delete[] array1;
		delete[] array2;
		throw; // 异常重新抛出,捕获到什么抛出什么
	}
	// ...
	cout << "delete []" << array1 << endl;
	delete[] array1;
	cout << "delete []" << array2 << endl;
	delete[] array2;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

2.RAII和智能指针的设计思路

1.什么是RAII

RAII = Resource Acquisition Is Initialization

资源获取即初始化

它是 C++ 独有的、解决资源管理 + 异常安全的最核心设计思想。

一句话总结:
把资源交给一个对象管理,利用对象生命周期自动释放资源。

RAII 的核心规则

构造函数 :创建对象时,自动获取资源(new 内存、打开文件、加锁)
析构函数:对象销毁时,自动释放资源(delete、关闭文件、解锁)

不管程序是正常结束,还是因为异常中途退出,栈上对象都会被自动销毁 → 析构函数一定会执行 → 资源一定会释放。

RAII 的本质,就是利用 C++ 对象生命周期结束时,会自动调用析构函数这一特性,把资源的释放交给编译器自动完成。

2.智能指针的设计思路

智能指针类除了满足 RAII 的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类一样,**重载 operator* / operator-> / operator **等运算符,方便访问资源。

cpp 复制代码
template<class T>
class SmartPtr
{
public:
	// RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{
	}
	~SmartPtr()
	{
		cout << "delete[] " << _ptr << endl;
		delete[] _ptr;
	}
	// 重载运算符,模拟指针的⾏为,⽅便访问资源
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator[](size_t i)
	{
		return _ptr[i];
	}
private:
	T* _ptr;
};

3.C++标准库中智能指针的使用

#include<memory>

1.auto_ptr (已被弃用)

auto_ptr 是 C++98 里最早的智能指针,也是 C++ 第一个官方 RAII 实现,用来自动管理堆内存,防止异常泄漏。

但C++11 已经弃用,C++17 直接移除,现在绝对不推荐使用!

因此只做了解

核心特点
•最早的智能指针,基于 RAII,自动 delete 内存;
•拥有权转移式管理指针:
•一个 auto_ptr 赋值给另一个时,原对象会被置空,变成 "无效指针";
•没有引用计数,同一时间只有一个对象拥有内存。

但为什么不推荐呢?

致命缺陷

•赋值 / 拷贝后,原对象悬空,极易引发空指针崩溃;
•不能用于容器(如 vector<auto_ptr>),容器拷贝会直接炸;
•不支持数组,不能用 delete\[\];
•行为不符合直觉,坑多、难维护。

2.unique_ptr

unique_ptr 是 独占所有权 的智能指针,遵循 RAII 原则,同一时间只能有一个 unique_ptr 指向同一块内存,它离开作用域时,会自动释放内存。

核心特点:

独占所有权 :不支持拷贝,只支持移动

轻量高效 :和裸指针占用相同内存,无额外开销

•异常安全:无论正常退出还是异常抛出,内存都会自动释放

1.创建与使用

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 方式1:直接构造
    unique_ptr<int> p1(new int(10));
    
    // 方式2:推荐使用 make_unique(C++14 起)
    auto p2 = make_unique<int>(20);

    // 像裸指针一样使用
    cout << *p1 << endl;
    cout << *p2 << endl;

    // 自动释放,无需 delete
    return 0;
}

2. 移动(移动语义)

cpp 复制代码
unique_ptr<int> p1 = make_unique<int>(10);
// unique_ptr<int> p2 = p1; // 编译报错!禁止拷贝

unique_ptr<int> p2 = move(p1); // 所有权转移给 p2
// 此时 p1 变为空,p2 拥有内存
if (!p1) {
    cout << "p1 已经不再拥有内存" << endl;
}

为什么它比 auto_ptr 安全?

•auto_ptr 是**"拷贝即掏空",行为隐蔽且危险**;而 unique_ptr:

拷贝直接编译报错,从根源上杜绝悬空指针

•只有 std::move 才会转移所有权,行为明确

支持数组版本:unique_ptr<int\[\]>,自动用**delete\[\]**释放

3.shared_ptr

shared_ptr 是一种共享所有权 的智能指针,它通过引用计数机制来管理内存:

每多一个 shared_ptr 指向同一块内存,引用计数就 +1;每销毁一个 shared_ptr,引用计数就 -1;当计数降到 0 时,内存会被自动释放。

1. 创建与初始化

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 方式1:直接构造(不推荐,存在异常安全问题)
    shared_ptr<int> p1(new int(10));

    // 方式2:推荐使用 make_shared(更高效、异常安全)
    auto p2 = make_shared<int>(20);

    cout << *p1 << endl;
    cout << *p2 << endl;
    return 0;
}

2. 引用计数的变化

cpp 复制代码
auto p1 = make_shared<int>(10);
cout << p1.use_count() << endl; // 输出 1,只有 p1 指向它

auto p2 = p1; // 拷贝构造,共享所有权
cout << p1.use_count() << endl; // 输出 2
cout << p2.use_count() << endl; // 输出 2

p1.reset(); // p1 放弃所有权
cout << p2.use_count() << endl; // 输出 1

shared_ptr 是基于引用计数的共享所有权智能指针,解决了多个对象共享同一块内存的问题,同时保证了异常安全。但使用时必须注意循环引用的陷阱,需要配合 weak_ptr 来避免内存泄漏。

4.weak_ptr(辅助指针)

weak_ptr 是一种弱引用智能指针,它只能从 shared_ptr 或其他 weak_ptr 构造,本身不拥有对象的所有权

•不会改变 shared_ptr 的引用计数

•不能直接访问对象,必须先通过 lock() 升级为 shared_ptr

•可以检测对象是否已经被释放

•核心作用打破 shared_ptr 的循环引用,避免内存泄漏。

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    auto sp = make_shared<int>(10);
    weak_ptr<int> wp = sp; // 弱引用,不增加计数

    cout << "sp 引用计数:" << sp.use_count() << endl; // 输出 1

    // 尝试升级为 shared_ptr
    if (auto p = wp.lock()) {
        cout << "对象存在,值为:" << *p << endl;
        cout << "升级后计数:" << p.use_count() << endl; // 输出 2
    } else {
        cout << "对象已被释放" << endl;
    }

    return 0;
}

5.shared_ptr和weak_ptr解决引用循环

•如下图所述场景,n1 和 n2 析构后,管理两个节点的引用计数减到 1

•右边的节点什么时候释放呢,左边节点中的_next 管着呢,_next 析构后,右边的节点就释放了。

_next 什么时候析构呢,_next 是左边节点的的成员,左边节点释放,_next 就析构了。

•左边节点什么时候释放呢,左边节点由右边节点中的_prev 管着呢,_prev 析构后,左边的节点就释放了。

•_prev 什么时候析构呢,_prev 是右边节点的成员,右边节点释放,_prev 就析构了。

•至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏

•把 ListNode 结构体中的_next 和_prev 改成 weak_ptr,weak_ptr 绑定到 shared_ptr 时不会增加它的引用计数,_next 和_prev 不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题。

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

struct Node {
    shared_ptr<Node> next;
    weak_ptr<Node> prev;  // 这里改成 weak_ptr!

    ~Node() {
        cout << "Node 析构!内存已释放" << endl;
    }
};

int main() {
    auto n1 = make_shared<Node>();
    auto n2 = make_shared<Node>();

    n1->next = n2;
    n2->prev = n1;

    // 正常释放!
    return 0;
}

4.删除器

所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为 new 经常使用,所以为了简洁一点,unique_ptr 和 shared_ptr 都特化了一份 的版本。

cpp 复制代码
template<class T>
class DeleteArray
{
public:
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};

class Fclose
{
public:
    void operator()(FILE* ptr)
    {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};


// 仿函数对象做删除器
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());

// 函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);

// lambda表达式做删除器
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);

// 实现其他资源管理的删除器
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});

5.C++11和boost中智能指针的关系

一句话总结:C++11 的标准智能指针,基本就是把 Boost.SmartPtr 成熟实现 "抄进标准库",并做了少量简化与扩展。

•Boost 库是为 C++ 语言标准库提供扩展的一些 C++ 程序库的总称,Boost 社区建立的初衷之一就是为 C++ 的标准化工作提供可供参考的实现,Boost 社区的发起人 Dawes 本人就是 C++ 标准委员会的成员之一。在 Boost 库的开发中,Boost 社区也在这个方向上取得了丰硕的成果,C++11 及之后的新语法和库有很多都是从 Boost 中来的。

•C++ 98 中产生了第一个智能指针 auto_ptr。

•C++ boost 给出了更实用的 scoped_ptr/scoped_array 和 shared_ptr/shared_array 和 weak_ptr 等。

•C++ TR1,引入了 shared_ptr 等,不过注意的是 TR1 并不是标准版。

•C++ 11,引入了 unique_ptr 和 shared_ptr 和 weak_ptr。需要注意的是 unique_ptr 对应 boost 的 scoped_ptr。并且这些智能指针的实现原理是参考 boost 中的实现的。

6.shared_ptr简单实现

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<functional>
using namespace std;
struct Date
{
	int _year;
	int _month;
	int _day;
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

	~Date()
	{
		cout << "~Date()" << endl;
	}
};

namespace wyy
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T*ptr=nullptr)
			:_ptr(ptr)
			,_count(new int(1))
		{ }

		~shared_ptr()
		{
			/*delete _ptr;
			delete _count;*/
			release();
		}
		template<class D>
		shared_ptr(T*ptr,D del)
			:_ptr(ptr)
			,_count(new int(1))
			,_del(del)
		{ }
		shared_ptr(const shared_ptr<T>& ptr)
			:_ptr(ptr)
			,_count(ptr._count)
			,_del(ptr._del)
		{ 
			(*_count)++;
		}
		
		shared_ptr<T>& operator=(const shared_ptr<T>& ptr)
		{
			if (_ptr != ptr._ptr)
			{
				release();

				_ptr = ptr._ptr;
				_count = ptr._count;
				_del = ptr._del;
				(*_count)++;
			}
			return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		int use_count()
		{
			return *_count;
		}
		T* get()
		{

			return _ptr;
		}
	private:
		void release()
		{
			if (--(*_count) == 0)
			{
				_del(_ptr);
				delete _count;
				_ptr = nullptr;
				_count = nullptr;
			}
		}
		T* _ptr;
		int* _count;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};
}
相关推荐
丘山望岳1 小时前
二叉搜索双壁——map和set
开发语言·数据结构·c++
QiLinkOS1 小时前
合肥气链科技有限公司创办与未来技术应用
c语言·数据结构·c++·人工智能·单片机·嵌入式硬件·算法
瑞雪兆丰年兮1 小时前
[从0开始学Java|第十六、十七天]项目阶段(拼图小游戏)
java·开发语言
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第85题】【Mysql篇】第15题:MySQL 的事务中,幻读是怎么解决的?
java·开发语言·数据库·mysql·面试
yaoxin5211231 小时前
423. Java 日期时间 API - DayOfWeek 和 Month 枚举
开发语言·python
秋雨梧桐叶落莳1 小时前
iOS——抽屉视图详解
开发语言·macos·ui·ios·objective-c·cocoa
郝学胜-神的一滴1 小时前
Qt 高级开发 016:半内存管理机制
开发语言·c++·qt·程序人生·用户界面
会编程的土豆1 小时前
Go 语言匿名函数详解
c++·golang·xcode
Byte Wizard1 小时前
动态内存管理
c语言·开发语言