什么是智能指针?为什么要引入它呢?在此之前,先来看一段代码:
void test()
{
int* p1 = new int;
int* p2 = new int;
delete p1;
delete p2;
}
C++内存分配中,使用 new 操作符,如果分配失败,可能会抛出 std::bad_alloc 异常。那么请想一想:
如果p1抛出异常,则不会有问题出现。而如果p2申请时抛出异常
,那么就会导致p1申请的空间无法释放,导致内存泄漏
。
鉴于异常导致执行流乱跳,可能造成内存泄漏,我们期望一种“智能”的指针,可以在抛异常时,自动释放已申请的空间
。
智能指针的核心思想,就是 RAII。
RAII
(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
}
private:
T* _ptr;
};
void TestSmartPtr()
{
SmartPtr<int> sp1 = new int;
SmartPtr<int> sp2 = new int;
}
我们将申请的空间托管给 SmartPtr 对象,申请空间时构造对象,对象析构时释放空间。这样,就可以在 new 抛异常时,伴随着对象的析构而自动释放空间。
除了最核心的 RAII 思想,我们还要实现指针的特性,让类能拥有指针的行为,即为重载指针运算符
。
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
void TestSmartPtr()
{
SmartPtr<int> sp1 = new int;
*sp1 = 2;
SmartPtr <pair<int, string>> sp2 = new pair<int, string>(1, "Black Myth:");
sp2->first += 2;
sp2->second += "WuKong";
cout << sp2->first << ":" << sp2->second << endl;
}
我们重载了*
操作符和->
操作符,使得 SmartPtr 类拥有了和指针一样的行为。这点早在迭代器的实现时,便已经详细讲解过。
智能指针的难点,便是拷贝问题。
void TestSmartPtr()
{
SmartPtr<int> sp1 = new int;
SmartPtr<int> sp2 = sp1;
}
在上述代码的拷贝构造中,以往常规意义的浅拷贝和深拷贝都不对:
而我们接下来介绍的智能指针,将围绕这点来展开讨论。
C++98 时,提供了第一个智能指针auto_ptr
。
auto_ptr 原理:独占资源的所有权
,并且在拷贝时转移资源的所有权。
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~auto_ptr()
{
delete _ptr;
}
auto_ptr(auto_ptr<T>& ap)
: _ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
void test_auto_ptr()
{
auto_ptr<int> ap1 = new int;
auto_ptr<int> ap2 = ap1;//所有权转移
*ap1 = 1;//悬空指针
}
auto_ptr 在拷贝时,将资源的所有权转移,从而使自身置空。这种行为虽然可以防止多个 auto_ptr 同时释放同一块内存,但是由于悬空指针的情况出现,在后续的代码中极易出现访问空指针的错误。
由于其设计上的一些缺陷和危险性
,它在 C++11 中被弃用
,并最终在 C++17 中被完全移除
。
C++11 时,新增了unique_ptr
、shared_ptr
和weak_ptr
等智能指针,以代替auto_ptr。
unique_ptr 原理:独占资源的所有权
,并禁止任何拷贝。
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~unique_ptr()
{
delete _ptr;
}
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
void test_unique_ptr()
{
unique_ptr<int> up1 = new int;
//unique_ptr up2 = up1;//独占所有权
*up1 = 1;
}
unique_ptr 通过显式 delete 拷贝构造和赋值重载,使得其对象不能进行任何拷贝,只能进行移动。
它是 auto_ptr 的直接替代品,推荐用于管理独占所有权的动态内存。
shared_ptr 原理:共享资源的所有权
,通过引用计数
来实现,多个指针可以共享同一个资源,当最后一个指针销毁时,资源才会被释放。
如何实现引用计数呢?常规的 int 类型的成员变量或者静态成员变量都不行:
所以,我们转换角度,引用计数不应该与对象或类挂钩,而是与资源相挂钩。
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~shared_ptr()
{
release();
}
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)//防止同一资源的指针相互赋值
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
private:
void release()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
T* _ptr;
int* _pcount = new int(1);//指向当前资源的指针数
};
void test_shared_ptr()
{
shared_ptr<int> sp1 = new int;
shared_ptr<int> sp2 = sp1;//共享所有权
shared_ptr<int> sp3 = new int;
sp3 = sp2;
}
我们为了实现引用计数,新增一个成员变量 _pcount,指向当前资源的计数(表示有多少指针共享这个资源)。
值得一提的是,赋值重载的判断,不再是以对象来判断(this != &sp),而是以资源来判断(_ptr != sp._ptr),防止同一资源的指针相互赋值。跨资源赋值时,先将当前资源 release(计数减1,如果为0则释放资源),再指向目标资源,增加计数。
但是,shared_ptr 的引用计数,会出现一种问题——循环引用
。
template<class T>
struct ListNode
{
T _val;
shared_ptr<ListNode<T>> _prev;
shared_ptr<ListNode<T>> _next;
ListNode(const T& val = T())
: _val(val)
{}
};
void test_shared_ptr()
{
shared_ptr<ListNode<int>> n1 = new ListNode<int>;
shared_ptr<ListNode<int>> n2 = new ListNode<int>;
//循环引用
n1->_next = n2;
n2->_prev = n1;
}
在上述代码中:
这就是循环引用,这会导致引用计数不会减到0,从而内存泄漏
。
为了解决 shared_ptr 的循环引用问题,weak_ptr 应运而生。
weak_ptr 原理:共享资源的所有权
,但是一种弱引用
,不参与引用计数,不管理资源的生命周期。
template<class T>
class weak_ptr
{
public:
weak_ptr()
: _ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
: _ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
template<class T>
struct ListNode
{
T _val;
weak_ptr<ListNode<T>> _prev;
weak_ptr<ListNode<T>> _next;
ListNode(const T& val = T())
: _val(val)
{}
};
void test_shared_ptr()
{
shared_ptr<ListNode<int>> n1 = new ListNode<int>;
shared_ptr<ListNode<int>> n2 = new ListNode<int>;
n1->_next = n2;
n2->_prev = n1;
}
正因其作用就是配合 shared_ptr 打破循环引用,所以 weak_ptr 没有原生指针的构造函数,只有默认构造和 shared_ptr 的构造函数。
将节点内的指针换为 weak_ptr,则让两个节点的计数均为1,当 n1 和 n2 析构时,计数便能正常减到0,从而释放节点空间。
试想一下,如果不是用 new 来申请资源,应该如何进行正确地释放资源呢?这就涉及到定制删除器的设计。
默认情况下,我们应该用 delete 进行释放,而对于特殊的资源申请方式,我们要传对应的删除器(如函数对象、lambda表达式等)进行特定的删除。
unique_ptr
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr, function<void(T*)> del = [](T* ptr) {delete ptr; })
: _ptr(ptr)
, _del(del)
{}
~unique_ptr()
{
_del(_ptr);
}
//...
private:
T* _ptr;
function<void(T*)> _del;
};
template<class T>
struct DelArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
void test_unique_ptr()
{
unique_ptr<ListNode<int>> up1(new ListNode<int>[10], DelArray<ListNode<int>>());
unique_ptr<ListNode<int>> up2(new ListNode<int>[10], [](ListNode<int>* ptr) {delete[] ptr; });
unique_ptr<FILE> up3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
unique_ptr<ListNode<int>> up4(new ListNode<int>);
}
shared_ptr
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr, function<void(T*)> del = [](T* ptr) {delete ptr; })
: _ptr(ptr)
, _del(del)
{}
~shared_ptr()
{
release();
}
//...
private:
void release()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
}
}
T* _ptr;
int* _pcount = new int(1);
function<void(T*)> _del;
};
template<class T>
struct DelArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
void test_shared_ptr()
{
shared_ptr<ListNode<int>> sp1(new ListNode<int>[10], DelArray<ListNode<int>>());
shared_ptr<ListNode<int>> sp2(new ListNode<int>[10], [](ListNode<int>* ptr) {delete[] ptr; });
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
shared_ptr<ListNode<int>> sp4(new ListNode<int>);
}
我们为了实现定制删除器,新增了一个成员变量 _del,由于参数类型无法显式定义,所以使用function
函数包装器,默认缺省为 delete 的lambda
表达式。
同时,默认构造函数简化为一个双参数构造函数,可以自由选择是否显式传入自定义删除器。
C++智能指针的优势: