* 和 -> 的重载,使 智能指针对象 具有指针的行为能力,能让用户像使用指针一样的使用。RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
我把四个智能指针的特点介绍在前面,你若还有什么细节问题再去具体的栏目下翻找吧~
auto_ptr:管理权转移,通过拷贝构造函数和赋值重载函数来实现。
unique_ptr:禁用拷贝构造和赋值构造
share_ptr:引用计数
weak_ptr:弱关联性
核心功能:管理权转移
管理权转移的同时也会导致 原指针悬空,容易造成野指针问题,不推荐使用。
namespace ttang
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 无力吐槽的神拷贝...
// 管理权转移:导致的是原来的指针悬空,很多公司明令禁止使用 auto_ptr
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
T* operator=(auto_ptr<T>& ap)
{
T* tmp = ap._ptr;
ap._ptr = nullptr;
return tmp;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
--------------------------------------------------
void test_auto()
{
auto_ptr<int> ap1(new int(1));
auto_ptr<int> ap2(ap1);
*ap1 = 1; // err...管理权转移以后导致ap1悬空,不能访问
*ap2 = 1;
}
}
核心功能:防拷贝(
= delete声明拷贝构造和复制重载)
unique_ptr 的指针,简单粗暴,是防了拷贝,不过也只解决了不需要拷贝的场景。
(ps:从 boost 里面吸收来的)
(pps:需要拷贝的场景就需要使用到接下来会介绍的 shared_ptr 和 weak_ptr 了)
namespace ttang
{
template<class T>
class unique_ptr
{
private:
T* _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;
}
// C++11思路:设置不许再实现了,语法直接支持的(不需要私有了)
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete; // 严格来说赋值也封了更好一点
// C++98思路:只声明不实现,但是用的人可能会在外面强行定义,所以再加一条,声明为私有
//private:
// unique_ptr(const unique_ptr& up);
// unique_ptr& operator=(const unique_ptr& up);
};
--------------------------------------------------
void test_unique()
{
unique_ptr<int> up1(new int(1));
unique_ptr<int> up2(up1); // err...
}
}
核心功能:引用计数
之前在一开始的概述部分介绍了两个智能指针为什么智能的原因,走到了 shared_ptr,我们的智能指针就真的更神了,他甚至还引申出 智能指针三大件 的说法:
shared_ptr 允许拷贝,意在允许多个智能指针可以指向同一块资源,并且能够保证共享的资源只会被释放一次,程序不会多次析构而崩溃。为了保证在最后一个智能指针使用完毕才释放,C++11 使用了引用计数的技术。
在具体对 shared_ptr 实现之前,对于这里的 引用计数,可以稍微探讨一下:
1)如果要使用引用计数,设置一个静态变量 count 行不行呢?不行,因为静态变量属于所有对象。而 每实例化一个对象都可能多有个资源,每个资源应该配对一个引用计数。
2)如果是多个线程去调用引用计数,还需要保证其线程安全,那就加个锁吧。
3)计数加锁后,shared_ptr 本身就会是线程安全的,但是他生成的对象不是线程安全的。
namespace ttang
{
template<class T>
class shared_ptr{
private:
T* _ptr; // 用指针,一个资源的多个指针要看见并修改这一个count
int* _pcount;
public:
// 【构造】
shared_ptr(T* ptr = NULL)
: _ptr(ptr)
, _pcount(new int(1))
{}
// 【拷贝构造】
shared_ptr(const shared_ptr& s)
: _ptr(s.ptr)
, _pcount(s._pcount)
{
*(_pcount)++;
}
// 【赋值重载】
/*
* 【1】正常赋值:
* sp1 = sp4; 被赋值sp1原来有资源,的肯定要把原来的资源--,sp4要++
* 【2】自己给自己赋值:
* sp1 = sp1; 自己给自己
* sp1 = sp2;(管理一个资源) 也是自己给自己,这时候 if(&sp != this) 就不得行了哦,防不了这一种
*/
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 检查不是自己赋给自己
if(this != &sp)
{
// 赋值之前查一下自己的_pcount,
if(--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = s._ptr;
_pcount = s._pcount;
*(_pcount)++;
}
return *this;
}
// 模拟指针行为,解引用 return 数据内容
T& operator*()
{
return *_ptr;
}
// 模拟指针行为,箭头指向 return 指针自己
T* operator->()
{
return _ptr;
}
//【析构】
~shared_ptr()
{
--(*_pcount);
if(*_pcount == 0)
{
delete _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
// 还可以扩展,看下面的代码示例
};
}
在上面的基础上:
namespace ttang
{
template<class T>
class shared_ptr
{
private:
T* _ptr;
int* _pcount;
mutex* _pmtx; // 锁也得是指针,因为是多个指针指向同一把锁
function<void(T*)> _del = [](T* ptr) { // 解决对 deletor 的保存问题,需要一个缺省的!!
cout << "lambda delete:" << ptr << endl;
delete ptr;
};
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1)) // 每个资源都分配一个引用计数count
, _pmtx(new mutex) // 每个资源都有一把锁,保证自己资源计数安全
{}
// 定制删除器(通过仿函数实现的!--是可调用对象,所以我们拿的一个function定义_del)
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
, _del(del)
{}
~shared_ptr()
{
Release();
}
void Release()
{
_pmtx->lock();
bool deleteFlag = false;
if (--(*_pcount) == 0)
{
if (_ptr)
{
//cout << "delete:" << _ptr << endl;
//delete _ptr;
_del(_ptr); // 如果_del 不给缺省的话,这里默认的构造可能会出问题
}
delete _pcount;
deleteFlag = true;
// delete _pmtx; 锁也要释放的呀,可以下面又要解锁。如何解决?
}
_pmtx->unlock();
if (deleteFlag)
{
delete _pmtx;
}
}
void AddCount()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
AddCount();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
AddCount();
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
};
void test_shared()
{
shared_ptr<int> sp1(new int(1));
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(sp2);
shared_ptr<int> sp4(new int(10));
//sp1 = sp4;
sp4 = sp1;
sp1 = sp1;
sp1 = sp2;
}
}
我们说 shared_ptr本身是线程安全的,因为计数是加锁保护的;
那 shared_ptr 管理的对象是否是线程安全呢?不安全。
如果需要多线程访问资源,需要程序员手动加锁。
🌰举例:
namespace ttang
{
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void SharePtrFunc(ttang::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
//cout << sp.get() << endl;
//cout << &sp << endl;
for (size_t i = 0; i < n; ++i)
{
// 智能指针拷贝会++计数,析构会--计数,这里是线程安全的。
ttang::shared_ptr<Date> copy(sp);
mtx.lock();
sp->_year++;
sp->_day++;
sp->_month++;
mtx.unlock();
}
}
void test_shared_safe()
{
ttang::shared_ptr<Date> p(new Date);
cout << p.get() << endl;
const size_t n = 10000;
mutex mtx;
thread t1(SharePtrFunc, ref(p), n, ref(mtx)); // 线程中以引用传递对象参数,必须加一个ref(),是一个库函数,否则会认为是传值传参会报错。
thread t2(SharePtrFunc, ref(p), n, ref(mtx)); // 13 底下可以检测,19 是直接报错
//cout << &p << endl;
t1.join();
t2.join();
cout << p.use_count() << endl;
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
}
}
这里举例一个 List 数据结构
我们按照传统通常这样去定义一个节点:
struct ListNode{
ListNode* _next;
ListNode* _prev;
// ...
};
然后这样去使用他:
ListNode* n1 = new ListNode;
ListNode* n2 = new ListNode;
n1->_next = n2;
n2->_prev = n1;
//...
如果在这里有异常抛出,后面的代码就不会执行了~
//...
delete n1;
delete n2;
但事实上,这样使用会出现一个问题,如果在 delete 节点前,就抛异常,代码运行逻辑就出去了,会导致没有释放的情况。好说,我们学过了 RAII,可以选择利用对象的生命周期来实现对资源的控制,于是乎可以在使用时将节点定义成 shared_ptr。
Node 节点定义成 shared_ptr,要完成 Node1->_next = Node2,就同样需要在结构体里把 _next 和 _prev 定义成 shared_ptr。
那么代码就应该写成这样:
struct ListNode{
ttang::shared_ptr _next;
ttang::shared_ptr _prev;
// ...
};
然后这样去使用他
std::shared_ptr<ListNode> n1(new ListNode); // std 里面只能这样显示的调构造
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl; //1
cout << n2.use_count() << endl; //1
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl; //2
cout << n2.use_count() << endl; //2
向上面这样,shared_ptr 管理的两个节点相互指向,奇怪的事情就出现了:

可以看到,随着程序的结束,两个原本应该随进程周期结束而析构的指针,并没有析构,也就是说出现了未释放、内存泄露的情况。怎么回事呢?

针对上述问题的出现,C++11 提供了一个解决方案:
核心功能:弱连接 ,专门用来解决 shared_ptr 的循环引用问题。
注意有三:
weak_ptr 的智能指针可以指向 shared_pre 的指针指向的资源,而不增加 share_ptr 的引用计数。
于是定义结点的代码应该修改成这样:
struct ListNode
{
std::weak_ptr _next;
std::weak_ptr _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
namespace ttang
{
template<class T>
class weak_ptr // 超简单实现,库里肯定不是这样滴
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
};
}
特殊定制析构方式,不难,看代码吧。
结合 ttang::shared 里的,构造的时候第二个参数传入可调用对象就行。
// 定制删除器 -- 可调用对象
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "void operator()(T* ptr)" << endl;
delete[] ptr;
}
};
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void test_shared_deletor()
{
ttang::shared_ptr<Date> sp0(new Date);
ttang::shared_ptr<Date> spa1(new Date[10], DeleteArray<Date>());
ttang::shared_ptr<Date> spa2(new Date[10], [](Date* ptr) {
cout << "lambda delete[]:" << ptr << endl;
delete[] ptr;
});
ttang::shared_ptr<FILE> spF3(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "lambda fclose:" << ptr << endl;
fclose(ptr);
});
}
🥰如果本文对你有些帮助,欢迎👉 点赞 收藏 关注,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 若有差错恳请留言指正~~