智能指针

先说指针
指针保存对象地址, 通过指针可以间接访问该对象.
指针的作用(列出以下几点),

1. C/C++ 中参数传递默认使用值语义, 这样函数内部无法修改传进来的参数值,这种情况需要使用以值的方式传递参数地址, 函数内部解引用后操作该对象.
2. C/C++中内存分配: 一般在函数内声明的变量分配在调用栈空间, 静态变量分配在静态存储区, 文字常量编译时值已确定, 也分配在地址空间某个区域, 但如果想使用地址空间的堆空间, 必须有某种方式去引用这块内存, C/C++中提供的方式是指针
3. 参数传递, 相比较一个4k大小的变量, 更喜欢传递4byte的变量地址,
4. 函数指针, 增加了一层间接性, 可以在运行时决定执行的操作

5. 转型, 对内存的重新解释


上面的第二点在使用地址空间中的堆区后在C/C++中是需要程序员显示去释放的, 所谓释放无非是通知操作系统标记该内存为空闲, 可以供其他代码使用, 不然, 这块内存将一直无法使用, 这也是所谓的内存泄露, 堆栈上的内存没有类似问题, 在执行线程内, 其操做模式决定了其必然自动扩展和消除. 静态和文字常量其生命周期与程序一样长, 自然无销毁其内存这一说, 代码段只读, 更无释放内存问题. 有的只是内存换页时换进换出, 这条对于所有内存全适用(除非特殊操作VirtualLock)
显示释放动态内存给C/C++程序带来很多bug, C中很原始, 你malloc了内存, 那你必须记得调用 free 函数来释放, 遇到函数内分配内存而返回给调用函数的情况, 要不由调用者来分配, 然后由调用者释放, 要不由被调用函数分配, 以注释或其他不成文的方式约定由调用者来释放, 比如Mac平台上, 说凡是以类似Create开头的函数返回的对象,全由调用者来释放,有些还需规定调用者必须使用指定的函数来释放,因为分配函数不一定是malloc,有可能是某个其他的分配策略,分配/释放必须匹配。
C++ 中增加了异常机制, 在触发异常后, 一般执行栈展开, 如果异常发生在销毁动态内存之前,那么,即时 C++ 保证栈上的所有对象都会被析构,但指针对象只是一般对象,并没有析构这一说,他在栈上占4字节内存(32bit),除了销毁这4字节内存外别无其它。

RAII
资源获取即初始化,说的是利用对象的生命周期来控制资源,因为C++对对象作出了很多保证,比如对象在超出其作用域后析构函数确保调用,栈展开时对象析构器的调用等,这样如果把某个资源与某对象绑定,那么在对象被析构时,资源也将释放。
内存当然是一种资源,资源还有很多其他类型,比如Win32的句柄,内核对象,网络连接,你觉得是资源的都算,资源嘛,必定不是无限的。
这样很自然的想法是我们构造这样的一个对象,在其初始化时与其绑定一内存,而在其析构中放置销毁该内存的代码,STL 中的auto_ptr就是这样的策略,初始化时给他一需要释放的指针,然后他会记住去释放这块内存,没你什么事了, C++ 11 STL 中的unique_ptr,boost 中的 scoped_ptr 均是这样的模型,不同的地方是在赋值时的策略不一样,有转移内部对象的(auto_ptr),有禁止复制/赋值的(scoped_ptr),也有虽不支持复制/赋值但却支持move语义(unique_ptr)。

引用对象
对于一些对象,我们只需或者说应该只有一个对象,其拷贝没有意义,比如说两个自己,有意思只发生在天马行空的幻想里,对于这类对象,我们一旦创建出来,其它地方对其引用即可,问题是这样的对象占用的内存什么时候释放? 首先你不可能把这样的对象分配在栈上。既然是堆上, 那创建者释放,不是,调用者释放也不是,有时某个地方还想保存其一段时间,比如使用其作为对象的一成员,那么其生命期必须大于该包含对象的生命期。
解决这样的问题,我们必须记住当前有多少对其引用,在没有任何引用时,或可考虑释放,这好办的,因为对象的引用次数增减可由 赋值构造,复制构造,析构,或显示的拷贝函数来决定,这样我们就需要某个类似计数器的东西,问题是把它放哪? 大致有两种方式:
一种是所谓的侵入式,无非是说在每个引用对象内部增加一个计数器成员,这样就好了,Objective-C 中的Cocoa框架基类NSObject就是这样的,作为所有类型的基类,其内部已经实现了计数对象,计数函数,如果你想使用引用计数功能只需继承该类,也就变成了所谓的Cocoa类型。但是对于C++,有大量的现有类是没有内嵌计数器的,不仅如此,如果你构造出这样的基类,那些不想使用这样特性的也不答应,你非要强加我不要的东西,换我也是不愿意的。况且这样的东西毕竟有空间开销,但话说回来,不然又怎么是引用对象,而引用对象与一般的值对象并没有像在托管语言中这样详细区分,大家意见并不是那么统一,有份来自MSDN的建议可参考.


这样就引出了第二种实现方案,也是最为通用的,
我们不妨这样,先创建一个需要被引用的对象,然后在创建另一个对象,封装之前的对象,内部包含一计数器,职责是记录之前对象的引用次数,接口方面提供指针变量支持的接口,这样在使用该对象时与使用原先对象的指针别无二样,一般实现还会采用模板形式,这样可以参数化 欲封装的指针类型,boost/ C++ 11 STL 中均提供该类型: shared_ptr。

引用计数引起的资源互锁(weak_ptr 的引入)
形如这样的代码就会出现资源互锁的现象,
struct Owner {
 boost::shared_ptr<Owner> other;
};  


boost::shared_ptr<Owner> p1 (new Owner());  
boost::shared_ptr<Owner> p2 (new Owner());
p1->other = p2; //   p1 references p2
p2->other = p1; // p2 references p1
P1,P2 所引用的内存将不会得到释放,简单在脑中推理下便很明了了,
weak_ptr 对象的引入便是解决如此问题,这时,我们一般会互锁的两个对象一个是说为的parent方,另一个是child方,parent拥有child是理所当然的,管理它嘛,但child并不真正有用parent,也就是不对其引用计数进行操作,但还是 拥有其引用计数 的。具体参考MSDN或ios文档对内存的讲解,或其他资源了。

智能指针与STL容器
不推荐把指针直接放入容器的原因是你还是必须显示去管理每个元素引用的内存,你当然可以这样做。STL容器还要求其元素必须可拷贝,因为在扩展容量时需要搬移对象,这样无法复制的对象就不能放入容器中, (这里额外注意两点, 1是 异常安全, 2是对象拷贝开销) 如果你恰巧有这样的对象,或不想过问内存释放(你必须时刻记住在销毁容器同时去释放容器中每个元素指向的内存)可以把对象放入shared_ptr, 而后把shared_ptr对象放入容器,从此云淡风清。关于这点,boost提供存放指针类型的数组,可以考虑。

智能指针的开销
没有免费的午餐,先说形式上的,当你使用某个对象T时,你现在面对时形如 shared_ptr<T> 的东西,当然没T*直观,但好歹有提醒,摆明之效,我向来觉得什么东西不要太隐晦,说明就是这么个事实好,尤其是程序上的事情,好的抽象固然是好的,但有时会让使用者忘记其真正做的是什么事情,并不总是好的。
对于非引用策略的简单RAII实现,我们只需保存一个指针,这固然是没有什么开销的,提供的接口方面,使用inline也可免除,但对于引用策略,计数器是必须的,其一是占用内存,也就是说每一引用对象,如今必多一个计数器的空间开销,另一方面,计数器必须动态分配,诸如new,malloc的内存分配开销还是很客观的,这也是为什么STL与boost高版本中提供 make_shared 函数,据说它把两次分配动作(一次是分配引用对象,一是分配计数器)缩减为一次了,很有趣。


多说些关于shared_ptr 的内容
上面有说过分配/释放必须配对,而正常模式下,shared_ptr是采用delete来释放内存的,但也可以在构造时提供一个用来执行释放操作的函数对象,该函数对象须提供类似于这样的接口 void operator()(T* p)const。
进一步把分配/释放模式抽象出来,放到其他资源上,其必然有获取/归还 这一过程,宽泛的说,如果一对象职责是用来管理另一对象的生存期,都可划为智能指针一类,不同的只是其实现策略不同

 
Ref,
Memory addressing (abstract)
Welcome Back to C++(Modern C++)
What is a smart pointer and when should I use one?

posted @ 2012-05-07 19:52  godion  阅读(1015)  评论(4编辑  收藏  举报