前言
由于C++引入的抛异常会影响程序执行流,导致new申请的资源因抛异常而无法释放,造成内存泄漏问题,Java采用垃圾回收机制解决此问题,而C++引入智能指针,将指针封装为一个类,利用类在生命周期结束后会自动调用析构函数的特性,通过析构函数释放申请的资源。
auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针,auto_ptr文档。
以下代码演示了auto_ptr的使用及问题。
struct Date
{
friend std::ostream& operator<<(std::ostream& out, Date ptr);
int _year = 1970;
};
std::ostream& operator<<(std::ostream& out, Date ptr)
{
out << ptr._year;
return out;
}
int main()
{
//test auto_ptr
std::auto_ptr<int> ptr1(new int(10));
std::auto_ptr<Date> ptr2(new Date());
cout << *ptr1 << endl;
cout << *ptr2 << endl;
cout << ptr2->_year << endl;
return 0;
}
如果对auto_ptr类型的指针进行一次拷贝呢?
struct Date
{
friend std::ostream& operator<<(std::ostream& out, Date ptr);
int _year = 1970;
};
std::ostream& operator<<(std::ostream& out, Date ptr)
{
out << ptr._year;
return out;
}
int main()
{
//test auto_ptr
std::auto_ptr<int> ptr1(new int(10));
std::auto_ptr<int> ptrCopy(ptr1);
std::auto_ptr<Date> ptr2(new Date());
cout << *ptr1 << endl;
cout << *ptrCopy << endl;
cout << *ptr2 << endl;
cout << ptr2->_year << endl;
return 0;
}
通过监视窗口可以看到,在将ptr1拷贝给ptrCopy后,ptr1的值为empty。
实际上,auto_ptr的实现原理类似管理权转移的思想,在拷贝过后,会使被拷贝指针为空,C++标准委员会建议不要使用auto_ptr,下面简化模拟实现了一份auto_ptr来了解它的原理。
namespace zyh
{
template<class T>
class auto_ptr
{
public:
auto_ptr()
:_ptr(nullptr)
{}
auto_ptr(T* const ptr)
:_ptr(ptr)
{}
// 管理权转移
auto_ptr(auto_ptr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
std::cout << "delete:" << _ptr << std::endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr = nullptr;
};
}
// 测试代码
// test auto_ptr
zyh::auto_ptr<int> ptr1(new int(10));
//zyh::auto_ptr<int> ptrCopy(ptr1);
zyh::auto_ptr<Date> ptr2(new Date());
cout << *ptr1 << endl;
//cout << *ptrCopy << endl;
cout << *ptr2 << endl;
cout << ptr2->_year << endl;
可以看到,模拟实现的auto_ptr在将ptr1拷贝给ptrCopy后,ptr1为空。
unique_ptr
C++11提供更靠谱的unique_ptr,unique_ptr文档。
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份unique_ptr来了解它的原
理。
namespace zyh
{
template<class T>
class unique_ptr
{
public:
unique_ptr()
{}
unique_ptr(T* const ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
std::cout << "delete:" << _ptr << std::endl;
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 = nullptr;
};
}
// 测试代码
// test unique_ptr
zyh::unique_ptr<int> ptr1(new int(10));
// 禁止拷贝行为
zyh::unique_ptr<int> ptr2(ptr1);
zyh::unique_ptr<int> ptr2(new int(20));
zyh::unique_ptr<Date> ptr3(new Date());
cout << *ptr3 << endl;
cout << ptr3->_year << endl;
使用unique_ptr无法进行拷贝构造。
shared_ptr
C++11提供更靠谱的并且支持拷贝的shared_ptr,shared_ptr文档。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:
办公室的同事在下班之前都会通知剩下的人,让最后走的人记得把门锁下。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享;
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减1;
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成了野指针。
下面简化模拟实现了一份shared_ptr来了解它的原理。
namespace zyh
{
template<class T>
class shared_ptr
{
public:
shared_ptr()
{}
shared_ptr(T* const ptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
void Release()
{
if (_ptr != nullptr && --(*_pcount) == 0)
{
std::cout << "delete:" << _ptr << std::endl;
delete _ptr;
delete _pcount;
}
}
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& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr = nullptr;
int* _pcount = nullptr;
};
}
// 测试代码
// test shared_ptr
zyh::shared_ptr<int> ptr1(new int(10));
zyh::shared_ptr<int> ptr2(new int(20));
zyh::shared_ptr<int> ptr3 = ptr1;
ptr2 = ptr3;
zyh::shared_ptr<int> ptr4(new int(40));
ptr1 = ptr4;
cout << "ptr1: " << *ptr1 << endl
<< "ptr2: " << *ptr2 << endl
<< "ptr3: " << *ptr3 << endl
<< "ptr4: " << *ptr4 << endl;
定值删除器
如果想要用智能指针管理不是用new产生的对象呢?其实shared_ptr设计了一个删除器来解决这个问题,通过传入仿函数或Lambda表达式提供的方法来释放资源。
在我们模拟实现的shared_ptr里增加定值删除器。
namespace zyh
{
template<class T>
class shared_ptr
{
public:
shared_ptr()
{}
shared_ptr(T* const ptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T* const ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
void Release()
{
if (_ptr != nullptr && --(*_pcount) == 0)
{
//std::cout << "delete:" << _ptr << std::endl;
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
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& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr = nullptr;
int* _pcount = nullptr;
std::function<void(T*)> _del = [](T* ptr) {
std::cout << "delete:" << ptr << std::endl;
delete ptr;
};
};
}
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
std::cout << "delete[]:" << ptr << std::endl;
delete[] ptr;
}
};
int main()
{
// 测试代码
// 定值删除器
zyh::shared_ptr<ListNode> ptr1(new ListNode[10], DeleteArray<ListNode>());
zyh::shared_ptr<ListNode> ptr2(new ListNode[10], [](ListNode* ptr) {
std::cout << "delete[]:" << ptr << std::endl;
delete[] ptr; });
zyh::shared_ptr<FILE> ptr3(fopen("./Test.cpp", "r"), [](FILE* fp) {
std::cout << "fclose: " << fp << std::endl;
fclose(fp); });
return 0;
}
循环引用
如果用shared_ptr管理一个双向链表呢?
struct ListNode
{
ListNode(int value = 0)
:_value(value)
{}
int _value;
zyh::shared_ptr<ListNode> _next;
zyh::shared_ptr<ListNode> _prev;
};
int main()
{
// 测试代码
// shared_ptr的循环引用问题
zyh::shared_ptr<ListNode> ptr1(new ListNode(10));
zyh::shared_ptr<ListNode> ptr2(new ListNode(20));
ptr1->_next = ptr2;
ptr2->_prev = ptr1;
cout << ptr1.use_count() << " " << ptr2.use_count() << endl;
return 0;
}
我们模拟实现的shared_ptr的析构函数是会打印delete的信息的,而实际上没打印,说明根本没有释放申请的资源,产生了内存泄漏。
为什么会产生这种现象?
观察上图
- 要释放ptr1对象的资源,得先delete ptr2._prev;
- 要delete ptr2._prev,得先释放ptr2对象;
- 要释放ptr2对象的资源,得先delete ptr1._next;
- 要delete ptr1._next,得先释放ptr1对象的资源;
- 要释放ptr1对象的资源,得先delete ptr2._prev。
于是就构成了shared_ptr的循环引用。
这里采用zyh::shared_ptr是为了方便观察,换成std::shared_ptr也会造成循环引用。
C++11采用weak_ptr解决这种问题。
weak_ptr
weak_ptr文档,weak_ptr的原理是让weak_ptr类型的指针指向一个对象时,引用计数不会加1。
下面简化模拟实现了一份shared_ptr来了解它的原理。
namespace zyh
{
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;
};
}
struct ListNode
{
ListNode(int value = 0)
:_value(value)
{}
int _value;
//zyh::shared_ptr<ListNode> _next;
//zyh::shared_ptr<ListNode> _prev;
zyh::weak_ptr<ListNode> _next;
zyh::weak_ptr<ListNode> _prev;
};
int main()
{
// 测试代码
// shared_ptr的循环引用问题
zyh::shared_ptr<ListNode> ptr1(new ListNode(10));
zyh::shared_ptr<ListNode> ptr2(new ListNode(20));
cout << ptr1.use_count() << " " << ptr2.use_count() << endl;
ptr1->_next = ptr2;
ptr2->_prev = ptr1;
cout << ptr1.use_count() << " " << ptr2.use_count() << endl;
return 0;
}
可以看到,在使用weak_ptr后,ptr1和ptr2指向的资源可以正常释放了。
总结
- 任何时候都不要使用auto_ptr;
- 不需要拷贝指针的情况,可以使用unique_ptr;
- 使用shared_ptr时,如果出现了循环引用,将某些指针设置为weak_ptr,通常是shared_ptr指向的对象内部的指针。