C++智能指针是一种用于管理动态分配的内存资源的指针类。它们提供了自动内存管理的功能,可以避免内存泄漏和悬空指针问题。
目录
一、RAII思想
RAII(Resource Acquisition Is Initialization)是一种C++编程范式,在C++中主要用来管理资源,智能指针是一种RAII的实现方式,在C++中用于管理动态分配的内存。智能指针会自动管理内存的生命周期,在指针的作用域结束时自动释放资源,从而避免内存泄漏和野指针的问题。
二、智能指针的优势
智能指针的核心思想:就是在构造函数中给指针分配资源(比如动态分配的内存),而在析构函数中释放指针指向的资源。从而实现对资源的自动管理。在一些特殊情况智能真有绝对的优势,如下简单的小例子。
#include <iostream>
#include <memory>
using namespace std;
void divide(int a, int b) {
//int *p=new int(1);
unique_ptr<int> p = make_unique<int>(1);
if (b == 0) {
throw runtime_error("Division by zero error");
}
//delete p;
cout << "Result: " << a / b << endl;
}
int main() {
try
{
divide(10, 0);
} catch (std::runtime_error& e) {
cout << "Exception caught: " << e.what() << endl;
}
return 0;
}
在上面的代码中,如果使用普通指针int *p = new int(1)
,当抛出异常时,会导致程序直接跳转到main
函数中,打破正常代码执行流,p
指向的资源没有被释放(对应的delete p;语句没有被执行),从而导致内存泄漏。
而如果使用智能指针unique_ptr<int> p = make_unique<int>(1)
,当抛出异常时,智能指针p
离开其作用域,会自动调用析构函数从而资源的自动释放,从而避免了内存泄漏问题。
三、智能指针的理解
1.智能指针的说明和分类
- auto_ptr(c++98)标准:auto_ptr在进行指针拷贝时会出现资源转移的问题(现在几乎没有使用,可以完全用下面的智能指针替代)。
- unique_ptr: 独占所有权,确保只有一个指针指向对象。简单理解,直接禁用了拷贝构造和赋值操作符。从而实现这个指针独占此资源。
- shared_ptr: 允许多个指针共享对象的所有权,引用计数管理生命周期。可能会出现循环引用的问题,可以用weak_ptr来解决此问题。
- weak_ptr: 解决shared_ptr可能的循环引用问题。
2.auto_ptr的理解
template<class T>
class auto_ptr
{
public:
//RAII思想
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)//_ptr不等于空再去释放资源
{
cout << "~auto_ptr()"<<"->" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
//像指针一样访问
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
如:ap2(ap1),auto_ptr 解决拷贝问题,通过管理权的转移,把拷贝出来的ap2指向那块资源把ap1置空,所以并没有实现两个指针指向同一块资源。两个指针并没有指向同一块空间,可能出现问题,所以几乎没有再使用了(在C++17标准中已经删除)。
3.unique_ptr
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "~auto_ptr()" << "->" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
unique_ptr主要是禁用了禁用了拷贝构造和赋值操作符。实现了资源的独享。
4.shared_ptr
template<class T>
class shared_ptr //引用计数随着资源走 所以在构造中new出来引用计数
{
public:
shared_ptr(T* ptr)
:_ptr(ptr),_pcount(new int(1))
{}
template<class D>
shared_ptr(T* ptr,D del)
: _ptr(ptr)
, _pcount(new int(1))
,_del(del)
{}
//function<void(T*)> _del;
void release()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
//sp1=sp2
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)//避免sp1=sp1;或者两个指针指向同一份资源的情况
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
int use_count() const
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get_ptr() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
1.循环引用问题
shared_ptr因为是采用引用计数来决定指针指向资源的释放,在一些特殊的场景下会出现循环引用的问题,如下的例子中会出现循环引用问题。
#include<iostream>
using namespace std;
struct ListNode
{
int _val;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
//weak_ptr<ListNode> _prev;
//weak_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr()
{
shared_ptr<ListNode>n1 = make_shared<ListNode>();
shared_ptr<ListNode>n2 = make_shared<ListNode>();
//循环引用
n1->_next = n2;
n2->_prev = n1;
}
int main()
{
test_shared_ptr();
return 0;
}
代码运行结果:(可见并没有打印析构的语句,所以可知节点资源并没有被释放造成了内存泄漏)
此例子中的循环引用,简单来说就是,在代码中创建两个ListNode对象 n1 和 n2,并且相互之间存在引用关系,这种循环引用会导致问题,导致智能指针中的引用计数无法降至零,导致内存泄漏。
解决方法可以通过weak_ptr去接收shared_ptr的赋值,weak_ptr并不会增加由shared_ptr赋值的引用计数。(不会把图中的引用计数1改变成2) 所以就可以解决循环引用的问题了,资源可以正常释放了。
5.weak_ptr
weak_ptr
是 C++11 标准引入的一种智能指针,用于解决 shared_ptr
的循环引用问题。weak_ptr
允许访问一个由 shared_ptr
管理的对象,但不会增加该对象的引用计数,常用于解决 shared_ptr循环引用的问题。weak_ptr
本身并不拥有所指向对象的所有权,它仅仅是对shared_ptr
指向的对象的一个观察者,并非资源的所有者。
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get_ptr())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get_ptr();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
四、定制删除器
上述代码中,我们所有析构函数中我们都是用的delete去释放资源,那假如我们开辟的一块空间呢,本来应使用delete[ ]去释放空间,而我们缺使用了delete去释放资源那么的话就会发生错误。C++标准库中是通过定制删除器去实现对智能指针的指向资源的删除。同时也允许用户自定义
删除器来指定释放资源的方式。
#include<iostream>
#include<memory>
void Test_shared_ptr()
{
std::shared_ptr<int> sp1(new int[10], [](int* ptr) {delete[] ptr; });
std::shared_ptr<int> sp2(new int[10]); //没有指定删除器
}
int main()
{
Test_shared_ptr();
return 0;
}
从C++17开始,std::shared_ptr
能够根据所管理的对象是否为数组来自动选择正确的删除器。对于非数组类型的动态分配,默认调用 delete
;而对于数组类型(如 new int[10]
),则默认调用 delete[]。
所以当编译器支持C++17标准,如果是数组类型不指定删除器并不会出错。
在C++17之前,std::shared_ptr
无法自动识别动态分配的数组类型,因此会使用delete
而不是delete[]
,这可能导致未定义的行为或内存泄漏。
总结
本文分享了一些本人对智能指针知识的理解,希望对大家的学习有所帮助,如有错误请大佬指出,万分感谢。