1. 问题和思路
shared_ptr来自boost,现在已经被C++11收入麾下。
当确实需要在代码中有多处指针指向同一块堆内存时,unique_ptr就用不上了,此时需要使用shared_ptr.
编程语言中指针的引入,很大的一个原因就是需要在代码中的多处(空间或时间上)可以操作同一个对象(同一内存),特别是当程序存在并发时,对同一对象的操作时序也无法保证。而在并发应用中,更是可能在不同的时间线上存在多个指针指向同一处内存。
背后的业务逻辑是存在不同时间,不同地点操作同一数据对象的需求。
单一线程(无并发)的应用中,通常在流程复杂到一定程度时,才会造成指针管理困难。
举个例子,先定义一个变量:ilst,它是一个用于保存指针的容器:
std::list <int *> ilst;
然后有一个Add_1(int* p)函数,它判断入参p指向的整数是不是奇数,如果是,将它加入ilst中;再有一个Add_2(int* p)函数,它判断入参p指向的整数是否是3的倍数,如果是,也加入ilst中。最后main函数
int main()
{
for(int i = 0; i < 100; ++i)
{
int * p = new int(i);
Add_1(p);
Add_2(p);
}
/*此处是围绕ilst的其他处理*/
//最后释放列表中的指针:
for(auto it = ilst.begin(); it != ilst.end(); ++ it)
{
/* 如果一个整数既是奇数,又是3的倍数,则会添加两次,然后再释放两次 */
delete * it;
}
}
出大问题了。
一个整数可能既是奇数,又是3的倍数,所以存在一些指针p被前后加入到ilst中两次。于是在第二个for循环中被释放了两次
既然问题出在内存释放上,能不能使用“std::unique_ptr”解决呢?不行,unique_ptr包装下的指针不能复制,决定了某个指针一旦采用push_back(move(p))的语法加入列表(其中p的类型是unique_ptr <int>),p所拥有的裸指针将变成空指针,后续对它的取值,就是在访问非法内存。
【课堂作业】:感受std::unique_ptr的唯一性
将上例代码中的int*改为std::unique_ptr<int>类型,编译通过后测试运行
说白了,我们就是既要让多个指针共项同一内存,有不想手工释放。因为越是存在多个指针指向多一块内存的情况,就越难以靠程序员的大脑去维护。哪块内存已经没有任何指针直线它(因此千万别忘了释放),哪块内存还有几个指针在指向(因此千万别释放)
要解决这一问题,引入GC(Garbage Collection(垃圾回收))是个办法。为此,程序需要有一个额外的“保姆”线程用于查找,标记哪些堆内存已经不在使用中(没有任何指针指向它),然后释放;而为了保证标记的正确性,该线程在工作时,往往要求程序中的其他线程暂停工作。对程序性能,GC有正面影响,也有负面影响,总体上负面影响巨大
当前C++语言没有GC机制,除性能上的考虑之外,关键还在于引入GC后,会对原有的编程方式影响较大,特别像原有RAII机制中的析构函数调用时机的明确性等。
【重要】:C++天生的GC机制:栈内存
C++支持各种对象在栈上分配内存,而栈的内存可自动回收,因此事实上“栈”是C++语言中天生的性能高效加是一个方便的GC实现。依赖独立线程实现GC的语言,通常只支持内置的简单类型(int,double,boolean等)在栈中分配(会自动回收),复杂对象均需在堆中分配(必须由GC线程回收)。
shared_ptr智能指针,正如其名“shared”可用于管理多个指针共同拥有的内存。
int * pa = new int(123);
int * pb = pa;
pa、pb两个指针共同指向同一块内存,就可以称作pa、pb共享内存。
如果我们通过“pa”释放发所指向的内存,就会造成“pb”指向一块已经被释放的内存,如果随后又想通过“pb”访问那块内存,程序就会运行出错,结果可能悄无声息,也可能程序直接过掉。示例如下:
delete pa;
* pb = 124; ///pb所指向的内存,其实已被收回
///或者,尝试释放
delete pb;
引用计数
shared_ptr的思路是:另弄一个计数,记录待管理的那块内存到底有几个指针指向它,每当有个指针要被释放前,通过计数检查它是不是最后一个指向该内存的指针,如果不是,就不真正释放内存,只是将计数减一,如果是则释放内存。这个“计数”的专业术语叫做“引用计数”
假设有一块内存叫M,然后先后有两个智能指针引用它,我们画四步示意,
首先,智能指针s1创建时,申请了一块内存M,这时计数值为1。
接着,智能指针s2也要引用内存M,于是计数值升为2。
后面,智能指针s1出了作用范围,自动析构,于是计数减1,但内存M并没有被释放
直到s2出了作用范围并析构,计数减为0,M被真正释放。
这其中最重要的一步是第2步,s2创建时,并没有自己申请一块内存,否则,它和s1就井水不犯河水,不存在“共享”了。
而这其中没有提到但最重要的一个实现原理,就是“智能指针”本身其实是一个“栈”对象,不是特指某个智能指针,而是指全部,所以它才会有“出了作用范围”这一说。当新的智能指针对象构造时,会去自增引用计数,析构时则自减引用计数。
2. shared_ptr模拟实现:
#include <iostream>
//当指针不可用,用户又要强行用时,一死了之
#include <cassert>
using namespace std;
//用以记录引用计数
struct MyRefCount
{
int count;
MyRefCount()
: count(1)
{
}
};
template <typename T>
class MySharedPtr
{
public:
//explicit可以防止赋值式构造
explicit MySharedPtr(T* ptr)
//这里创建引用计数
: _ptr(ptr), _rc(new MyRefCount)
{
}
//拷贝构造, 构造一个新的MySharedPtr
//构造入参是MySharedPtr引用
MySharedPtr(MySharedPtr const& sp)
: _ptr(sp._ptr), _rc(sp._rc)
{
++ (_rc->count);
}
//赋值运算符重载
MySharedPtr & operator = (MySharedPtr const& sp)
{
if(& sp == this) ///this是个指针
{
return *this;
}
//将参数的裸指针和引用计数指针,赋给当前对象
_ptr = sp._ptr;
_rc = sp._rc;
++ (_rc->count); //引用计数增加1
return *this; //返回当前对象,即一个新的MySharedPtr
}
~MySharedPtr() //MySharedPtr析构
{
--_rc->count;
//只有当引用计数为0时,
//才会删除裸指针,同时删除引用计数指针
if(_rc->count == 0)
{
delete _ptr;
delete _rc;
}
}
T* operator ->()
{
if(_rc->count == 0)
{
return nullptr;
}
//this->a,即为_ptr->a
return _ptr;
}
T& operator * ()
{
assert((_rc->count > 0));
return * _ptr;
}
int GetRefCount() const
{
assert((_rc->count > 0));
return _rc->count;
}
private:
T* _ptr; //裸指针
MyRefCount* _rc; //引用计数
};
void testMySharedPtr()
{
MySharedPtr <int> sp1(new int);
cout << "ref count = " << sp1.GetRefCount() << endl; ///
{
MySharedPtr <int> sp2(sp1);
cout << "ref count = " << sp2.GetRefCount() << endl;
*sp2 = 10;
cout << "*sp2 = " << *sp2 << endl;
} //sp2生命周期结束,被析构
cout << "*sp1 = " << *sp1 << endl;
cout << "ref count = " << sp1.GetRefCount() << endl;
}
003行产生了一个全新的智能指针sp1,它分配一个整数的内存。现在sp1的引用计数是1
006行,特意嵌入一个子代码块,以便人为地改变变量的作用域。
007行,从sp1拷贝构造出sp2,意味着sp2和sp1各自拥有的裸指针,指向同一块内存。
现在sp2的引用计数是2,sp1的引用计数当然也是2,因为它们各自的_rc指向的引用计数对象是同一个。
010行通过sp2,修改了所指向的整数的值。
012行,内嵌的代码块结束,于是作为栈变量,sp2结束生命周期,被析构。根据推理,此时引用计数减为1,裸指针没有被杀出。
014行,尝试用来证明sp1还正常地活着,我们访问了它的裸指针的内容,确实还是10.
015行输出的当前引用计数,将是1
这个函数结束,sp1也被析构,这次裸指针将被删除。
修改代码,让MySharedPtr持有一个用户自定义类,并尝试输出更加详细的信息。
MySharedPtr在模拟裸指针的行为上,还有许多事需要完善。比如它是并发不安全的,当多个线程同时修改引用计数等操作时,需要对计数操作加锁等。