C++ 智能指针
- 为什么需要智能指针?
- auto_ptr
- unique_ptr
- shared_ptr
- weak_ptr
- 智能指针的核心实现
- unique_ptr的简单实现
- Counter的简单实现
- share_ptr的简单实现
- weak_ptr简单实现
- shared_ptr的线程安全性
- 多线程无保护读写 shared_ptr 可能出现的问题
- make_shared()
- share_ptr/unique_ptr自定义删除器
为什么需要智能指针?
如果指针忘记调用delete,将会造成内存泄漏。当超出了智能指针类的作用域时,智能指针类会自动调用析构函数,析构函数会自动释放资源。
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
C++STL共提供了四个智能指针: auto_ptr, unique_ptr,shared_ptr, weak_ptr 其中C++11只支持后三个,C++98支持所有四个。
auto_ptr
auto_ptr采用独占式拥有模式,下面有一个例子:
auto_ptr<string> p1 (new string ("I reigned lonely as a cloud."));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.
当程序运行时访问p1将会报错,因为所有权从p1转让给了p2,此时p1不再拥有该字符串对象从而变成空指针。
故auto_ptr的缺点是:存在潜在的内存崩溃问题!
unique_ptr
unique_ptr用于替换auto_ptr,实现了独占式拥有概念,保证同一时间内只有一个智能指针可以指向该对象。为此,unique_ptr的拷贝构造和拷贝赋值均被声明为delete。因此无法实施拷贝和赋值操作,但可以移动构造和移动赋值。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用,还是上面那个例子:
unique_ptr<string> p3 (new string ("auto")); //#4
unique_ptr<string> p4; //#5
p4 = p3;//此时会报错!!
编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。尝试复制p3时会编译期出错,而auto_ptr能通过编译期从而在运行期埋下出错的隐患。因此,unique_ptr比auto_ptr更安全。
另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 不允许
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 允许
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
如果确实想执行类似与#1的操作,C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
shared_ptr
shared_ptr实现共享式拥有概念。通过引用计数,多个智能指针可以指向相同对象,该对象和其相关资源会在最后一个引用被销毁时候释放。
它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。
当复制一个shared_ptr,引用计数会+1。当我们调用release()或者当一个shared_ptr离开作用域时,计数减1(普通的指针如果没有delete操作,离开作用域时并不会被释放,只有在进程结束后才会被释放)。当计数等于0时,则delete内存。
成员函数:
- use_count :返回引用计数的个数
- unique :返回是否是独占所有权( use_count 为 1)
- swap :交换两个 shared_ptr 对象(即交换所拥有的对象)
- reset :放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
- get :返回内部对象(指针)
share_ptr的简单例子:
int main()
{
string *s1 = new string("s1");
shared_ptr<string> ps1(s1);
shared_ptr<string> ps2;
ps2 = ps1;
cout << ps1.use_count()<<endl; //2
cout<<ps2.use_count()<<endl; //2
cout << ps1.unique()<<endl; //0
string *s3 = new string("s3");
shared_ptr<string> ps3(s3);
cout << (ps1.get()) << endl; //033AEB48
cout << ps3.get() << endl; //033B2C50
swap(ps1, ps3); //交换所拥有的对象
cout << (ps1.get())<<endl; //033B2C50
cout << ps3.get() << endl; //033AEB48
cout << ps1.use_count()<<endl; //1
cout << ps2.use_count() << endl; //2
ps2 = ps1;
cout << ps1.use_count()<<endl; //2
cout << ps2.use_count() << endl; //2
ps1.reset(); //放弃ps1的拥有权,引用计数的减少
cout << ps1.use_count()<<endl; //0
cout << ps2.use_count()<<endl; //1
}
share_ptr的缺点为:当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏,此时需要使用weak_ptr。
weak_ptr
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放;它是对对象的一种弱引用,不会增加对象的引用计数;它和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
class B; //声明
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout << "A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout << "B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
cout << pb.use_count() << endl; //1
cout << pa.use_count() << endl; //1
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; //2
cout << pa.use_count() << endl; //2
}
int main()
{
fun();
return 0;
}
可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A、B的析构函数没有被调用)运行结果没有输出析构函数的内容,造成内存泄露。如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_,改为weak_ptr pb_ ,运行结果如下:
1
1
1
2
B delete
A delete
这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减1,同时pa析构时使A的计数减1,那么A的计数为0,A得到释放。
注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(),因为pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:
shared_ptr<B> p = pa->pb_.lock();
p->print();
weak_ptr 没有重载*和->运算符,但可以使用 lock 获得一个可用的 shared_ptr 对象. 注意, weak_ptr 在使用前需要检查合法性.
成员函数:
- expired :用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false.
- lock :用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 - - - shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同.
- use_count :返回与 shared_ptr 共享的对象的引用计数.
- reset :将 weak_ptr 置空.
- weak_ptr :支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数.
智能指针的核心实现
unique_ptr的简单实现
简单的实现了unique_ptr,包括如下成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数,禁用,不支持
- 拷贝赋值函数,禁用,不支持
- reset():释放源资源,指向新资源
- release():返回资源,放弃对资源的管理
- get():返回资源,只是供外部使用,依然管理资源
- operator bool (): 是否持有资源
- operator * ()
- operator -> ()
template<typename T>
class UniquePtr
{
public:
UniquePtr(T *pResource = NULL)
: m_pResource(pResource)
{
}
~UniquePtr()
{
del();
}
public:
void reset(T* pResource) // 先释放资源(如果持有), 再持有资源
{
del();
m_pResource = pResource;
}
T* release() // 返回资源,资源的释放由调用方处理
{
T* pTemp = m_pResource;
m_pResource = nullptr;
return pTemp;
}
T* get() // 获取资源,调用方应该只使用不释放,否则会两次delete资源
{
return m_pResource;
}
public:
operator bool() const // 是否持有资源
{
return m_pResource != nullptr;
}
T& operator * ()
{
return *m_pResource;
}
T* operator -> ()
{
return m_pResource;
}
private:
void del()
{
if (nullptr == m_pResource) return;
delete m_pResource;
m_pResource = nullptr;
}
private:
UniquePtr(const UniquePtr &) = delete; // 禁用拷贝构造
UniquePtr& operator = (const UniquePtr &) = delete; // 禁用拷贝赋值
private:
T *m_pResource;
};
Counter的简单实现
为了实现weak_ptr和share_ptr的引用计数,首先先实现一个Counter计数器类。
Counter对象的目地就是用来申请一个块内存来存引用基数,s是share_ptr的引用计数,w是weak_ptr的引用计数,当w为0时,删除Counter对象。
class Counter
{
public:
Counter() : s(0), w(0){};
int s; //share_ptr的引用计数
int w; //weak_ptr的引用计数
};
share_ptr的简单实现
share_ptr的给出的函数接口为:构造,拷贝构造,赋值,解引用,通过release来在引用计数为0的时候删除_ptr和cnt的内存,各个函数中计数器的变化如下:
- 构造函数中计数初始化为1;
- 拷贝构造函数中计数值加1;
- 赋值运算符中,左边的对象引用计数减1,右边的对象引用计数加1;
- 析构函数中引用计数减1;
- 在赋值运算符和析构函数中,如果减1后为0,则调用delete释放对象。
template<class T>
class Weak_ptr;//先引用
template<class T>
class Share_ptr
{
T * ptr;//管理的指针
Count * cnt;//计数
public:
Share_ptr(T * p = 0):ptr(p)//构造函数
{
ptr = p;
cnt = new Count();
}
Share_ptr(Share_ptr<T> const& s)//拷贝构造函数,
{
ptr=s.ptr;//对象切换
cnt=s.cnt;//值切换
cnt->s++;//share计数增加
}
Share_ptr(Weak_ptr<T> const& w)
{
ptr = w.ptr;
cnt = w.cnt;
cnt->s++; //share计数增加
}
~Share_ptr()//离开生命周期时调用
{
release();//清空内存
}
Share_ptr<T> &operator=(Share_ptr<T> const& s)//赋值运算
{
if(this != &s)//判断是否是相等的指针如果不是
{
release();//释放原有的
ptr=s.ptr;//修改对象指针
cnt=s.cnt;//修改计数器
cnt->s++;//计数器增加
}
return *this;//返回
}
T& operator*()
{
*ptr;//解引用
}
T* operator->()
{
return ptr;//返回原指针
}
friend class Weak_ptr<T>;//方便Weak指针操作本类
protected:
void release()//释放操作
{
cnt->s--;//share计数减1
if(cnt->s<1)//如果小于一了,说明没有指向原指针的share指针
{
delete ptr;//删除ptr,调用ptr的析构函数
if(cnt->w<1)//如果weak引用小于1了再去删除cnt
{
delete cnt;
cnt=NULL;
}
}
}
};
weak_ptr简单实现
weak_ptr的作为弱引用指针,其实现依赖于counter的计数器类和share_ptr的赋值。
weak_ptr一般通过share_ptr来构造,通过expired函数检查原始指针是否为空,lock来转化为share_ptr。
template<class T>
class Weak_ptr
{
T * ptr;
Count * cnt;
public:
Weak_ptr()//构造函数
{
cnt=0;
ptr=0;
}
Weak_ptr(Weak_ptr<T> &w):ptr(w.ptr),cnt(w.cnt)//同上
{
cnt->w++;
}
Weak_ptr(Share_ptr<T> &s):ptr(s.ptr),cnt(s.cnt)//同上
{
cnt->w++;
}
Weak_ptr<T>& operator=(Weak_ptr<T> &w)//同上
{
if(this!=&w)
{
release();
cnt=w.cnt;
ptr=w.ptr;
cnt->w++;
}
return *this;
}
Weak_ptr<T>& operator=(Share_ptr<T> &s)//同上
{
release();
cnt=s.cnt;
ptr=s.ptr;
cnt->w++;
return *this;
}
Share_ptr<T> lock()//强转
{
return static_cast<Share_ptr<T>>(*this);
}
~Weak_ptr()//释放
{
release();
}
friend class Share_ptr<T>;
protected:
void release()
{
cnt->w--;
if(cnt->s<1&&cnt->w<1)
{
//这里的cnt不用删除,因为在share中已经被删了
//delete cnt;
cnt=NULL;
}
}
};
shared_ptr的线程安全性
- 引用计数增加是安全的,引用计数在堆上。
- 不同线程同时操作同一个shared_ptr的引用是不安全的 。
- 不同的shared_ptr指向同一块内存,操作同一个内存也是不安全的(即shared_ptr指向对象的读写不是线程安全的)。
shared_ptr 是引用计数型智能指针,几乎所有的实现都采用在堆上放个计数值的办法。具体来说,shared_ptr 包含两个成员,一个是指向 Foo 的指针 ptr,另一个是 ref_count 指针,指向堆上的 ref_count 对象。ref_count 对象有多个成员,具体的数据结构如图 1 所示,其中 deleter 和 allocator 是可选的。
图 1:shared_ptr 的数据结构。
为了简化并突出重点,后文只画出 use_count:
以上是 shared_ptr x(new Foo); 对应的内存数据结构。
如果再执行 shared_ptr y = x; 那么对应的数据结构如下:
但是 y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生。
中间步骤 1,复制 ptr 指针:
中间步骤 2,复制 ref_count 指针,导致引用计数加 1:
步骤1和步骤2的先后顺序跟实现相关(因此步骤 2 里没有画出 y.ptr 的指向),我见过的都是先1后2。
既然 y=x 有两个步骤,如果没有 mutex 保护,那么在多线程里就有 race condition。
多线程无保护读写 shared_ptr 可能出现的问题
考虑一个简单的场景,有 3 个 shared_ptr 对象 x、g、n:
shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr
shared_ptr<Foo> x; // 线程 A 的局部变量
shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量
一开始,各安其事。
线程 A 执行 x = g; (即 read g),以下完成了步骤 1,还没来及执行步骤 2。这时切换到了 B 线程。
同时编程 B 执行 g = n; (即 write G),两个步骤一起完成了。
先是步骤 1:
再是步骤 2:
这时 Foo1 对象已经销毁,x.ptr 成了空悬指针!
最后回到线程 A,完成步骤 2:
多线程无保护地读写 g,造成了“x.ptr 是空悬指针”的后果。这正是多线程读写同一个 shared_ptr 必须加锁的原因。
make_shared()
智能指针shared_ptr有两种初始化的方式:
shared_ptr<int> sp1 (new int(10)); //通过new构造数据对象,调用了share_ptr的构造函数
shared_ptr<int> sp2 = make_shared<int>(10); //通过make_shared构造数据对象
之前看过一些相关的文档,描述了这两种方式的不同,主要的区别是说通过new构造的时候:
-
通过new构造,涉及到两次内存分配,第一次是通过new为数据对象分配内存,即上方的new int(10),第二次是构造一个shared_ptr的管理对象,管理对象记录了强引用(shared_ptr)计数,弱引用(weak_ptr)计数,以及数据对象(new int(10))的地址。当管理对象发现强引用计数为0时,释放数据对象的内存,当管理对象发现弱引用计数为0时,释放管理对象的内存。
-
通过make_shared构造,只分配一次内存,这一块内存里既包括管理对象,也包括数据对象。由于是在一块内存里,所以即使强引用计数已被清零,但如果弱引用计数还没有清零,那么也无法释放这一块内存,直到弱引用计数清零时,这一块内存(包括管理对象和数据对象)才能被释放。
注意:构造函数是保护或私有时,无法使用 make_shared()。
share_ptr销毁了,但还有weak_ptr指向那个对象,weak_ptr怎么知道这个对象已销毁?
share_ptr销毁的时候,只是把指向的对象销毁了,而计数器Counter还没被销毁,Counter里面记录了share_ptr的引用计数以及weak_ptr的引用计数,weak_ptr可以通过查询Counter里面的值来知道对象已被销毁。当weak_ptr也小于1时,Counter才会被销毁。
share_ptr/unique_ptr自定义删除器
默认情况下,智能指针使用 delete 释放其管理的资源,有时候,可能要修改默认使用 delete 释放资源的行为,除此之外,我们也可以自定义删除器。
Connection 是一个管理连接类,在释放 Connection 之前,我们需要调用 close 函数来关闭连接。观察如下代码:
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Connection{
public:
explicit Connection(string name):_name(name){
}
string get_name() const {
return _name;
}
private:
string _name;
};
void close(Connection* connection){
cout <<string ("关闭")+connection->get_name ()+"管理的连接中..." << endl;
// 关闭连接的代码
// .....
cout << "关闭完成。" << endl;
}
int main(){
// 新建管理连接 Connection 的智能指针
shared_ptr<Connection> sp(new Connection);
unique_ptr<Connection> up(new Connection);
}
执行上述代码,发现并没有办法调用 close 函数,因为控制权完全在 shared_ptr/unique_ptr 中。你可能会说,在退出作用域之前我调用 close (sp.get ()) 先关闭连接,这样不就可以了嘛?实际上,这种做法对于 shared_ptr 并不安全,手动 close 之后,不能确保 sp 管理的 Connection 只有一份拷贝(即 sp 中的计数器多于 1)。因此,需要使用自定义的删除器。
删除函数定义类似于:
void Deleter(Connection *connection){
close(connection);
delete connection;
}
当删除器的指针 Deleter 传给 shared_ptr/unique_ptr 时,shared_ptr/unique_ptr 不会使用默认的 delete val 来释放其管理的资源,而是使用 Deleter (val) 来释放资源,这样就调用了 Deleter 来释放管理的资源。后面的各种方式的原理也是如此。
int main(){
// 新建管理连接 Connection 的智能指针
shared_ptr<Connection> sp(new Connection("shared_ptr"), Deleter);
unique_ptr<Connection, decltype(Deleter)*> up(new Connection("unique_ptr"), Deleter);
}
shared_ptr 在使用的时候,只需要把函数式删除器的指针传给构造函数就行;而 unique_ptr 还用增加一个模板参数 decltype (Deleter)*,这是 shared_ptr 和 unique_ptr 的不同点之一。