C++高阶-智能指针shared_ptr

智能指针

shared_ptr 能够知道它所指向的内存资源还有没有人在使用?从而可以在没人使用的情况下自动释放这块内存资源。

引用计数

shared_ptr 对它所指向的内存资源的管理使用引用计数

  • 当新增一个 shared_ptr 对该资源进行管理时,也就是新增一个指向此资源的 shared_ptr 时,它就会将该内存资源的引用计数加 1;
  • 反之,当减少一个 shared_ptr 对该资源进行管理时,就会将该内存资源的引用计数减 1;
  • 如果该内存资源的引用计数变为 0,则说明没有任何指针对其进行管理,就自动调用 delete 操作符释放这块内存资源。
// 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 的时候,聪明地自动释放掉这块内存资源。

用 make_shared()函数简化 shared_ptr 的创建

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 来管理内存资源:

  • 有多个使用者共同使用同一个对象,而这个对象没有一个明确的拥有者
  • 某一个对象的复制操作很费时;如果一个对象的复制操作很费时,同时我们又需要在函数间传递这个对象,我们往往会选择传递指向这个对象的指针来代替传递对象本身,以此来避免对象的复制操作。既然选择使用指针,那么使用 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 对象所指向的对象拥有成员函数,我们还可以把“->”符号放在 shared_ptr 对象的后面,直接调用它所指向对象的成员函数。
  • 两者唯一的不同是,我们不能对 shared_ptr 对象进行加减运算而使它的指向发生偏移;
  • 也不能多此一举地使用 delete 操作符显式地释放它所指向的对象。
// 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);

shared_ptr 的成员函数

reset()函数

reset()函数可以用于重新( re)设置( set)一个 shared_ptr 对象所管理的内存资源。

  • 如果我们不用任何实际参数而直接调用 reset() 函数,shared_ptr 将停止对原有内存资源的管理,这块内存资源的引用计数减 1。
  • 如果用另外一个普通指针或者 shared_ptr 对象作为实际参数调用 reset()函数, shared_ptr 在停止对原有内存资源的管理的同时,又会重新开始对新的内存资源的管理。
  • 类似的操作也可以通过 shared_ptr 的“=”赋值操作符来完成。
// 停止 spEmp 对 Staff 对象的管理
spEmp.reset();
// 将 spEmp 指向一个新创建的 Officer 对象
spEmp.reset(new Officer("Muench",1986));
// 此时, spBoss 也同样停止了对原有 Staff 对象的管理而重新指向这个 Officer 对象
spBoss = spEmp;

get()函数

虽然可以像普通指针一样地直接使用 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()函数

use_count()函数返回 shared_ptr 的引用计数。它在调试的时候特别有用,因为它可以在程序执行的关键点获得某块内存资源的引用计数,这可以为调试提供非常有用的信息。

swap()函数

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 却导致了新的内存泄漏问题。

shared_ptr 的一个变种——weak_ptr

weak_ptr 表示对内存资源的一种弱( weak)引用,当某个 weak_ptr 指向某个内存资源时,并不会改变其引用计数。 weak_ptr 只是这个内存资源的一个旁观者,它拥有内存资源的访问权,但没有所有权。通过各自指向对方的 weak_ptr,它们都可以访问对方,但无法控制对方的生死。

  • 待补充????

对 shared_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 操作符释放内存。

你可能感兴趣的:(C++基础,c++)