顾得泉:个人主页
个人专栏:《Linux操作系统》 《C++从入门到精通》 《LeedCode刷题》
键盘敲烂,年薪百万!
一、为什么需要智能指针?
下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析MergeSort函数中的问题。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
问题分析:上面的问题分析出来我们发现有什么问题?
程序明面上看起来没什么问题,可以正常运行,但是p1和p2没有释放,会出现内存泄漏,因为抛异常了会执行流调转。
这时候就需要我们引入智能指针的概念了:
智能指针是C++中的一种特殊指针,它能够自动管理动态分配的内存,避免内存泄漏和悬空指针的问题。智能指针通过在对象生命周期结束时自动释放内存,减少了手动释放内存的工作量和出错的可能性。
二、智能指针的原理
1.RAII
RAII是Resource Acquisition Is Initialization的缩写,意为资源获取即初始化。它是一种C++编程技术,用于管理资源的生命周期。RAII的核心思想是,通过在对象的构造函数中获取资源,并在析构函数中释放资源,来确保资源的正确管理。
具体来说,RAII的使用方法是通过在对象的构造函数中获取资源,例如打开文件、分配内存等,然后在析构函数中释放资源,例如关闭文件、释放内存等。这样,在对象的生命周期结束时,资源会自动被释放,无需手动管理。
这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命期内始终保持有效。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
2.智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
总结一下智能指针的原理:
1.具有RAll特性
2.重载operator*和operator->,具有和指针一样的行为。
三、智能指针的分类
1.std::auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。
下面演示的auto_ptr的使用及问题。其原理是:管理权转移。
namespace GoodQ
{
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;
};
}
int main()
{
std::auto_ptr<int> sp1(new int);
std::auto_ptr<int> sp2(sp1); // 管理权转移
// sp1悬空
*sp2 = 10;
cout << *sp2 << endl;
cout << *sp1 << endl;
return 0;
}
结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
2.std::unique_ptr
C++11中开始提供更靠谱的unique_ptr.其原理是:防止拷贝。
namespace GoodQ
{
template<class T>
class unique_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;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}
int main()
{
std::unique_ptr<int> sp1(new int);
return 0;
}
3.std::shard_ptr
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
1.shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2.在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减1。
3.如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4.如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
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)
{}
void release()
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
}
}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_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;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
std::shared_ptr的循环引用问题
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
循环引用分析:
1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动
delete。
2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
4. 也就是说_next析构了,node2就释放了。
5. 也就是说_prev析构了,node1就释放了。
6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
如果不是new出来的对象如何通过智能指针管理呢?
其实shared_ptr设计了一个删除器来解决这个问题,代码同上.
四、扩展阅读
1.C++ 98 中产生了第一个智能指针auto_ptr.
2.C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
3.C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
4.C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
结语:关于本次C++11中智能指针的分享到这里就结束了,希望本篇文章的分享会对大家的学习带来些许帮助,如果大家有什么问题,欢迎大家在评论区留言~~~