📝前言:
这篇文章我们来讲讲C++11------智能指针:
🎬个人简介:努力学习ing
📋个人专栏:C++学习笔记
🎀CSDN主页 愚润求学
🌄其他专栏:C语言入门基础,python入门基础,python刷题专栏,Linux
文章目录
一,什么是智能指针
传统指针的问题
- 内存泄漏(忘记delete)
- 悬垂指针(delete后继续访问)
- 异常安全问题(抛出异常后,后面的delete语句没办法执行)
为了解决上面的问题就出现了:RAII和智能指针设计思路
RAII和智能指针设计思路
RAII:
- 全称:Resource Acquisition Is Initialization(资源获取即初始化)
- 是⼀种管理资源的类的设计思想.
- 本质是⼀种利用对象生命周期来管理获取到的动态资源 ,避免资源泄漏。这里的资源可以是内存、⽂件指针、网络连接、互斥锁等等。
- 对象控制对资源的访问
- 获取资源 与对象的构造绑定
- 释放资源 与对象的析构绑定
智能指针:
- 智能指针就是利用了RAII的设计思想。同时,还要求这个对象能够满足普通指针的行为,方便访问资源。
- 即:重载
operator*/operator->/operator[]
等运算符,方便访问资源
二,智能指针的使用
(1)C++标准库中的智能指针
总体概述
- 所在头文件:
<memory>
- 除了
weak_ptr
他们都符合RAII和像指针⼀样访问的行为。(weak_ptr
不能直接管理资源) - 不同的指针,区别主要是:解决智能指针拷贝时的思路不同
不同指针的特点
下面讲讲不同的智能指针的特点:
auto_ptr
- 这是C++98的,是一个糟糕的指针
- 拷贝时:把被拷贝对象的资源管理权移交给拷贝对象
- 问题:这样会导致被拷贝对象悬空,后续访问会报错
- 这指针能不用就不用,C++11也有更好的
unique_ptr
- 特点:不支持拷贝,只支持移动(传
move(ptr)
移动构造,移动后,原ptr
也会悬空,但是这是用户自己的行为) - 如果不需要拷贝的场景就十分建议使用
shared_ptr
- 支持拷贝,也支持移动
- 底层通过引用计数实现
weak_ptr
- 不支持RAII,也就是:不能用它直接管理资源
- 主要用于:解决
shared_ptr
的循环引用导致的内存泄漏问题
delete的问题
- 智能指针析构时默认是进行
delete
释放资源,这也就意味着如果不是new
出来的资源(如:malloc、new[]、fopen...
),交给智能指针管理,析构时就会崩溃
为了解决上面这个问题:
- 智能指针支持用户提供一个删除器,在删除器中实现资源的释放。
- 但是提供位置不同
unique_ptr
:作为模板参数提供(建议提供仿函数)shared_ptr
:作为构造函数参数提供(仿函数、lambda、函数指针等都可以,建议lambda)
- 删除器的本质是一个可调用对象。当提供了删除器,在智能指针析构的时候,就会调用这个删除器去释放资源。(这个删除器的调用在智能指针析构函数内部)
- 因为
new[]
经常使⽤,所以为了简洁⼀点,unique_ptr
和shared_ptr
都特化了⼀份[]
的版本
删除器提供示例
示例:
cpp
unique_ptr<Date> up1(new Date[5]);
程序崩溃:
使用特化版本[]
的:
cpp
unique_ptr<Date[]> up1(new Date[5]);

shared_ptr
的删除器
示例(使用仿函数):
cpp
class Del
{
public:
void operator()(Date* ptr)
{
delete[] ptr;
}
};
// 传入仿函数删除器
shared_ptr<Date> up1(new Date[5], Del());
return 0;
示例(使用lambda):
cpp
shared_ptr<Date> up1(new Date[5], [](Date* ptr) {delete[] ptr; });
unique_ptr
删除器
示例(使用仿函数):
cpp
// 模版参数处提仿函数作为删除器
unique_ptr<Date, Del> up1(new Date[5]);
其他知识
shared_ptr
除了支持用指向资源的指针构造,还支持make_shared
用初始化资源对象的值直接构造
示例(普通:使用指向资源的指针构造):
cpp
shared_ptr<Date> sp1(new Date); // 使用指向Date的指针构造
shared_ptr<Date> sp2(sp1); // 用 sp1 拷贝构造 sp2
shared_ptr<Date> sp3 = sp2;
// 上面这三管理同一块空间

示例(使用make_shared
)
cpp
shared_ptr<Date> sp1(new Date);
// 使用语法:make_shared<要创建对象的类型>(初始化对象的参数列表)
shared_ptr<Date> sp2 = make_shared<Date>(2025, 5, 2); // 用初始化对象的值直接构造

可见,管理的是新分配的空间。
shared_ptr
和unique_ptr
都支持了operator bool
的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false
,否则返回true
。
示例(直接把智能指针对象给if
判断是否为空):
cpp
shared_ptr<Date> sp2 = make_shared<Date>(2025, 5, 2); // 用初始化对象的值直接构造
if (sp2) // 实际上是 if(sp2.operator bool())
{
cout << "p2不为空" << endl;
}
shared_ptr
和unique_ptr
的构造函数都用了explicit
修饰,防止普通指针隐式类型转换成智能指针对象
(2)C++11和boost库中智能指针的关系
- Boost 库是 C++ 标准库的扩展程序库集合
- Boost 库由 Boost 社区的成员进行维护。社区成员贡献了许多 C++ 标准库中未包含的功能和工具(有好有坏)
- C++ 委员会在制定新的 C++ 标准时,经常参考 Boost 库,取其精华
- 比如:C ++ 11 引入的
unique_ptr
和shared_ptr
和weak_ptr
。分别是参考boost的scoped_ptr
、shared_ptr
和weak_ptr
三,智能指针的原理
unique_ptr模拟实现
unique_ptr
的思路是不支持拷贝,只支持移动,在移动的时候,把资源管理权交给另一个指针。
实现代码(不带删除器版本的):
cpp
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{
cout << "init: " << _ptr << endl;
}
~unique_ptr()
{
if (_ptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
// 禁用拷贝构造和拷贝赋值
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
unique_ptr(unique_ptr<T>&& up)
:_ptr(up._ptr)
{
up._ptr = nullptr; // 原来的置空,相当于转移了管理权
}
unique_ptr<T>& operator=(unique_ptr<T>&& up)
{
delete _ptr; // 先释放当前所管理的空间
_ptr = up._ptr;
up._ptr = nullptr;
}
private:
T* _ptr;
};
shared_ptr模拟实现(重点)
底层利用引用计数器来实现多个指针同时管理。
模拟实现代码(带删除器)
cpp
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr) // 没有删除器的时候,匹配这个
:_ptr(ptr)
, _count(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del) // 有删除器的时候会匹配这个
:_ptr(ptr)
,_count(new int(1))
,_del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_count(sp._count)
,_del(sp._del)
{
++(*_count);
}
// 移动构造函数(区别就是_count不变,并且原来的指针要置空)
shared_ptr(shared_ptr<T>&& sp)
: _ptr(sp._ptr)
, _count(sp._count)
, _del(move(sp._del)) // 这里也调用del的移动构造函数,提高效率
{
sp._ptr = nullptr;
sp._count = nullptr;
}
void release() // 不只用于析构,赋值的时候,release也可能要使用
{
if (_count && --(*_count) == 0)
{
_del(_ptr); // 调用删除器,删除管理的资源
delete _count;
_ptr = nullptr;
_count = nullptr; // 在把自己的资源释放了
}
}
~shared_ptr()
{
release();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_count = sp._count;
_del = sp._del;
++(*_count);
}
return *this;
}
// 移动赋值
shared_ptr<T>& operator=(shared_ptr<T>&& sp)
{
if (this != &sp)
{
release();
_ptr = sp._ptr;
_count = sp._count;
_del = move(sp._del);
sp._ptr = nullptr;
sp._count = nullptr;
}
return *this;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
int use_count() const
{
return _count ? *_count : 0; // 如果 _count 为空,返回 0
}
private:
T* _ptr;
int* _count;
function<void(T*)> _del = [](T* ptr) {delete ptr;}; // 删除器,并提供缺省值
};
测试代码:
cpp
int main()
{
// 构造
tr::shared_ptr<Date> sp1(new Date);
// 拷贝构造
tr::shared_ptr<Date> sp2(sp1);
cout << sp2.use_count() << endl;
tr::shared_ptr<Date> sp3(new Date);
cout << sp3.use_count() << endl;
// 拷贝赋值
sp3 = sp1;
cout << sp1.use_count() << endl;
cout << "------------------------------------------------------------" << endl;
// 移动构造
tr::shared_ptr<Date> sp4(move(sp1));
cout << sp4.use_count() << endl;
cout << sp1.use_count() << endl; // sp1已经悬空了(不应该继续使用sp1,这里输出0是因为在use_count 里面有判断)
// 移动赋值
sp4 = move(sp2);
cout << sp4.use_count() << endl; // sp2也悬空了,不再管理Date资源
cout << "------------------------------------------------------------" << endl;
tr::shared_ptr<Date> sp5(new Date[5], [](Date* ptr) {delete[] ptr; });
cout << sp5.use_count() << endl;
return 0;
}
运行结果:
四,shared_ptr和weak_ptr
(1)shared_ptr循环引用问题
如果节点有前后指针,prev
和next
:
- 当
n1
的next
指向n2
的时候,n2
的引用计数+1 == 2
- 当
n2
的prev
指向n1
的时候,n1
的引用计数+1 == 2
- 当
n1
和n2
析构的时候,引用计数分别-1
变成1
- 但是这时候,
n1
还依被prev
管理,只有prev
释放,n1
的引用计数才能到0
然后被释放 prev
什么时候被释放呢?只有当n2
被释放的时候才被释放,但是n2
被n1
的next
管理n1
的next
什么时候释放?只有n1
被释放才被释放。- 所以造成了循环。
(2)weak_ptr解决问题
weak_ptr
不支持RAII,也不⽀持访问资源,weak_ptr
构造时不支持绑定到资源,只支持绑定到shared_ptr
,绑定到shared_ptr
时,不增加shared_ptr
的引用计数weak_ptr
也没有重载operator*
和operator->
等,因为他不参与资源管理。- 如果
weak_ptr
绑定的shared_ptr
已经释放了资源(或者指向了别的资源),那么他去访问资源就是很危险的(悬空) expired()
可以用来判断weak_ptr
绑定的资源是否失效,失效时返回true
,没失效返回false
lock()
在weak_ptr
绑定的对象还存在的时候,weak_ptr.lock()
可以返回⼀个管理资源的shared_ptr
,这样weak_ptr
就可以访问资源,并且就算原来的shared_ptr
改变了,也有这个新的shared_ptr
(相当于这就是用weak_ptr
创建了一个新的shared_ptr
,引用计数会增加)
weak_ptr使用示例
使用示例:
cpp
int main()
{
shared_ptr<string> sp1(new string("111111"));
shared_ptr<string> sp2(sp1);
weak_ptr<string> wp = sp1;
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
cout << "-------------------------------------------------" << endl;
wp = sp1; // 重新绑定
//shared_ptr<string> sp3 = wp.lock();
cout << wp.use_count() << endl;
auto sp3 = wp.lock(); // 利用weak_ptr自己创建一个shared_ptr
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}
运行结果:
五,share_ptr线程安全问题
这里简单讲解一下,暂时不做具体讲解。
shared_ptr
的引用计数对象在堆上,如果多个shared_ptr
对象在多个线程中同时访问修改引用计数,就会存在线程安全问题。- 即:原来是引用计数是
1
,两个线程同时访问增加:理论上结果应该是1 + 1 + 1 == 3
,但是如果两个线程同时拿,拿到的都是1
,则变成两个1 + 1 == 2
出现了错误 - 解决方法:引用计数加锁 或原子操作
atomic<int>
- 说明一下:智能指针本身是线性安全的,但是指向的资源不是线性安全的。(智能指针指向的资源的线程安全性取决于资源本身的实现,与智能指针无关)
六,内存泄漏问题
内存泄漏:
- 因为疏忽或错误造成程序未能释放已经不再使⽤的内存。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:
- 普通程序:运行完就结束。不害怕内存泄漏问题,因为进程正常结束,页表的映射关系解除,物理内存也可以释放。
- 长期运行的程序:如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。
如何检测?
- 用检测工具
如何预防?
- 程序员编写代码时,正确使用智能指针,事前预防
- 事后排错,用检测工具
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!