文章目录
- 1.为什么需要智能指针?
- 2.智能指针原理
-
- [2.1 RAll](#2.1 RAll)
- [2.2 像指针一样使用](#2.2 像指针一样使用)
- 3.C++11的智能指针
-
- [3.1 auto_ptr](#3.1 auto_ptr)
- [3.2 unique_ptr](#3.2 unique_ptr)
- [3.3 shared_ptr](#3.3 shared_ptr)
- [3.4 weak_ptr](#3.4 weak_ptr)
- 4.删除器
- 希望读者们多多三连支持
- 小编会继续更新
- 你们的鼓励就是我前进的动力!
智能指针是 C++ 中用于自动管理动态内存的类模板,它通过 RAII(资源获取即初始化)技术避免手动 new / delete 操作,从而显著减少内存泄漏和悬空指针的风险
1.为什么需要智能指针?
cpp
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
cpp
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 像指针一样使用
都叫做智能指针了,那肯定是可以当作指针一样使用了,指针可以解引用,也可
以通过 -> 去访问所指空间中的内容,因此类中还得需要将 * 、-> 重载下,才可让其像指针一样去使用
cpp
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
C++ 98中产生了第一个智能指针auto_ptrC++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptrC++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的
3.1 auto_ptr
cpp
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
cpp
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
cpp
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 其实也会有疏漏,比如:引用循环
cpp
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 ),最终导致这两个节点所占用的资源都无法被释放,造成内存泄漏
cpp
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.删除器
cpp
#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 出来的对象,因此利用仿函数设置了删除器,这样就可以调用对应的删除
希望读者们多多三连支持
小编会继续更新
你们的鼓励就是我前进的动力!
