目录
一: 🔥 为什么需要智能指针?
💢 下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析MergeSort函数中的问题。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
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抛异常会如何?
异常会传播到 main() 的 try-catch 块中。由于p1和p2都未成功分配,因此不
会调用delete
。
- 如果p2这里new抛异常会如何?
异常同样传播到 main() 的 try-catch 块中。p1已分配但p2未分配,p1的delete调用不会执行,可能导致
内存泄漏
。
- 如果div调用这里又会抛异常会如何?
异常传播到 main() 的 try-catch 块中。p1和p2都已分配但delete调用不会执行,导致
内存泄漏
。
💢 在这三种情况下,如果 new 或 div() 抛出异常,并且没有适当的异常处理来释放已分配的资源,就可能导致内存泄漏
。为了避免这种情况,可以使用智能指针
(如std::unique_ptr)来自动管理内存。
二: 🔥 内存泄漏
2.1 📖 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,
失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,
出现内存泄漏会导致响应越来越慢,最终卡死。
三: 🔥 智能指针的使用及原理
📖 3.1 RAII
💢 RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
获取到资源以后去初始化一个对象,将资源交给对象管理:
资源获取即初始化
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处
:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
💢 如下我们来使用 RAII 的思想 设计一个智能指针SmartPtr :
//实现一个最简易的智能指针
template<class T>
class smart_ptr
{
public:
smart_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
//对象析构时自动释放所管理的资源
~smart_ptr()
{
cout << "delete " << _ptr << endl;
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
private:
T* _ptr;
};
- 💢 此时,我们使用智能指针来代替裸指针,并让它发生除0错误:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw invalid_argument("除0错误");
}
return a / b;
}
void func()
{
smart_ptr<int> p1(new int);
smart_ptr<int> p2(new int);
cout << div() << endl;
}
int main()
{
try
{
func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
💢 可以看到,刚才由于抛异常未能释放的资源现在可以正常释放。
📖 3.2 智能指针的原理
💢 上述的 SmartPtr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
template<class T>
class smart_ptr
{
public:
smart_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~smart_ptr()
{
cout << "delete " << _ptr << endl;
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*() const
{
return *_ptr;
}
T* get() const
{
return _ptr;
}
T* operator->() const //T* = const T* _ptr
{
return _ptr;
}
private:
T* _ptr;
};
💢 总结一下智能指针的原理:
- RAII特性。
- 重载operator*和opertaor->,具有像指针一样的行为。
📖 3.3 std::auto_ptr
💢 std::auto_ptr文档
💢 auto_ptr是c++98版本库中提供的智能指针,该指针解决上诉的问题采取的措施是管理权转移的思想
,也就是原对象拷贝给新对象的时候,原对象就会被设置为nullptr
,此时就只有新对象指向一块资源空间。
- 如果auto_ptr调用拷贝构造函数或者赋值重载函数后,如果再去使用原来的对象的话,那么整个程序就会崩溃掉(因为原来的对象被设置为nullptr),这对程序是有很大的伤害的.所以很多公司会禁用auto_ptr智能指针。
结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。
📖 3.4 std::unique_ptr
unique_ptr文档
💢 unique_ptr是c++11版本库中提供的智能指针,它直接将拷贝构造函数和赋值重载函数给禁用掉
,因此,不让其进行拷贝和赋值。
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原
理
// C++11库才更新智能指针实现
// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace bit
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}
📖 3.5 std::shared_ptr
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
share_ptr是c++11版本库中的智能指针,shared_ptr允许多个智能指针可以指向同一块资源,并且能够保证共享的资源只会被释放一次,因此是程序不会崩溃掉。
3.5.1. shared_ptr的原理
shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源:
- shared_ptr在其内部,
给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 当一个
shared_ptr对象被销毁时(调用析构函数)
,析构函数内就会将该计数减一。 如果引用计数减为0后
,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
如果引用计数不是0
,就说明自己还有其他对象在使用,则不能释放该资源
,否则其他对象就成为野指针。- 引用计数是用来记录资源对象中有多少个指针指向该资源对象。
💢 以下是shared_ptr的模拟实现
template<class T>
class share_ptr
{
public:
share_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
share_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
share_ptr(const share_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
void release()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
// 赋值重载 需要先将已有的资源释放
share_ptr<T>& operator=(const share_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
~share_ptr()
{
release();
}
T* get()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T* ptr)> _del = [](T* ptr) {delete ptr; };
};
3.5.2 std::shared_ptr的循环引用
💢 shared_ptr固然好用,但是它也会有问题存在。假设我们要使用定义一个双向链表,如果我们想要让创建出来的链表的节点都定义成shared_ptr智能指针,那么也需要将节点内的_pre和_next都定义成shared_ptr的智能指针。如果定义成普通指针,那么就不能赋值给shared_ptr的智能指针。
💢 循环引用分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
-
💢 原理就是,node1->_next = node2; 和node2->_prev = node1; 时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
-
💢 weak_ptr对象指向shared_ptr对象时,不会增加shared_ptr中的引用计数,因此当node1销毁掉时,则node1指向的空间就会被销毁掉,node2类似,所以weak_ptr指针可以很好解决循环引用的问题。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
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;
}
四: 🔥 定制删除器
- 如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题。
当我们释放一个指向数组的指针的时候,delete[]后面的空方括号是必须存在(如下),它指示编译器此指针指向的是一个对象数组的第一个元素,如果我们在delete一个指向数组的指针中忽略了方括号,我们的程序可能在执行过程中在没有任何警告下行为异常。
如果我们打开一个了文件,返回一个文件指针,让一个shared_ptr对象去指向该文件,那么在调用析构函数的时候就不能采用delete方法,而是使用flose()函数去关闭该文件。
因此,shared_ptr 类中提供了一个构造函数可以自定义一个删除器去指定析构函数的删除方式。
template <class U, class D> shared_ptr (U* p, D del);
- 这个自定义删除器可以是函数指针,仿函数,lamber,包装器。
// 仿函数的删除器
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main()
{
FreeFunc<int> freeFunc;
std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });
std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p){fclose(p); });
return 0;
}
五: 🔥 智能指针常考面试题
1. 什么是智能指针?它们的优点是什么?
- 智能指针是对象,它们封装了原始指针,并提供了自动内存管理功能。
- 优点包括:自动释放资源,防止内存泄漏,减少悬挂指针,提高代码的可维护性和安全性。
2. 智能指针为什是对象?
-
封装性: 智能指针封装了原始指针以及其他与内存管理相关的功能,例如
引用计数(对于 shared_ptr),删除器(对于自定义删除器的智能指针)
等等。 -
构造函数和析构函数: 智能指针具有构造函数和析构函数,这些构造函数和析构函数在智能指针的生命周期内管理资源的分配和释放。例如,
shared_ptr 会在对象被销毁时自动减少引用计数,并在引用计数为零时释放资源。
-
成员函数:智能指针提供了一些成员函数,用于访问和管理其封装的指针。例如,unique_ptr 提供 release、reset、get 等成员函数,shared_ptr 提供 use_count、unique、reset 等成员函数。
-
运算符重载: 智能指针重载了一些运算符,使其使用起来更像原始指针。例如,operator* 和 operator-> 允许智能指针像原始指针一样使用。
3. C++中有哪些智能指针?它们的用途是什么?
std::unique_ptr
: 提供独占所有权的智能指针,适用于单一所有权场景。std::shared_ptr
: 提供共享所有权的智能指针,适用于多个对象共享同一资源的场景。std::weak_ptr
: 用于解决shared_ptr的循环引用问题,不参与引用计数。
4. std::shared_ptr 如何实现引用计数?
std::shared_ptr
内部维护一个引用计数,当一个新的shared_ptr对象指向同一个对象时,引用计数增加;当shared_ptr对象被销毁时,引用计数减少。引用计数为零时,释放所管理的对象。
5. 什么是std::weak_ptr?它的作用是什么?
std::weak_ptr
是一个不增加引用计数的智能指针,主要用于避免shared_ptr的循环引用问题。- weak_ptr 只能从shared_ptr 或另一个 weak_ptr 构造,可以使用 lock 方法提升为 shared_ptr。
6. 如何避免shared_ptr的循环引用?
- 使用std::weak_ptr 打破循环引用。例如,在双向链表或树结构中,父节点使用shared_ptr指向子节点,而子节点使用weak_ptr指向父节点。
7. std::make_shared与直接使用构造函数创建shared_ptr有何不同?
- std::make_shared会在单个内存块中分配对象和引用计数,具有更好的性能和更少的内存碎片。
- 使用构造函数时,引用计数和对象可能分配在不同的内存块中。
以上就是我对 【C++】智能指针 的理解,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉