智能指针是 C++ 中用于自动化管理动态内存的类模板,通过封装原生指针,并利用 RAII(资源获取即初始化)技术,确保内存的自动释放,从而避免内存泄漏和悬空指针问题。它是现代 C++ 内存管理的核心工具之一。
原生指针的缺陷:
1.内存泄漏:忘记调用delete
2.悬空指针:释放后仍访问指针
3.重复释放:同一内存被多次delete
智能指针的优势:
1.自动释放内存,不需手动delete,超出作用域自动释放
2.防止悬空指针,自动置空或报错(如weak_ptr)
C++11引入了三种主要智能指针,定义在<memory>头文件中:
std::unique_ptr 独占所有权 替代new/delete,明确资源归属
std::shared_ptr 共享所有权 多个对象共享同一资源
std::weak_ptr 弱引用(不增加计数) 解决shared_ptr的循环引用问题
std::unique_ptr:独占所有权
特点:
1.同一时间只有一个unique_ptr可以持有资源
2.禁止拷贝,但允许移动(std::move)
3.轻量级,无额外开销
示例:
#include <memory>
void demo() {
std::unique_ptr<int> up1(new int(42)); // 独占资源
// std::unique_ptr<int> up2 = up1; // 错误!禁止拷贝
std::unique_ptr<int> up3 = std::move(up1); // 合法,所有权转移
if (up1) {
std::cout << *up1; // up1 已为空,不会执行
}
if (up3) {
std::cout << *up3; // 输出 42
}
} // up3 析构时自动释放内存
如果想要用unique_ptr管理动态数组并通过指针算术访问元素,该如何做?
错误示范:
void demo()
{
unique_ptr<int>up1(new int[10]);
unique_ptr<int>up2 = move(up1);
for (int i = 0; i < 10; i++)
{
cout <<*(up2+i) << " ";
}
}
两个关键错误:
1.unique_ptr<int>的默认删除器是delete,而不是delete[],会导致内存释放错误
2.unique_ptr<int>不支持直接指针算术,因为它是一个类对象,不是原生指针
智能指针与数组的兼容性问题:
1.默认删除器不匹配:new int[10]需要delete[]释放,但unique_ptr<int>默认使用delete,会导致未定义行为(内存泄漏或崩溃)
2.unique_ptr<int>是一个类模板,未重载operator+,因此up2+i无法编译。即使能编译,unique_ptr的设计初衷是管理单一对象,而非数组。
正确的数组管理方式:
1.使用unique_ptr<T[]>特化版本
unique_ptr<int[]>up(new int[10]);
for (int i = 0; i < 10; i++)
{
up[i] = i;
}
直接初始化:
unique_ptr<int[]>up(new int[3]{1,2,3});
2.C++17的std::make_unique支持数组
auto up = make_unique<int[]>(10);//等价于new int[10]
std::shared_ptr 共享所有权
特点:
1.通过引用计数管理资源,最后一个shared_ptr析构时释放内存
2.支持拷贝和移动
3.线程安全(引用计数原子操作,但指向的数据需额外同步)
shared_ptr<int>sp1 = make_shared<int>(100);//推荐用法
shared_ptr<int>sp2 = sp1;//引用计数+1
cout << *sp1 << "," << *sp2;//输出100,100
cout << sp1.use_count();//输出2(引用计数)
shared_ptr不直接支持数组。因为其设计初衷是共享单个对象,而非数组。如果需要管理数组,需要定义删除器:
shared_ptr<int>sp(new int[10], [](int* p) {delete[]p; });
//但无法使用sp[i]访问元素,需要手动转换
int* raw = sp.get();
raw[0] = 10;//不安全
推荐使用vector
shared_ptr<vector<int>>sp = make_shared<vector<int>>(10);
(*sp)[0] = 42;
std::weak_ptr 弱引用
特点:
1.不增加引用计数,不阻止资源释放
2.需要通过lock()转换为shared_ptr后访问数据
3.用于解决shared_ptr的循环引用问题
示例:打破循环引用
struct Node
{
shared_ptr<Node>next;
weak_ptr<Node>prev;//用weak_ptr避免循环引用
};
void demo()
{
auto node1 = make_shared<Node>();
auto node2 = make_shared<Node>();
node1->next = node2;
node2->prev = node1;//不会增加引用计数
}
make_shared<Node>()的功能:在堆内存中动态创建一个Node对象,并返回一个shared_ptr<Node>智能指针来管理它。等价于:
shared_ptr<Node> node1(new Node()); // 传统方式(不推荐)
优势:1.更高效:make_shared会一次性分配内存(对象+控制块),而shared_ptr<Node>(new Node())需要两次分配 2.更安全:避免内存泄漏。如果new后抛出异常,shared_ptr可能无法正确接管内存
逐行解析:
struct Node
{
shared_ptr<Node> next; // 下一个节点(强引用,会增加引用计数)
weak_ptr<Node> prev; // 上一个节点(弱引用,不增加计数)
};
void demo()
{
// 创建两个节点(引用计数初始为1)
auto node1 = make_shared<Node>(); // node1 的引用计数 = 1
auto node2 = make_shared<Node>(); // node2 的引用计数 = 1
// 构建双向关系
node1->next = node2; // node2 的引用计数 +1 → 变为2
node2->prev = node1; // node1 的引用计数不变(因为 prev 是 weak_ptr)
} // 作用域结束:
// node1 的引用计数从1→0,触发释放 Node1;
// node2 的引用计数从2→1(因 Node1 的 next 已释放),Node2 也会被释放
shared_ptr的引用计数:
每次拷贝shared_ptr,引用计数+1;每次析构shared_ptr,引用计数-1.计数归零时,自动释放内存。
weak_ptr的作用:不增加引用计数,避免循环引用导致的内存泄漏。需通过lock()获取临时shared_ptr才能访问对象
if (auto temp = node2->prev.lock())
{
// 安全使用 temp(此时引用计数 +1)
}
else
{
// 对象已被释放
}
如果prev也用shared_ptr,会导致:
Node1 ──强引用──> Node2
↑ │
└──强引用───────┘
Node1与Node2的引用计数永远大于等于1,无法释放,内存泄漏。
注意事项:
1.优先考虑使用make_shared和make_unique
auto ptr=make_unique<int>(10);
2.明确所有权语义:unique_ptr独占所有权,shared_ptr+weak_ptr共享所有权
3.避免裸指针与智能指针混用
int*raw=new int(10);
shared_ptr<int>sp(raw);//控制块和raw可能不同步
4.不用于非堆内存
int x=10;
unique_ptr<int>up(&x);//错误!栈内存不能delete