一、为什么需要智能指针?
在我们异常一节就已经讲过,当使用异常的时候,几个函数层层嵌套,其中如果抛异常就可能导致没有释放堆区开辟的空间。这样就很容易导致内存泄漏。关于内存泄漏,我也曾在C 内存管理一文中写过。
为了更好的管理我们申请的空间,C 引入了智能指针。
参考文章:
1.【C 】异常_
2. C 内存管理
二、智能指针
1.RAII
RAII ( Resource Acquisition Is Initialization )是一种 利用对象生命周期来控制程序资源 (如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在 对象析构的时候释放资源 。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做 法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{
cout << "管理空间:" << _ptr << endl;
}
~SmartPtr()
{
if (_ptr)
{
delete _ptr;
}
cout << "释放空间:" << _ptr << endl;
}
private:
T* _ptr;
};
int main()
{
SmartPtr<int> sp1(new int(1));
return 0;
}
我们可以看到,智能指针的引入,极大的便利了我们管理空间。
在封装了几层的函数中抛异常,我们也能够来通过智能指针来管理好空间。
2.智能指针的完善
上述的 SmartPtr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过-> 去访问所指空间中的内容,因此: AutoPtr 模板类中还得需要将 * 、 -> 重载下,才可让其 像指针一样去使用 。
代码语言:javascript复制template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{
cout << "管理空间" << _ptr << endl;
}
~SmartPtr()
{
if (_ptr)
{
delete _ptr;
}
cout << "释放空间:" << _ptr << endl;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
class Date
{
public:
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int(1));
cout << *sp1 << endl;
SmartPtr<int> sp2(new int[10]);
sp2[2] = 10;
cout << sp2[2] << endl;
return 0;
}
通过符号的重载,我们使得智能指针具有了普通指针的功能。
但是我们发现,智能指针没有提供拷贝的功能,那么接下来我们看看库中实现的智能指针是如何做的?
三、标准库中的智能指针
1.std::auto_ptr
参考文献:std::auto_ptr
auto_ptr 是C 库中的第一个智能指针,其在面对拷贝构造的解决办法是:转移所有权(当用当前的智能指针对象拷贝出一个新对象时,当前对象资源的所有权会转移给新对象,然后自身的资源会置空)。这样也随之带来一个问题,新对象产生的同时,旧对象将会导致对象悬空。如果后续还有人使用了旧对象,就会引发问题。
代码语言:javascript复制int main()
{
std::auto_ptr<int> ap1(new int(10));
std::auto_ptr<int> ap2(ap1);
cout << *ap1 << endl;
return 0;
}
库中实现方法模拟:
代码语言:javascript复制template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
2.std::unique_ptr
参考文献: std::unique_ptr
unique_ptr 的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份 UniquePtr 来了解它的原 理 。
代码语言:javascript复制//用delete关键字来不让拷贝构造函数生成
unique_ptr(unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
通过使用delete关键字,将拷贝构造函数删除,使得其无法生成,来实现无法拷贝的操作。
3.shared_ptr
A)shared_ptr中的引用计数
shared_ptr 的原理:是通过 引用计数 的方式来实现多个 shared_ptr 对象之间共享资源。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。 2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。 3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源; 4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
那么代码中该如何实现呢?有以下几种方法:
1.在成员变量中增加了一个整数类型来记录 。因为每个对象都会有一个自己的成员变量,我们修改的时候需要照顾到每一个指向同一块空间的智能指针对象,这样的办法是不可行的。
2.在类中增加了一个静态的整数类型成员变量。这样就变成了整个类共享这一个成员变量,所以这个办法也是不可行的。
3.添加一个int类型的指针,int类型中记录的是指向其的指针的个数。库中采用的也是这种办法。
实现代码:
代码语言:javascript复制template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_count(new int(1))
{}
~shared_ptr()
{
release();
}
void release()
{
if (--(*_count) == 0)
{
delete _ptr;
delete _count;
}
}
//拷贝构造函数
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_count(sp._count)
{
(*_count) ;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//不能给自己赋值
if (_ptr != sp._ptr)
{
//先把原先的资源释放一次
release();
_ptr = sp._ptr;
_count = sp._count;
(*_count) ;
}
return *this;
}
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
T operator[](size_t pos) const
{
return _ptr[pos];
}
int use_count() const
{
return *_count;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _count;
};
我们通过下面函数测试:
代码语言:javascript复制void TestShared_ptr()
{
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(new int(5));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
cout << sp1.get() << endl;
cout << sp2.get() << endl;
sp1 = sp3;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
cout << sp1.get() << endl;
cout << sp2.get() << endl;
}
B)shared_ptr中产生的线程安全问题
我们代码有时候可能是一行,但是转成汇编之后可能有几行,其中 和--操作就是这样。在 操作转成汇编之后,有三行操作,如果在这时候时间片轮转到了时间,将正在运行的线程切出,别的线程也对其中的数据进行操作的时候,就会引发问题了。这是因为这样的操作是非原子性的。
我们用下面的代码来验证:
代码语言:javascript复制void TestShared_ptr2()
{
shared_ptr<int> sp1(new int);
int N = 10000;
thread t1([&]() //lambda表达式
{
for (int i = 0; i < N; i)
{
shared_ptr<int> sp2(sp1);
}
});
thread t2([&]() //lambda表达式
{
for (int i = 0; i < N; i)
{
shared_ptr<int> sp3(sp1);
}
});
t1.join();
t2.join();
cout << sp1.use_count() << endl;
}
出现的答案有很多种,可能是整数、负数,甚至还可能会报错。
为了解决这个问题,我们需要给 操作来加锁,使得该操作具有原子性(通俗理解为:要么不做,要么就做完)。
因为枷锁和解锁的过程是具有原子性的,所以,不需要我们担心锁的安全问题。
和引用计数的实现方法一样,我们加锁的操作也是在成员变量中增加一个锁类型的指针。
代码语言:javascript复制template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_count(new int(1))
,_mtx(new mutex)
{}
~shared_ptr()
{
release();
}
void release()
{
bool flag = false;
_mtx->lock();
if (--(*_count) == 0)
{
delete _ptr;
delete _count;
flag = true;
}
_mtx->unlock();
//如果清空了数据,那么锁也要释放
if (flag == true)
{
delete _mtx;
}
}
//拷贝构造函数
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_count(sp._count)
,_mtx(sp._mtx)
{
_mtx->lock();
(*_count) ;
_mtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//不能给自己赋值
if (_ptr != sp._ptr)
{
//先把原先的资源释放一次
release();
_ptr = sp._ptr;
_count = sp._count;
_mtx->lock();
(*_count) ;
_mtx->unlock();
}
return *this;
}
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
T operator[](size_t pos) const
{
return _ptr[pos];
}
int use_count() const
{
return *_count;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _count;
mutex* _mtx;
};
值得一提的是,虽然引用计数我们在类内加锁了,但是如果在线程中对智能指针中的资源 的时候,还是不安全的。
代码语言:javascript复制void TestShared_ptr2()
{
shared_ptr<int> sp1(new int);
int N = 10000;
thread t1([&]() //lambda表达式
{
for (int i = 0; i < N; i)
{
(*sp1);
}
});
thread t2([&]() //lambda表达式
{
for (int i = 0; i < N; i)
{
(*sp1);
}
});
t1.join();
t2.join();
cout << *sp1 << endl;
}
库中提供的shared_ptr也是有这个问题的。所以在我们对智能指针中的资源操作的时候,我们也需要手动加锁。
C)shared_ptr中的循环引用问题
虽然shared_ptr相较于以往的智能指针,表现的十分好,但是仍旧是有缺陷的。
我们看下面的问题:
代码语言:javascript复制struct ListNode
{
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
};
void TestShared_ptr3()
{
shared_ptr<ListNode> sp1(new ListNode);
shared_ptr<ListNode> sp2(new ListNode);
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
当我们存储的是双向链表的节点并且剩下两个节点的时候,这时候sp1中存储的链表节点n1的_prev指向sp2中,n2的_next指向的是n1节点。当代码结束后,调用析构函数,此时的_count == 3,调用析构--count,此时仍旧不等于0,而如果要满足0释放空间,则需要:
1.sp1需要释放,则需要n2先释放,n2中的指针清除之后,_count == 0,才能释放。
2.sp2需要释放,需要n1先释放,n1中的指针清除之后,_count == 0,才能完成释放。
所以,我们会发现,这就造成死循环了。就像两个小孩打架抓着对方的头发,A对B说你先放手我就放,B也对A说你先放手我就放。
为了解决这个问题,C 中引入了weak_ptr。
4.weak_ptr
weak_ptr是为了解决shared_ptr而专门设计出的一款智能指针,解决办法也很简单,那就是不设计引用计数,自然也就不会有因为 count != 0 而无法释放空间的问题了。
由于库中的weak_ptr考虑的非常全面,我们这里只对解决shared_ptr的缺陷作模拟。
代码如下:
代码语言:javascript复制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*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
private:
T* _ptr;
};
struct ListNode
{
qingshan::weak_ptr<ListNode> _prev;
qingshan::weak_ptr<ListNode> _next;
};
void TestShared_ptr3()
{
qingshan::shared_ptr<ListNode> sp1(new ListNode);
qingshan::shared_ptr<ListNode> sp2(new ListNode);
sp1->_next = sp2;
sp1->_prev = sp2;
sp2->_prev = sp1;
sp2->_next = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}
最后我们也是看到,节点成功释放了。
5.定制删除器
我们在析构函数中只用了delete来释放申请的空间,那么如果我们使用new[ ] 来申请的空间,那么同样的我们也需要用 delete[ ] 来释放空间。
为此,我们就需要定制删除器。定制删除器本质上是一个仿函数。与我们在哈希一文中提到的hashfunc一样。
我们还需要再shared_ptr类中增加一个成员变量 _del 来实现释放空间。
代码如下:
代码语言:javascript复制template<class T, class D = default_delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _count(new int(1))
, _mtx(new mutex)
{}
~shared_ptr()
{
release();
cout << "~shared_ptr" << endl;
}
void release()
{
bool flag = false;
_mtx->lock();
if (--(*_count) == 0)
{
//delete _ptr;
_del(_ptr);
delete _count;
flag = true;
}
_mtx->unlock();
//如果清空了数据,那么锁也要释放
if (flag == true)
{
delete _mtx;
}
}
//拷贝构造函数
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
, _mtx(sp._mtx)
{
_mtx->lock();
(*_count) ;
_mtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//不能给自己赋值
if (_ptr != sp._ptr)
{
//先把原先的资源释放一次
release();
_ptr = sp._ptr;
_count = sp._count;
_mtx->lock();
(*_count) ;
_mtx->unlock();
}
return *this;
}
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
T operator[](size_t pos) const
{
return _ptr[pos];
}
int use_count() const
{
return *_count;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _count;
mutex* _mtx;
D _del;
};
void TestShared_ptr4()
{
qingshan::shared_ptr<int> sp1(new int[10]);
qingshan::shared_ptr<int, DeleteArray<int>> sp2(new int[10]);
qingshan::shared_ptr<FILE, Fclose> sp3(fopen("test.txt", "w"));
}
定制删除器功能十分强大,我们甚至还可以用其来关闭文件。但是我们这里实现的只能在模版中提供类型来定制删除器。
库中的提供的shared_ptr是能够通过给构造函数传参来定制删除器的,所以还能使用包装器和lambda表达式。