shared_ptr 能够知道它所指向的内存资源还有没有人在使用?从而可以在没人使用的情况下自动释放这块内存资源。
shared_ptr 对它所指向的内存资源的管理使用引用计数。
// shared_ptr 定义在头文件中
#include
#include
using namespace std;
int main()
{
shared_ptr<int> spFirst( new int );
// 这时,只有 spFirst 这个指针指向这块 int 类型的内存资源,
// 所以这时它的引用计数是 1
cout<<"当前引用计数: "<<spFirst.use_count()<<endl;
{
// 创建另外一个 shared_ptr,并用 spFirst 对其进行赋值,
// 让它们指向同一块内存资源
shared_ptr<int> spCopy = spFirst;
// 因为 spFirst 和 spCopy 都指向这一块内存资源,
// 所以这一资源的引用计数增加为 2
cout<<"当前引用计数: "<<spFirst.use_count()<<endl;
}
// 当超出 spCopy 的作用域, spCopy 结束其生命周期,
// 这块内存资源的引用计数减 1 而重新变为 1 cout<<"当前引用计数:
"<<spFirst.use_count()<<endl;
// 当程序最终结束执行返回, spFirst 指针也结束其生命周期后,
// 从此没有任何指针指向此内存资源,引用计数减为 0,内存资源自动得到释放
return 0;
}
通过一个简单的引用计数, shared_ptr 就知道了当前还有多少人在使用它所管理的内存资源, 从而也就可以在使用者减少为 0 的时候,聪明地自动释放掉这块内存资源。
C++11 的标准库中提供了一个 make_shared()
函数,我们可以使用它来完成内存资源的申请并直接交给 shared_ptr 进行管理。
make_shared()
是一个函数模板, 它以需要创建的对象类型为模板参数,而它返回的正是指向这个新创建对象的shared_ptr
。如果这个类的构造函数需要参数,它还可以带有不定个数的参数,并在执行的时候将它们转递给这个类的构造函数作为其参数。
// 使用 int 作为模板参数, make_shared()将创建一个 int 对象,
// 并返回一个指向这个对象的 shared_ptr 指针
shared_ptr<int> spFirst = make_shared<int>();
// 使用 string 作为模板参数, make_shared()将创建一个 string 对象,
// make_shared()的参数将传递给 string 类的构造函数作为其参数,
// 我们最终得到的 shared_ptr 指向一个 string 对象,
// 而这个 string 对象的内容是“WangGang”
shared_ptr<string> spName = make_shared<string>("WangGang");
make_shared()的使用,省去了程序员使用 new 操作符直接申请内存的危险操作,将内存资源的申请和释放完全交给标准库去完成。
shared_ptr为了得到能够在内存资源的使用者个数为 0 时自动释放这块内存资源的能力,它增加了额外的引用计数而牺牲了一定的性能。
如果在不需要 shared_ptr 的场景下过度滥用 shared_ptr,不仅无助于内存资源的管理,反而可能会影响程序的性能,最终得不偿失。只有在合适的场景下使用 shared_ptr,才能起到事半功倍的效果。
当出现以下情况时应该优先考虑使用 shared_ptr 来管理内存资源:
class SalarySys
{
// …
public:
~SalarySys()
{
// …
// 首先,释放容器中普通指针所指向的资源
for(Employee* p : m_vecEmp)
{
delete p; // 释放指针所指向的对象
}
// 然后,用 clear()函数清空容器
m_vecEmp.clear();
}
// …
private:
vector<Employee*> vecEmp; // 保存普通指针的容器
}
如果把这个 vecEmp 容器中的普通指针替换成相应的 shared_ptr,这个过程会简单的多。我们只需要使用 clear()函数清空容器中保存的 shared_ptr,而随着 shared_ptr 的释放,它会自动释放它所管理的资源,而无需我们主动去释放:
class SalarySys
{
// …
public:
~SalarySys()
{
// …
// 用 clear()函数释放容器中的 shared_ptr
// shared_ptr 在释放的时候也会连带地释放它所管理的 Employee 对象
m_vecEmp.clear();
}
// …
private:
vector<shared_ptr<Employee>> vecEmp; // 保存 shared_ptr 的容器
}
shared_ptr 实际上是定义在
头文件中的一个类模板,如果我们需要用它来管理某种类型的一块内存资源(某个类型的对象或者数值数据),那就需要使用这个特定的数据类型来作为shared_ptr 类模板的类型参数,以形成一个具体的可以指向这个特定数据类型内存资源的 shared_ptr模板类,然后用这个模板类创建 shared_ptr 对象就可以用于管理相应类型的内存资源。
// 为了管理一块 int 类型的内存资源,使用 int 作为 shared_ptr 类模板的类型参数
// 得到相应的模板类 shared_ptr并创建对象 spInt
shared_ptr<int> spInt;
// 为了管理 Staff 对象而创建的 shared_ptr
// 这里我们用 Staff 的基类 Employee 作为类型参数
// 相当于使用基类类型的指针指向子类类型的对象
shared_ptr<Employee> spEmp;
上面的代码只是利用特化后的 shared_ptr 模板类创建了相应类型的 shared_ptr 对象,但此时这些 shared_ptr 尚未指向任何内存资源,自然也就无法直接使用。
更多时候,我们通过给 shared_ptr的构造函数提供一个有所指向的普通指针、一个 new 操作符返回的指针、甚至另外一个 shared_ptr对象作为参数,让这个 shared_ptr 创建之后即指向某块内存资源或者某个对象,从而可以立即投入使用。
// 通过有所指向的普通指针创建 shared_ptr
int* p = new int(0);
shared_ptr<int> spInt(p); // spInt 和 p 都是指向刚刚申请的 int 类型内存资源
string strName = "WangGang";
int nYear = 1982;
// 通过 new 操作符返回的指针创建 shared_ptr
// 同时传递的 strName 和 nYear 会被传递给 Staff 的构造函数用于创建 Staff 对象
// 完成后, spEmp 指向这个新创建的 Staff 对象
shared_ptr<Employee> spEmp(new Staff(strName,nYear));
// 通过 shared_ptr 创建 shared_ptr
// 完成后, spBoss 和 spEmp 指向同一个 Staff 对象
shared_ptr<Employee> spBoss(spEmp);
shared_ptr 的使用跟普通指针的使用几乎一模一样。
“*”
符号放在一个 shared_ptr 对象的前面,我们就可以得到它所指向的对象,进而可以对其进行读写操作。“->”
符号放在 shared_ptr 对象的后面,直接调用它所指向对象的成员函数。// spInt 指向一个 int 类型的数据
// 使用“*”得到 spInt 所指向的数据并写入数据
*spInt = 1;
// spEmp 指向的是一个 Staff 对象
// 使用“->”调用 spEmp 所指向对象的成员函数
int nSalary = spEmp->GetSalary();
// 使用“*”得到 spEmp 所指向的 Staff 对象,读取并赋值给 other 对象
Staff other = *spEmp;
// 一个以 shared_ptr 作为参数类型的函数
void Who(shared_ptr<Employee> sp)
{
// 调用 shared_ptr 参数所指对象的成员函数
cout<<"我是"<<sp->GetName()<<endl;
}
// 使用 spEmp 调用函数,向函数内传递它所指向的 Staff 对象
Who(spEmp);
reset()函数可以用于重新( re)设置( set)一个 shared_ptr 对象所管理的内存资源。
“=”
赋值操作符来完成。// 停止 spEmp 对 Staff 对象的管理
spEmp.reset();
// 将 spEmp 指向一个新创建的 Officer 对象
spEmp.reset(new Officer("Muench",1986));
// 此时, spBoss 也同样停止了对原有 Staff 对象的管理而重新指向这个 Officer 对象
spBoss = spEmp;
虽然可以像普通指针一样地直接使用 shared_ptr 来访问它所指向的内存资源,但在某些特殊情况下,比如需要对指针进行加减运算以访问某个范围内的内存资源,或者是需要判断某shared_ptr是否指向了某个内存资源以便对其进行进一步的访问。这时,我们就可以使用 shared_ptr 的 get()成员函数来获得指向它所管理的那块内存资源的普通指针,进而通过普通指针直接访问内存资源。
const int N = 30;
// spData 指向一块可以保存 N 个 int 类型数据的内存资源
shared_ptr<int> spData(new int[N]);
// 通过将 get()函数的返回值与 nullptr 进行比较,判断 spData 是否指向了某块内存资源
// 这里也可以简单地将 spData 直接与 true 进行比较以判断 spData 是否有效
// 等同于 if(true == spData)
if(nullptr != spData.get())
{
// 通过 get()函数获得这块内存资源的地址并保存到普通指针 p
int* p = spData.get();
for(int i = 0; i < N; ++i)
{
cin>>*p; // 通过普通指针直接访问这块内存资源
++p; // 对普通指针进行加 1 运算,使其指向下一个 int 数据
}
}
用 get()函数获得 shared_ptr 所管理内存资源的地址,实际上就是从shared_ptr 手中盗取了这块内存资源的管理权,这样,对这块内存资源的访问就不在 shared_ptr 的控制之下,通过普通指针和 shared_ptr 都可以访问这块内存资源,也就很有可能出现内存访问的冲突。所以,如非必要,不要使用 get()函数,即使使用,也该谨慎处理它所返回的普通指针。
use_count()函数返回 shared_ptr 的引用计数。它在调试的时候特别有用,因为它可以在程序执行的关键点获得某块内存资源的引用计数,这可以为调试提供非常有用的信息。
swap()函数可以很方便地交换两个 shared_ptr 所管理的内存资源,同时也会更新它们各自的引用计数。例如:
shared_ptr<Employee> spBoss(new Officer("WangGang",1986));
shared_prt<Employee> spStaff(new Staff("ChenLiangqiao",2013));
// 交换 spBoss 和 spStaff 所指向的对象
// 交换完成之后, spBoss 指向一个 Staff 对象,
// 而 spStaff 指向一个 Officer 对象
spBoss.swap(spStaff);
例子:运用 shared_ptr 来轻松地解决内存泄漏问题:
#include
#include
#include // 引入 shared_ptr 所在的头文件
using namespace std;
// 使用 shared_ptr 代替普通指针向函数内传递数据
bool WriteToFile(shared_ptr<int> spScore, const unsigned int nCount)
{
// 首先判断 spScore 是否有效
if(false == spScore)
return false; // 如果无效,直接返回 fasle,表示操作失败
// 打开输出文件…
// 用 get()函数获得 shared_ptr 所指向内存资源的地址并保存到普通指针 p
// 这里指针 p 所指向的内容不能修改,所以我们在 int 之前加上 const 修饰
const int* p = spScore.get();
for(unsigned int i = 0; i < nCount; ++i)
{
out<<*p<<endl; // 通过普通指针直接读取内存位置上的数据并输出
++p; // 对普通指针进行加 1 运算,指向下一个数据所在的位置
}
out.close();
return true;
}
int main()
{
while(true) // 构造输入输出无限循环
{
// 输入数据个数 N…
// 根据需要动态地申请可以容纳 N 个 int 类型数据的内存资源,并用返回的内存地址
// 创建 spScore 对象,也就是将这块内存资源交给 spScore 进行管理
// 此时,只有变量 spScore 指向这块内存资源,其引用计数是 1
shared_ptr<int> spScore(new int[N]);
// 获得指向这块内存资源的普通指针
int* p = spScore.get();
for(unsigned int i = 0;i < N; ++i)
{
cout<<"请输入第"<<i+1<<"个数据: ";
// 通过普通指针将输入的数据直接写入相应的内存位置
cin>>*p;
++p; // 对普通指针进行加 1 运算,指向下一个内存位置
}
// 用 spScore 作为参数,向函数传递它所管理的内存资源
// 此时,变量 spScore 和函数参数 spScore 都指向这块内存资源,
// 其引用计数增加 1 变为 2
WriteToFile(spScore,N);
// WriteToFile()执行完毕,函数参数被释放,
// 此时只有变量 spScore 指向这块内存资源,其引用计数减 1 变为 1
}// 一次 while 循环结束,变量 spScore 也被释放,
// 此时没有任何 shared_ptr 指向这块内存资源,其引用计数为 0,资源自动被释放
return 0;
}
shared_ptr 虽然能够自动释放它所管理的内存资源,但是这种释放还无法做到及时,往往是某块内存资源在逻辑上已经使用完毕了,只是因为管理它的 shard_ptr依然有效而无法得到及时的释放,在某种程度上也耗费了内存资源。
两个 shared_ptr 有可能会形成资源互锁。两者互为前提, 最后的结果是谁都无法得到释放,最终用来解决内存泄漏问题的 shared_ptr 却导致了新的内存泄漏问题。
weak_ptr 表示对内存资源的一种弱( weak)引用,当某个 weak_ptr 指向某个内存资源时,并不会改变其引用计数。 weak_ptr 只是这个内存资源的一个旁观者,它拥有内存资源的访问权,但没有所有权。通过各自指向对方的 weak_ptr,它们都可以访问对方,但无法控制对方的生死。
shared_ptr 所管理的资源的释放工作都是由它的删除器( deleter)来完成的。 shared_ptr 提供了一个可以接收某个函数指针(或函数对象)为参数的特殊的构造函数,通过这个构造函数,我们可以指定这个函数作为 shared_ptr 的删除器,从而让我们有机会通过这个函数对 shared_ptr 的释放工作进行自定义:
// shared_ptr 带删除器参数的构造函数
template<class Other, class D> shared_ptr(Other * ptr, D dtor);
第一个参数指向 shared_ptr 将要对其进行管理的某个资源(这里不再仅仅是内存资源,也有可能是程序中的其他共享资源,比如某个文件);
第二个参数则是负责释放这个资源的函数,也被称为 shared_ptr 的删除器。
当 shared_ptr 所管理资源的引用计数变为 0 时,它就会以指向这个资源的指针作为参数来调用这个函数,以此来完成资源的释放清理工作。这对于 shared_ptr 管理那些不是用 new申请也不是用 delete 释放的非内存资源时非常有用。
#include
#include
using namespace std;
// 负责 shared_ptr 释放工作的函数
// 其参数类型为指向 shared_ptr 所管理资源的指针
void CloseFile(ofstream* pLogFile)
{
// 完成额外的清理工作
(*pLogFile)<<"日志文件结束"<<endl; // 输出文件结束信息
pLogFile->close(); // 关闭文件
delete pLogFile; // 释放内存资源
}
// 某个需要记录日志的函数
void foo(shared_ptr<ofstream> spLogFile)
{
// 通过 shared_ptr 管理的文件输出日志
(*spLogFile)<<"foo()函数被调用"<<endl;
}
int main()
{
// 创建一个日志文件并交由 shared_ptr 进行管理
// 同时指定这个资源的释放工作由 CloseFile()函数完成
shared_ptr<ofstream> spLogFile(new ofstream("log.txt"),CloseFile);
// 通过 shard_ptr 访问它所管理的日志文件以记录日志
(*spLogFile)<<"main()函数被调用"<<endl;
// 将 shared_ptr 传递给 foo()函数供其记录日志
foo(spLogFile);
return 0; // 到这里,所有指向日志文件的 shared_ptr 都结束其生命周期,
// 日志文件的引用计数变为 0, CloseFile()函数被调用以完成释放工作
}
在这段代码中,我们在创建 shared_ptr 对 ofstream 对象进行管理的同时,提供一个 CloseFile()函数作为其删除器。当 ofstream 对象的引用计数变为 0 时, shared_ptr 就会以指向这个 ofstream 对象的指针为参数调用这个函数来完成资源的释放工作。这里对 ofstream 对象的释放,不仅仅是用delete 操作符释放内存资源,在这之前,我们还向日志文件中输出了表示日志文件结束的信息,然后使用它的 close()成员函数关闭了日志文件,最后才是用 delete 操作符释放内存。