C++11新特性——智能指针
本文系统介绍了C++智能指针的核心概念与使用方法。首先分析了智能指针的使用场景和RAII设计思想,重点讲解了C++标准库中的unique_ptr(独占所有权)、shared_ptr(共享所有权)和weak_ptr(解决循环引用问题)三种智能指针的特性与适用场景。针对内存泄漏问题,文章阐述了其危害及预防措施,并提供了unique_ptr和shared_ptr的简化版实现代码。最后,通过具体示例展示了
目录
1 智能指针的使⽤场景分析
内存泄漏一直都是C++的一个很大的问题。因此在牵扯到C++的动态内存管理时,我们都会小心小心在小心,一定别忘了释放内存。然而,但凡牵扯到“别忘了”的解决方法都不是解决方法。因为你总有可能忘记。即使没有忘记,也有可能会出现内存泄漏。
void Func()
{
//申请内存
int* array = new int[10];
if (array == nullptr)
{
//抛出异常
throw exception();
}
//释放内存
delete[] array;
}
当异常抛出时,delete 将不会被执行,因此也会导致内存泄漏
我们知道当一个函数执行结束后函数栈帧将销毁,变量都会从栈帧中删除,因此动态申请的变量占据的内存将会被释放。如果动态申请的变量被释放时指向的内存也被释放,那该多好啊。这就是智能指针背后的思想与想要达到的效果
2 RAII和智能指针的设计思路
智能指针的行为类似于指针的类对象,但是这种对象还有其他功能
RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源。
RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀样,重载 operator*/operator->/operator[] 等运算符,⽅便访问资源。
RAII和智能指针的设计思路:
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{}
// 通过析构函数释放内存
~SmartPtr()
{
delete[] _ptr;
}
// 重载运算符,模拟指针的⾏为,⽅便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
//申请内存,通过智能指针进行管理
SmartPtr<int> array = new int[10];
我们可能会忘记释放申请的内存资源,但是编译器一定不会忘记。因为一个对象的析构函数在对象销毁时一定会进行调用,这正好对应智能指针背后的思想与想要达到的效果——动态申请的变量被释放时指向的内存也被释放
3 C++标准库智能指针的使⽤
C++标准库中的智能指针需要包含头文件 <memory>,该文件包含模板的定义。智能指针包含很多种,除了 weak_ptr 外都符合RALL和像指针一样访问的行为
当智能指针过期时,其析构函数将使用 delete 来自动释放内存。如果将动态申请的内存返回的地址赋值给这些对象,将无需自己释放这些内存,在智能指针销毁时,动态申请的内存将自动释放
int main()
{
// 创建智能指针
auto_ptr<int> ptr1(new int);
*ptr1 = 1;
cout << *ptr1 << endl;
// 拷贝
// 拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象
// 结果:被拷⻉对象悬空
auto_ptr<int> ptr2(ptr1);
cout << *ptr2 << endl;
// ptr1对象已经悬空,访问会直接报错
cout << *ptr1 << endl;
return 0;
}
C++11设计出新的智能指针后,强烈建议不要使⽤auto_ptr。其他C++11出来之前很多公司也是明令禁⽌使⽤这个智能指针的。
3.2 unique_ptr
unique_ptr 是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不⽀持拷⻉,只⽀持移动。如果程序不需要多个指向同一个对象的指针和不需要拷⻉的场景就⾮常建议使⽤他。
int main()
{
unique_ptr<int> up1(new int);
// 不⽀持拷⻉
unique_ptr<int> up2(up1);// 报错
up2 = up1;// 报错
// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
unique_ptr<int> up3(move(up1));
return 0;
}
3.3 shared_ptr
shared_ptr 是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉,也⽀持移动。如果程序要使用多个指向同一个对象的指针和需要拷⻉的场景就需要使用 shared_ptr。
shared_ptr 底层是⽤引⽤计数的⽅式实现的
int main()
{
shared_ptr<int> sp1(new int);
// ⽀持拷⻉
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(sp2);
shared_ptr<int> sp4 = sp3;
// shared_ptr 底层是⽤引⽤计数的⽅式实现的
// 打印当前引用计数的个数
cout << sp1.use_count() << endl;
// ⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎
shared_ptr<int> sp5(move(sp1));
return 0;
}
4 shared_ptr和weak_ptr
4.1 shared_ptr循环引⽤问题
shared_ptr ⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引⽤的场景和资源没释放的原因,并且学会使⽤ weak_ptr 解决这种问题。
以下类似场景使用shared_ptr都会引发循环引用问题:
struct ListNode
{
int _data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引⽤ -- 内存泄露
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;// 1
cout << n2.use_count() << endl;// 1
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;// 2
cout << n2.use_count() << endl;// 2
// weak_ptr不⽀持管理资源,不⽀持RAII
// weak_ptr是专⻔绑定shared_ptr,不增加他的引⽤计数,作为⼀些场景的辅助管理
//std::weak_ptr<ListNode> wp(new ListNode);
return 0;
}

观察打印结果可以很明显的发现——n1 和 n2 的析构函数并没有进行调用,这就是shared_ptr循环引⽤问题导致的内存泄漏
如下图所述场景,n1和n2销毁后,管理两个节点的引⽤计数减到1:

- 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
- _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
- 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
- _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
⾄此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏
此时就需要使用 weak_ptr 来解决问题,weak_ptr 就是为了处理这种场景而专门创造的
4.2 weak_ptr
weak_ptr 不⽀持RAII,也不⽀持访问资源,因此 weak_ptr 构造时不⽀持绑定到资源,只⽀持绑定到 shared_ptr,绑定到 shared_ptr 时,不增加 shared_ptr 的引⽤计数,那么就可以解决 shared_ptr 的循环引⽤问题
weak_ptr 也没有重载 operator* 和 operator-> 等,因为他不参与资源管理,那么如果他绑定的 shared_ptr 已经释放了资源,那么他去访问资源就是很危险的。weak_ptr ⽀持 expired 检查指向的资源是否过期,use_count 也可获取 shared_ptr 的引⽤计数,weak_ptr 想访问资源时,可以调⽤ lock 返回⼀个管理资源的 shared_ptr ,如果资源已经被释放,返回的 shared_ptr 是⼀个空对象,如果资源没有释放,则通过返回的 shared_ptr 访问资源是安全的
int main()
{
shared_ptr<string> sp1(new string("111111"));
shared_ptr<string> sp2(sp1);
weak_ptr<string> wp = sp1;
// 检查智能指针是否过期
cout << wp.expired() << endl;// 0
// 查看绑定的 shared_ptr 的引用计数
cout << wp.use_count() << endl;// 2
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;// 0
cout << wp.use_count() << endl;// 1
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;// 1
cout << wp.use_count() << endl;// 0
wp = sp1;
// 调⽤ lock 返回⼀个管理资源的 shared_ptr
shared_ptr<string> sp3 = wp.lock();
cout << wp.expired() << endl;// 0
cout << wp.use_count() << endl;// 2
*sp3 += "###";
cout << *sp1 << endl;// 222222###
return 0;
}
5 内存泄漏
5.1 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏?
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
内存泄漏的危害?
普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死。
int main()
{
// 申请⼀个1G未释放,这个程序多次运⾏也没啥危害
// 因为程序⻢上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
5.2 如何避免内存泄漏
- ⼯程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下⼀条智能指针来管理才有保证。
- 尽量使⽤智能指针来管理资源,如果⾃⼰场景⽐较特殊,采⽤RAII思想⾃⼰造个轮⼦管理。
- 定期使⽤内存泄漏⼯具检测,尤其是每次项⽬快上线前,不过有些⼯具不够靠谱,或者是收费。
- 总结⼀下:内存泄漏⾮常常⻅,解决⽅案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测⼯具
6 实现 unique_ptr 和 shared_ptr
namespace MySmartPtr
{
template<class T>
class unique_ptr
{
public:
//直接构造
unique_ptr(const T* x = nullptr)
:_ptr(x)
{}
//析构函数
~unique_ptr()
{
//释放资源
delete _ptr;
}
//禁用拷贝和赋值函数
unique_ptr(const unique_ptr<T>& x) = delete;
bool operator=(const unique_ptr<T>& x) = delete;
// 重载运算符,模拟指针的⾏为,⽅便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
//指向的资源
T* _ptr;
};
template<class T>
class shared_ptr
{
public:
// 直接构造,同时为引用计数申请资源
shared_ptr(T* x)
:_ptr(x)
,_pcount(new int(1))
{
//初始化定制删除器
_del = [](T* ptr) {
delete ptr;
};
}
// 直接构造+定制删除器
template <class D>
shared_ptr(T* x, D del)
:_ptr(x)
,_pcount(new int(1))
,_del(del)
{}
// 析构函数
~shared_ptr()
{
// 只有当引用计数为0时,才释放指向的资源
if ((--(*_pcount)) == 0)
{
// 释放资源
//delete _ptr;
// 使用定制删除器删除
_del(_ptr);
delete _pcount;
}
}
// 拷贝构造函数
shared_ptr(const shared_ptr<T>& x)
:_ptr(x._ptr)
,_pcount(x._pcount)
{
//引用计数增加
*(_pcount)++;
}
// 赋值运算符
shared_ptr<T>& operator=(const shared_ptr<T>& x)
{
//检查自己为自己赋值
if (_ptr != x._ptr)
{
//先检查之前指向的资源
if (--(*_pclose) == 0)
{
// 释放资源
//delete _ptr;
// 使用定制删除器删除
_del(_ptr);
delete _pcount;
}
_ptr = x._ptr;
_pcount = x._pcount;
//引用计数增加
*(_pcount)++;
}
return *this;
}
// 重载运算符,模拟指针的⾏为,⽅便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
// 指向的资源
T* _ptr;
// 引用计数
int* _pcount;
// 使用包装器包装定制删除器
function<void(T*)> _del;
};
}
更多推荐
所有评论(0)