一、RAII和智能指针的设计思路
RAII(全称Resource Acquisition Is Initialization),是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄露。这里的资源可以是内存、文件指针、网络连接、互斥锁等等,RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终有效,最后在对象析构时释放资源,这样保障了资源的正常释放,避免资源泄露的问题。
智能指针除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会像迭代器类一样,重载operator*、operator->、operator[]等运算符,方便访问资源。
二、C++标准库智能指针的使用
C++标准库中的智能指针都在<memory>这个头文件下面,智能指针有好几种,原理上而言主要是解决智能指针拷贝时的思路不同。
auto_ptr是C++98设计的智能指针,它的特点是拷贝时把被拷贝对象的资源管理权转移给拷贝对象。这是一个非常糟糕的设计,因为它会把被拷贝对象悬空,建议不要使用。
#include <iostream>
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{ }
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
auto_ptr<Date> ap1(new Date);
ap1->_year++;
//管理权转移,被拷贝对象悬空
auto_ptr<Date> ap2(ap1);
ap2->_year++;
//ap1->_year++;
return 0;
}
unique_ptr是C++11设计出来的指针,翻译出来就是唯一指针。它的特点是不支持拷贝,只支持移动。在不需要拷贝的场景中推荐使用。
#include <iostream>
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{ }
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
unique_ptr<Date> up1(new Date);
//禁止拷贝
//unique_ptr<Date> up2(up1);
//可以移动
unique_ptr<Date> up3(move(up1));
return 0;
}
shared_ptr是C++11设计出来的智能指针,翻译出来就是共享指针。它的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用,底层是用引用计数的方式实现的。
#include <iostream>
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{ }
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
//可以拷贝可以移动,通过底层引用计数实现
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
//移动会导致sp1管理的资源被转移,sp1悬空
shared_ptr<Date> sp3(move(sp1));
cout << sp1 << endl;
cout << sp2 << endl;
cout << sp3 << endl;
return 0;
}

share_ptr除了支持用指向资源的指针构造,还支持make_shared用初始化资源对象的值直接构造。

#include <iostream>
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{ }
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
shared_ptr<Date> sp1(new Date(2025, 11, 19));
//shared_ptr<Date> sp2 = make_shared<Date>(2025, 11, 19);
auto sp2 = make_shared<Date>(2025, 11, 19);
return 0;
}
shared_ptr和unique_ptr都支持了operator bool的类型转换。如果智能指针对象是一个空对象,没有管理资源,则返回false,否则返回true。意味着我们可以直接把智能指针对象给if判断是否为空。
#include <iostream>
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{ }
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(move(sp1));
if (!sp1)
{
cout << "sp1 空" << endl;
}
//if (sp2.operator bool())
if (sp2)
{
cout << "sp2 非空" << endl;
}
return 0;
}
智能指针析构时默认进行delete释放资源,这也就意味着如果不是new出来的资源交给智能指针管理的话,析构时就会崩溃。智能指针支持在构造时给一个删除器(本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式),当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。又因为new[]经常使用,所以unique_ptr和shared_ptr都特化了一份[]的版本。

#include <iostream>
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{ }
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
unique_ptr<Date[]> up1(new Date[10]);
shared_ptr<Date[]> sp1(new Date[10]);
return 0;
}
注意:unique_ptr和shared_ptr支持删除器的方式有所不同,unique_ptr是在类模板参数支持的,shared_ptr是在构造函数参数支持的。
#include <iostream>
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{ }
~Date()
{
cout << "~Date()" << endl;
}
};
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
unique_ptr<Date[]> up1(new Date[10]);
shared_ptr<Date[]> sp1(new Date[10]);
//定制删除器
shared_ptr<Date> sp2(new Date[10], DeleteArray<Date>());
shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr;});
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
auto del = [](Date* ptr) {delete[] ptr;};
unique_ptr<Date, decltype(del)> up3(new Date[5], del);
//C++20支持
//unique_ptr<Date, decltype(del)> up3(new Date[5]);
return 0;
}
shared_ptr和unique_ptr的构造函数都使用了explicit修饰,防止普通指针隐式类型转换成智能指针对象。也就是不支持shared_ptr<Date> sp3 = new Date 这样的写法。
三、智能指针的模拟实现
3.1 核心框架
这里重点就是shared_ptr的设计,尤其是引用计数的设计该如何实现。
有人可能会直接这样设计:
template<class T>
class shared_ptr
{
public:
private:
T* _ptr;
int _ref_count;
};
这样设计肯定是不对的,这样就是每个对象都是各自单独的引用计数,互不影响。而我们期望的是共同拥有一份引用计数,那这个成员变量能否设计为static呢?
答案还是不行!static成员变量属于这个类的所有对象,而我们要的是一个资源拥有一份引用计数,另一个新的资源也有一份单独的引用计数。所以,这里要使用堆上动态开辟的方式。
template<class T>
class shared_ptr
{
public:
private:
T* _ptr;
int* _pcount;
};
构造智能指针对象时,来一份资源,就new一个引用计数出来。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源。
大体的逻辑框架如下:
namespace zyc
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
{ }
//sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
~shared_ptr()
{
//最后一个管理资源的对象释放
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
private:
T* _ptr;
int* _pcount;
};
}
3.2 其它接口
这里最主要的就是赋值运算符重载。假设我们要把sp3赋值给sp1,该怎么实现呢?首先要先把sp1析构,那是不是上来就直接delete呢,并不是,我们要先把sp1的引用计数--,如果sp1是最后一个管理资源的指针,才需要delete掉。然后再把资源拷贝过来,让引用计数++。
同样地,这里也可以判断一下自己给自己赋值的情形。以往我们判断是否是自己给自己赋值都是这样写的:if(this != &sp)。但是这里不要忘了,假如sp1和sp2不是同一个对象,但它们都共同管理着同一份资源,也是一种自己给自己赋值,所以这里不能用这种方式判断。
void release()
{
//最后一个管理资源的对象释放
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
//sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
完整的模拟实现如下(如果面试时需要模拟实现一份智能指针的话,这样就已经够了):
namespace zyc
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{ }
//sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
void release()
{
//最后一个管理资源的对象释放
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
//sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
~shared_ptr()
{
release();
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](int i)
{
return _ptr[i];
}
private:
T* _ptr;
int* _pcount;
};
}
3.3 支持定制删除器
如果要支持定制删除器的话,第一时间想到的写法是下面这种:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{ }
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del)
{ }
// ...
};
但是这么写就面临一个问题,成员变量_del该如何定义?有人可能会想,直接写成D _del不就行了吗?注意,这里的D类型并不属于整个类,而是属于shared_ptr的构造函数的。那该怎么办呢,难道也要像unique_ptr一样写到整个类吗?没有必要,这里虽然用不了D这个类型,但是我们可以定义一个function进行包装。
private:
T* _ptr;
int* _pcount;
//D _del;
function<void(T*)> _del = [](T* ptr) {delete ptr;};
完整实现代码如下:
namespace zyc
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{ }
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del)
{ }
//sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
++(*_pcount);
}
void release()
{
//最后一个管理资源的对象释放
if (--(*_pcount) == 0)
{
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
//sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr()
{
release();
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](int i)
{
return _ptr[i];
}
private:
T* _ptr;
int* _pcount;
//D _del;
function<void(T*)> _del = [](T* ptr) {delete ptr;};
};
}
四、weak_ptr
4.1 shared_ptr循环引用的问题
shared_ptr大多数情况下管理资源都非常合适,支持RAII,也支持拷贝。但是在循环引用场景下会导致资源没得到释放,出现内存泄漏的问题。
比如我们来看下面这段代码:
#include <iostream>
#include <memory>
using namespace std;
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _prev;
std::shared_ptr<ListNode> _next;
ListNode(int val)
:_data(val)
,_prev(nullptr)
,_next(nullptr)
{ }
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode(1));
std::shared_ptr<ListNode> n2(new ListNode(2));
n1->_next = n2;
n2->_prev = n1;
return 0;
}
编译运行之后,我们可以发现,出现内存泄漏了。

但是去掉其中一个,代码又不会出现内存泄漏了。

那么为什么会出现这样的情况呢?


至此逻辑上形成了一个死循环,谁都不会释放,形成了循环引用,导致内存泄漏。
4.2 weak_ptr

为了解决上面的问题,C++11设计出来了weak_ptr,翻译过来就是弱指针。它完全不同于之前所讲的智能指针,它不支持RAII,也就意味着不能用它直接管理资源。所以我们看文档发现,weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,当绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用的问题。
#include <iostream>
#include <memory>
using namespace std;
struct ListNode
{
int _data;
std::weak_ptr<ListNode> _prev;
std::weak_ptr<ListNode> _next;
ListNode(int val)
:_data(val)
{ }
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode(1));
std::shared_ptr<ListNode> n2(new ListNode(2));
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}

weak_ptr也没有重载operator*和operator->,因为它不参与管理资源。weak_ptr支持expired检查指向的资源是否过期,use_count也可以获取shared_ptr的引用计数。weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr。如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有被释放,则返回的shared_ptr访问资源就是安全的。
五、内存泄漏
内存泄漏是指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指物理意义上的消失,而是应用程序分配某段内存之后,因为设计错误,失去了对该段内存的控制,从而造成了内存的浪费。
1168

被折叠的 条评论
为什么被折叠?



