目录
一、为什么需要智能指针?
二、智能指针
1.RAII
2.智能指针的完善
三、标准库中的智能指针
1.std::auto_ptr
2.std::unique_ptr
4.weak_ptr
5.定制删除器
在我们异常一节就已经讲过,当使用异常的时候,几个函数层层嵌套,其中如果抛异常就可能导致没有释放堆区开辟的空间。这样就很容易导致内存泄漏。关于内存泄漏,我也曾在C++内存管理一文中写过。
为了更好的管理我们申请的空间,C++引入了智能指针。
参考文章:
1.【C++】异常_
2. C++内存管理
template
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 sp1(new int(1));
return 0;
}
我们可以看到,智能指针的引入,极大的便利了我们管理空间。
在封装了几层的函数中抛异常,我们也能够来通过智能指针来管理好空间。
template
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 sp1(new int(1));
cout << *sp1 << endl;
SmartPtr sp2(new int[10]);
sp2[2] = 10;
cout << sp2[2] << endl;
return 0;
}
通过符号的重载,我们使得智能指针具有了普通指针的功能。
但是我们发现,智能指针没有提供拷贝的功能,那么接下来我们看看库中实现的智能指针是如何做的?
参考文献:std::auto_ptr
auto_ptr 是C++库中的第一个智能指针,其在面对拷贝构造的解决办法是:转移所有权(当用当前的智能指针对象拷贝出一个新对象时,当前对象资源的所有权会转移给新对象,然后自身的资源会置空)。这样也随之带来一个问题,新对象产生的同时,旧对象将会导致对象悬空。如果后续还有人使用了旧对象,就会引发问题。
int main()
{
std::auto_ptr ap1(new int(10));
std::auto_ptr ap2(ap1);
cout << *ap1 << endl;
return 0;
}
库中实现方法模拟:
template
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr& operator=(auto_ptr& 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;
};
//用delete关键字来不让拷贝构造函数生成
unique_ptr(unique_ptr& sp) = delete;
unique_ptr& operator=(unique_ptr& ap) = delete;
通过使用delete关键字,将拷贝构造函数删除,使得其无法生成,来实现无法拷贝的操作。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
那么代码中该如何实现呢?有以下几种方法:
1.在成员变量中增加了一个整数类型来记录 。因为每个对象都会有一个自己的成员变量,我们修改的时候需要照顾到每一个指向同一块空间的智能指针对象,这样的办法是不可行的。
2.在类中增加了一个静态的整数类型成员变量。这样就变成了整个类共享这一个成员变量,所以这个办法也是不可行的。
3.添加一个int类型的指针,int类型中记录的是指向其的指针的个数。库中采用的也是这种办法。
实现代码:
template
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& sp)
:_ptr(sp._ptr)
,_count(sp._count)
{
(*_count)++;
}
shared_ptr& operator=(const shared_ptr& 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;
};
我们通过下面函数测试:
void TestShared_ptr()
{
shared_ptr sp1(new int(10));
shared_ptr sp2(sp1);
shared_ptr 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;
}
我们代码有时候可能是一行,但是转成汇编之后可能有几行,其中++和--操作就是这样。在++操作转成汇编之后,有三行操作,如果在这时候时间片轮转到了时间,将正在运行的线程切出,别的线程也对其中的数据进行操作的时候,就会引发问题了。这是因为这样的操作是非原子性的。
我们用下面的代码来验证:
void TestShared_ptr2()
{
shared_ptr sp1(new int);
int N = 10000;
thread t1([&]() //lambda表达式
{
for (int i = 0; i < N; ++i)
{
shared_ptr sp2(sp1);
}
});
thread t2([&]() //lambda表达式
{
for (int i = 0; i < N; ++i)
{
shared_ptr sp3(sp1);
}
});
t1.join();
t2.join();
cout << sp1.use_count() << endl;
}
出现的答案有很多种,可能是整数、负数,甚至还可能会报错。
为了解决这个问题,我们需要给++操作来加锁,使得该操作具有原子性(通俗理解为:要么不做,要么就做完)。
因为枷锁和解锁的过程是具有原子性的,所以,不需要我们担心锁的安全问题。
和引用计数的实现方法一样,我们加锁的操作也是在成员变量中增加一个锁类型的指针。
template
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& sp)
:_ptr(sp._ptr)
,_count(sp._count)
,_mtx(sp._mtx)
{
_mtx->lock();
(*_count)++;
_mtx->unlock();
}
shared_ptr& operator=(const shared_ptr& 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;
};
值得一提的是,虽然引用计数我们在类内加锁了,但是如果在线程中对智能指针中的资源++的时候,还是不安全的。
void TestShared_ptr2()
{
shared_ptr 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也是有这个问题的。所以在我们对智能指针中的资源操作的时候,我们也需要手动加锁。
虽然shared_ptr相较于以往的智能指针,表现的十分好,但是仍旧是有缺陷的。
我们看下面的问题:
struct ListNode
{
shared_ptr _prev;
shared_ptr _next;
};
void TestShared_ptr3()
{
shared_ptr sp1(new ListNode);
shared_ptr 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。
weak_ptr是为了解决shared_ptr而专门设计出的一款智能指针,解决办法也很简单,那就是不设计引用计数,自然也就不会有因为 count != 0 而无法释放空间的问题了。
由于库中的weak_ptr考虑的非常全面,我们这里只对解决shared_ptr的缺陷作模拟。
代码如下:
template
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr& sp)
:_ptr(sp.get())
{}
weak_ptr& operator=(const shared_ptr& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
private:
T* _ptr;
};
struct ListNode
{
qingshan::weak_ptr _prev;
qingshan::weak_ptr _next;
};
void TestShared_ptr3()
{
qingshan::shared_ptr sp1(new ListNode);
qingshan::shared_ptr 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;
}
最后我们也是看到,节点成功释放了。
我们在析构函数中只用了delete来释放申请的空间,那么如果我们使用new[ ] 来申请的空间,那么同样的我们也需要用 delete[ ] 来释放空间。
为此,我们就需要定制删除器。定制删除器本质上是一个仿函数。与我们在哈希一文中提到的hashfunc一样。
我们还需要再shared_ptr类中增加一个成员变量 _del 来实现释放空间。
代码如下:
template>
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& sp)
:_ptr(sp._ptr)
, _count(sp._count)
, _mtx(sp._mtx)
{
_mtx->lock();
(*_count)++;
_mtx->unlock();
}
shared_ptr& operator=(const shared_ptr& 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 sp1(new int[10]);
qingshan::shared_ptr> sp2(new int[10]);
qingshan::shared_ptr sp3(fopen("test.txt", "w"));
}
定制删除器功能十分强大,我们甚至还可以用其来关闭文件。但是我们这里实现的只能在模版中提供类型来定制删除器。
库中的提供的shared_ptr是能够通过给构造函数传参来定制删除器的,所以还能使用包装器和lambda表达式。