智能指针详解
文章目录
一、智能指针背后的设计思想
我们先来看一个简单的例子:
void remodel(std::string & str)
{
std::string * ps = new std::string(str);
...
if (weird_thing())
throw exception();
str = *ps;
delete ps;
return;
}
当出现异常时(weird_thing()返回true),delete将不被执行,因此将导致内存泄露。
如何避免这种问题?有人会说,这还不简单,直接在throw exception();之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,看是否有这种潜在的内存泄露问题,那就是一场灾难!
这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将自动从栈内存中删除—因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。
我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果它指向的是对象,则可以在对象过期时,让它的析构函数删除指向的内存。
这正是智能指针背后的设计思想。我简单的总结下就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
二、普通指针存在的问题
我们来看看普通指针的悬垂指针问题。当有多个指针指向同一个基础对象时,如果某个指针delete了该基础对象,对这个指针来说它是明确了它所指的对象被释放掉了,所以它不会再对所指对象进行操作,但是对于剩下的其他指针来说呢?它们还傻傻地指向已经被删除的基础对象并随时准备对它进行操作。于是悬垂指针就形成了,程序崩溃也“指日可待”。我们通过代码+图来来探求悬垂指针的解决方法。
int * ptr1 = new int (1);
int * ptr2 = ptr1;
int * ptr3 = prt2;
cout << *ptr1 << endl;
cout << *ptr2 << endl;
cout << *ptr3 << endl;
delete ptr1;
cout << *ptr2 << endl;
从图可以看出,错误的产生来自于ptr1的”无知“:它并不知道还有其他指针共享着它指向的对象。如果有个办法让ptr1知道,除了它自己外还有两个指针指向基础对象,而它不应该删除基础对象,那么悬垂指针的问题就得以解决了。如下图:
三、shared_ptr类
1、make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
shared_ptr<int> p = make_shared<int>(42);//效率比下面的高
shared_ptr<int> p2(new int(42));// 不推荐,为了避免智能指针与普通指针的混用,所以最后使用make_shared,这样在内存分配之后立刻与智能指针绑定到一起.
2、shared_ptr的拷贝和赋值
auto p = make_shared<int>(42);
auto q(p);
auto r = p; //p递增,r递减。
每个shared_ptr都有一个关联的计数器,通常称为引用计数。
无论何时我们拷贝一个shared_ptr,计数器都会递增;当我们给shared_ptr赋予一个新值或者shared_ptr被销毁时,引用计数会递减。
一旦引用计数变为0,shared_ptr就会自动释放自己所管理的对象。
3、shared_ptr自动销毁所管理的对象
shared_ptr的析构函数会递减它所指向的对象的引用计数,当引用计数变为0时,shared_ptr就会通过析构函数自动释放自己所管理的对象。当动态对象不再使用时,shared_ptr会自动释放对象,这一特性使得动态内存的使用变得非常容易(尽量使用智能指针管理动态内存)。如果将shared_ptr放于容器中,而后不再需要全部元素,而只是使用其中一部分,要记得用erase删除不再需要的那些元素。
在多线程程序中,一个对象如果被多个线程访问,一般使用shared_ptr,通过引用计数来保证对象不被错误的释放导致其他线程访问出现问题。
4、使用动态内存的原因:
-
程序不知道自己需要多少对象。
-
程序不知道所需对象的准确类型。
-
允许多个对象共享相同的状态。
5、使用shared_ptr的一个例子:
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class StrBlob
{
public:
typedef vector<string>::size_type st;
StrBlob():data(make_shared<vector<string>>()){}
StrBlob(initializer_list<string> il):data(make_shared<vector<string>>()){}
inline st size() const { return data->size(); }
inline bool empty() const { return data->empty(); }
inline void push_back(const string &t) { data->push_back(t); }
void pop_back();
const string & front();
const string & back();
private:
shared_ptr<vector<string>> data;
void check(st i,const string &msg) const;
};
void StrBlob::check(st i,const string &msg) const
{
if(i >= data->size())
throw out_of_range(msg);
}
const string & StrBlob::front()
{
StrBlob::check(0,"front on empty StrBlob");
return data->front();
}
const string & StrBlob::back()
{
StrBlob::check(0,"back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
StrBlob::check(0,"pop_back on empty StrBlob");
return data->pop_back();
}
int main(int argc, char const *argv[])
{
StrBlob b1;
StrBlob b2 = {"a","an","the"};
b1 = b2;
b2.push_back("about");
cout << b1.size() << endl;
cout << b2.size() << endl;
return 0;
return 0;
}
四、shared_ptr的实现和循环引用问题
template<typename T>
class SmartPtr {
private:
T *_ptr; // 指向对应的对象
int *_cnt; // 计数
public:
SmartPtr(T *ptr) : _ptr(ptr), _cnt(new int(1)) {}
// 拷贝
SmartPtr(const SmartPtr &p) : _ptr(p._ptr), _cnt(p._cnt) { (*_cnt)++; }
// 赋值
SmartPtr & operator=(const SmartPtr &p) {
(*(p._cnt))++; // 给右侧的对象的计数++
(*_cnt)--; // 给左侧的对象的计数--
if (*_cnt == 0) {
delete _ptr;
delete _cnt;
}
_ptr = p._ptr;
_cnt = p._cnt;
return *this;
}
~SmartPtr() {
(*_cnt)--;
if (*_cnt == 0) {
delete _cnt;
delete _ptr;
}
}
};
但是这里还有一个严重的问题,就是关于循环引用(会引起内存泄漏) 的问题。对于什么是循环引用?我们用下面这个测试用例来解释:
class B;
class A
{
public:
shared_ptr<B> m_b;
};
class B
{
public:
shared_ptr<A> m_a;
};
void fun()
{
shared_ptr<A> pa(new A); // new出来的A的引用计数此时为1
shared_ptr<B> pb(new B); // new出来的B的引用计数此时为1
pa->m_b = b; // B的引用计数增加为2
pb->m_a = a; // A的引用计数增加为2
}
int main()
{
fun();
return 0;
}
分析class A对象的引用情况,该对象被main函数中的pa和class B对象中的ptr管理,因此pa引用计数是2,class B对象同理。
在这种情况下,在fun函数结束的时候,pa和pb的析构函数被调用,但是class A对象和class B对象仍然被一个智能指针管理,pa和pb引用计数变成1,于是这两个对象的内存无法被释放,造成内存泄漏,如下图所示:
因此,在这里标准库就引用了weak_ptr,将类里面的shared_ptr换成weak_ptr即可,由于weak_ptr并不会增加引用计数use的值,所以这里就能够打破shared_ptr所造成的循环引用问题。但是这里要注意一点,就是weak_ptr并不能单独用来管理空间。
五、weak_ptr类
1、weak_ptr详解
由于在shared_ptr(强引用:每创建一个变量引用该对象时,该对象的计数就增加1)的析构函数中,只有当use=1,进行减减之后为0,才会释放_ptr所指向的空间,所以在这里a和b都不会被释放,因此也不会调用析构函数,所以这里就出现了内存泄漏。
由于在shared_ptr单独使用的时候会出现循环引用的问题,造成内存泄漏,所以标准库又从boost库当中引入了weak_ptr(弱引用:不更改引用计数,类似普通指针)。对上面的测试用例进行修改:
class B;
class A
{
public:
weak_ptr<B> m_b;
};
class B
{
public:
weak_ptr<A> m_a;
};
void fun()
{
shared_ptr<A> pa(new A); // new出来的A的引用计数此时为1
shared_ptr<B> pb(new B); // new出来的B的引用计数此时为1
pa->m_b = b; // B的引用计数增加为2
pb->m_a = a; // A的引用计数增加为2
}
int main()
{
fun();
return 0;
}
解决方法很简单,把class A或者class B中的shared_ptr改成weak_ptr即可,由于weak_ptr不会增加shared_ptr的引用计数,所以pa和pb中有一个的引用计数为1,在pa和pb析构时,会正确地释放掉内存。
weak_ptr是一种不控制所指向对象生存周期的智能指针,他指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr,不会改变shared_ptr的引用计数。
auto p = make_shared<int>(10); // 使用shared_ptr来初始化。
weak_ptr<int> wp(p);
if(shared_ptr<int> np = wp.lock()) // 访问对象必须调用lock()
{
//使用np访问共享对象
}
2、weak_ptr的实现
weak_ptr的作为弱引用指针,其实现依赖于counter的计数器类和share_ptr的赋值,构造。
class Counter
{
public:
Counter():s(0),w(0){};
int s;
int w;
};
s是share_ptr的引用计数,w是weak_ptr的引用计数,当w为0时,删除Counter对象。当weak_ptr是由share_ptr构造或者赋值时,不会增加share_ptr的引用计数,只会增加自身的引用计数。
template<class T>
class WeakPtr
{
public:
WeakPtr()
{
_ptr=0;
cnt=0;
}
WeakPtr(SharePtr<T>& s): _ptr(s._ptr), cnt(s.cnt)
{
cout<<"w con s"<<endl;
cnt->w++;
}
WeakPtr(WeakPtr<T>& w): _ptr(w._ptr), cnt(w.cnt)
{
cnt->w++;
}
~WeakPtr()
{
release();
}
WeakPtr<T>& operator =(WeakPtr<T> & w)
{
if(this != &w)
{
release();
cnt=w.cnt;
cnt->w++;
_ptr=w._ptr;
}
return *this;
}
WeakPtr<T>& operator =(SharePtr<T> & s)
{
cout<<"w = s"<<endl;
release();
cnt=s.cnt;
cnt->w++;
_ptr=s._ptr;
return *this;
}
SharePtr<T> lock()
{
return SharePtr<T>(*this);
}
bool expired()
{
if(cnt)
{
if(cnt->s >0)
{
cout<<"empty "<<cnt->s<<endl;
return false;
}
}
return true;
}
friend class SharePtr<T>;//方便weak_ptr与share_ptr设置引用计数和赋值。
private:
void release()
{
if(cnt)
{
cnt->w--;
cout<<"weakptr release"<<cnt->w<<endl;
if(cnt->w <1&& cnt->s <1)
{
//delete cnt;
cnt=NULL;
}
}
}
T* _ptr;
Counter* cnt;
};
六、unique_ptr类
unique_ptr 独占智能指针,某个时刻只能有一个unique_ptr 指向一个给定对象。当unique_ptr 被销毁时,它所指向的对象也被销毁。unique_ptr 不支持拷贝赋值等操作,除非这个unique_ptr将要被销毁,这种情况,编译器执行一种特殊的"拷贝"
unique_ptr<int> p1(new int(42));//必须直接初始化。
unique_ptr<int> p2(p1);//error
unique_ptr<int> p3 = p1;/error
unique_ptr<int> clone(int p)
{
unique_ptr<int> ret(new int(p));
return ret; //ok
}
虽然不能拷贝或者赋值unique_ptr,但是通过调用release或者reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr。调用release会切断unique_ptr和它原来管理对象间的联系。release返回的指针通常用来初始化另一个智能指针或者给另一个智能指针赋值。
#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> clone(int p);
int main(int argc, char const *argv[])
{
unique_ptr<double> p;//
unique_ptr<string> p1(new string("ABC"));//使用new返回的指针初始化。
cout << *p1 << endl;
unique_ptr<string> p2(p1.release());//放弃对p1的控制权,返回指针并置空,然后初始化另一个指针。
cout << *p2 << endl;
unique_ptr<string> p3(new string("abc"));
p2.reset(p3.release());//释放p2的对象,并将p3的所有权转移给p2。
cout << *p2 << endl;
cout << *clone(10) << endl;
return 0;
}
unique_ptr<int> clone(int p)
{
return unique_ptr<int>(new int(p));//unique_ptr不能拷贝或者赋值,但是可以返回一个unique_ptr。
}
七、unique_ptr类为何优于auto_ptr类
可能大家认为前面的例子已经说明了unique_ptr为何优于auto_ptr,也就是安全问题,下面再叙述的清晰一点。请看下面的语句:
auto_ptr<string> p1(new string ("auto") ; // #1
auto_ptr<string> p2; // #2
p2 = p1; // #3
在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象;但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。下面来看使用unique_ptr的情况:
unique_ptr<string> p3 (new string ("auto"); // #4
unique_ptr<string> p4; // #5
p4 = p3; // #6
编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。但unique_ptr还有更聪明的地方。有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:
unique_ptr<string> demo(const char * s)
{
unique_ptr<string> temp (new string (s));
return temp;
}
unique_ptr<string> ps;
ps = demo('Uniquely special");
demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值,这正是unique_ptr更聪明的地方。
总之,当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
八、使用智能指针需要注意的问题:
九、如何选择智能指针?
在掌握了这几种智能指针后,应使用哪种智能指针呢?
-
如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;两个对象包含都指向第三个对象的指针;STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
-
如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。