c++ 智能指针shared_ptr详解

参考资料:: C++primer

为什么需要智能指针

在c++中,动态内存的管理的管理是通过一对运算符来完成的:new在内存中为对象分配空间并返回一个指向该对象的指针,delete接受一个动态对象的指针,销毁该对象,并且释放与之关联的内存。
来看一个例子:

int *process(int *a){
	int *b =new int(200);

}

在函数 process中我们申请了一块内存,但是在函数体内b 是一个临时变量,离开函数会销毁,但是我们动态申请的内存不会消失,这样就会造成内存泄漏;有时在尚有指针引用内存的情况下就释放了它,在这种情况下就会产生引用非法内存的指针。为了更容易(同时也更安全)地使用动态内存,C++11标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指的对象。C++11标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则"独占"所指向的对象。C++11标准库还定义了一个名为weak_ptr的辅助类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。智能指针是模板类而不是指针。
智能指针实质就是重载了->和*操作符的类,由类来实现对内存的管理,确保即使有异常产生,也可以通过智能指针类的析构函数完成内存的释放。

shared_ptr

智能指针也是模板类,因此在创建一个智能指针的时,必须提供指针所指向的类型例如:

shared_ptr<string> p1  //  shared_ptr p1指向string
shared_ptr<lsit<int>> p2 // shared_ptr p2 指向int的list;

默认初始化的智能指针中保存着一个空指针。智能指针的使用方式和普通指针类似,解引用一个智能指针返回它指向的对象,在一个条件判断中使用智能指针就是检测它是不是空。

if(p1 && p1->empty())
	*p1='hi';//如果p1指向一个空的string,解引用p1,讲一个新的值赋与string

shared_ptr操作如下:

shared_ptr< T >sp空智能指针
p将p用做一个条件判断,若p指向一个对象,为true
*p解引用p,获得他指向的对象
p->mem等价于(*p).mem
p.get()返回p中保存的指针,使用时要小心,若智能指针释放了其对象,返回的指针所指向的对象
swap(p,q)交换p和q的指针
p.swap(q)交换p和q的指针
make_shared< T > (args)返回一个shared_ptr,指向一个动态分配的类型为T的对象使用args初始化此对象
shared< T > p(q)p是shared_ptr q的拷贝,此操作会递增q中的计数器。
p=qp和q都是shared_ptr,保存的指针必须能相互转换,这个操作会递减p的引用次数,递增q的引用次数
p.unique()若p.use_count()为1,返回true,否则返回false
p.use_count()返回与p共享对象的智能指针数量

make_shared< T > (args)

最安全的分配和使用动态内存的方法就是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。头文件和share_ptr相同,在memory中.

shared_ptr<int> p3 = make_shared<int>(42);//指向一个值为42的int类型的shared_ptr
shared_ptr<string> p4 = make_shared<string>(10,'9');//指向值为"9999999999"的string类型
shared_ptr<int> p5 = make_shared<int>();//空的

拷贝和赋值

当进行拷贝和赋值的时候,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象
例如:

int main()
{
	// 使用 make_shared 创建空对象
	std::shared_ptr<int> p1 = std::make_shared<int>();
	*p1 = 100;
	std::cout << "p1 = " << *p1 << std::endl; // 输出100
	std::cout << "p1 Reference count = " << p1.use_count() << std::endl;//输出1
	std::shared_ptr<int> p2(p1);
	std::cout << "p2 Reference count = " << p2.use_count() << std::endl;//输出2
	std::cout << "p1 Reference count = " << p1.use_count() << std::endl;//输出2

在这里插入图片描述

当我们给shared_ptr赋一个新值得或者shared_ptr被销毁的时候,计数器会减一,一旦一个shared_ptr的计数器为0,会自动释放自己管理的对象;

auto r = make_shared<int>(42);//r指向的int只有一个引用者
r = p1;//赋值给r,r指向另一个地址,此时p1的计数加一,r的引用次数和递增后p1的一样
#include<iostream>
#include<memory>
int main(){
	// 使用 make_shared 创建空对象
	std::shared_ptr<int> p1 = std::make_shared<int>(100);
	std::cout << "p1 = " << *p1 << std::endl; // 输出100
	std::cout << "p1 Reference count = " << p1.use_count() << std::endl;//输出1
	std::shared_ptr<int> p2(p1);//p1给p2拷贝,此时p1的use_cout+1,p2和p1相等
	std::cout << "p2 Reference count = " << p2.use_count() << std::endl;//输出2
	std::cout << "p1 Reference count = " << p1.use_count() << std::endl;//输出2
	auto r1 = std::make_shared<int>(33);//r1指向新的地址
	std::shared_ptr<int> r2(r1);//和上边一样
	std::cout<<"*********************************************"<<std::endl;
	std::cout << "p2 Reference count = " << p2.use_count() << std::endl;//输出2
	std::cout << "p1 Reference count = " << p1.use_count() << std::endl;//输出2
	std::cout << "r1 Reference count = " << r1.use_count() << std::endl;//输出2
	std::cout << "r2 Reference count = " << r2.use_count() << std::endl;//输出2
	std::cout<<"*********************************************"<<std::endl;
	r1 = p1;//赋值操作,此时p1+1,r1和p1相等,同时r1原先的指向递交控制权,r2的use_count()变为1;
	if(r1==p1){
		std::cout<<"same"<<std::endl;
	}
	std::cout << "p2 Reference count = " << p2.use_count() << std::endl;//输出3
	std::cout << "p1 Reference count = " << p1.use_count() << std::endl;//输出3
	std::cout << "r1 Reference count = " << r1.use_count() << std::endl;//输出3
	std::cout << "r2 Reference count = " << r2.use_count() << std::endl;//输出1
		
}

在这里插入图片描述

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象,它是通过另一个特殊的成员函数-析构函数完成销毁工作的,类似于构造函数,每个类都有一个析构函数。析构函数控制对象销毁时做什么操作。析构函数一般用来释放对象所分配的资源。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。
shared_ptr还会自动释放相关联的内存

void use_factory(T args){
	shared_ptr<Foo> p = factory(arg);
}// p离开作用域,它指向的内存会被自动释放掉
#include<iostream> 
#include<memory>
using namespace std;
shared_ptr<int> process(int a){
	shared_ptr<int> p1 = make_shared<int>(a);
	return p1;
}
int main(){
	shared_ptr<int> p1;
	cout << p1.use_count()<<endl;//输出0
	p1 = process(2);
	cout << p1.use_count()<<endl;//输出1
}

上面代码中return语句向此函数的调用者返回一个p的拷贝,拷贝一个shared_ptr会增加计数值,现在p被销毁,他所指向的内存还有其他使用者,只要保证还有使用就不会被释放。当动态对象不再被使用时,shared_ptr类还会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

分离关联的原始指针reset()

要使 shared_ptr 对象取消与相关指针的关联,可以使用reset()函数:

p1.reset()

这种做法会让p1变为空,并且与p1相关联的计数减1;

#include<iostream>
#include<memory>
int main(){
std::shared_ptr<int> p1 = std::make_shared<int>(100);
	std::shared_ptr<int> p2(p1);
	std::cout << "p2 计数 = " << p2.use_count() << std::endl;//输出2 
	std::cout << "p1 计数 = " << p1.use_count() << std::endl;//输出2 
	std::cout<<"Reset "<<std::endl;
	p1.reset();
	std::cout << "p1 计数 = " << p1.use_count() << std::endl;//输出0 
	std::cout << "p2 计数 = " << p2.use_count() << std::endl;//输出1		
}
p1.reset(new int(200))

这种做法会将p1指向新的地址,计数为1;

#include<iostream>
#include<memory>
int main(){
std::shared_ptr<int> p1 = std::make_shared<int>(100);
	std::shared_ptr<int> p2(p1);
	std::cout << "p2 计数 = " << p2.use_count() <<";p2的值为" << *p2 <<std::endl;//输出p2 计数 = 2;p2的值为100
	std::cout << "p1 计数 = " << p1.use_count() << ";p1的值为" << *p1 <<std::endl;//输出p1 计数 = 2;p1的值为100 
	std::cout<<"Reset "<<std::endl;
	p1.reset(new int(200));
	std::cout << "p1 计数 = " << p1.use_count() <<";p1的值为" << *p1 <<std::endl ;//p1 计数 = 1;p1的值为200
	std::cout << "p2 计数 = " << p2.use_count() <<";p2的值为" << *p2 <<std::endl ;//p2 计数 = 1;p2的值为100 
}

shared_ptr与new结合使用

shared_ptr<double>p1//p1指向一个double
shared_ptr<int>p2(new int(42))//p2指向一个值为42的int

接受指针参数的智能指针构造函数是explicit,因此必须使用直接初始化的方式来初始化一个智能指针:

shared_ptr<double>p1 = new double(12.3)//错误的做法p1指向一个double
shared_ptr<int>p2(new int(42))//正确 直接初始化的方式p2指向一个值为42的int

处于同样的原因,返回shared_ptr函数不能在其返回中隐式的转换。
例如下面的做法是不行的:

shared_ptr<int> process(int p){
	return new int(p);//这样做不行必须 return shared_ptr<int>(new int(p));
	
}

不要混合使用普通指针和智能指针

shared_ptr可以协调对象的析构,但是仅限于自身的拷贝(shared_ptr之间),这就是为什么推荐使用make_shared ,这样吧我们就能在分配对象的同时将shared_ptr与之绑定,从而避免无意中将同一块内存绑定到多个独立创建的shared_ptr 例如下面我们就将同一块内存绑定在不同的shared_ptr上

#include<iostream>
#include<memory>
int main(){
	int *a = new int(100);
	std::shared_ptr<int> p1(a);
	std::shared_ptr<int> p2(a);
	std::cout << "p2 计数 = " << p2.use_count() <<";p2的值为" << *p2 <<std::endl;//输出p2 计数 = 1;p2的值为100
	std::cout << "p1 计数 = " << p1.use_count() << ";p1的值为" << *p1 <<std::endl;//输出p1 计数 = 1;p1的值为100 		
}

再例如:

#include<iostream>
#include<memory>
using namespace std;
void process(shared_ptr<int> ptr){
	*ptr = 100;
}
int main(){
	shared_ptr<int> p1(new int(2));
	process(p1);
	int i = *p1;
	cout<<"i的值为"<< i<< endl; 
		
}

上面函数是按照值传递的,因此实参会被拷贝到ptr中,拷贝会增加计数,在运行的过程中计数的值为2,但process运行结束后,计数减一,变为原来的1,因此内存不会被销毁。

#include<iostream>
#include<memory>
using namespace std;
void process(shared_ptr<int> ptr){
	*ptr = 100;
}
int main(){
	int *p1 = new int(1); 
	process(p1)//错误,只能显示的将p1转为shared_ptr 
	process(shared_ptr<int>(p1));
	int i = *p1;
	cout<<"i的值为"<< i<< endl; 
	
}

上面的代码在运行的过程中,我们将一个临时的shared_ptr传递给process,当函数结束时,临时对象会被销毁,同时引用计数会减一,此时引用计数为0,内存会被释放,指针p1就会变为一个空悬指针,会出现一个意向不到的数。我用的devC++,结果是:i的值为12149808。

不要使用get初始化另一个智能指针或为他赋值

智能指针类型定义了一个名为get的函数,返回一个内置的指针指向智能指针管理的对象,这个函数适用于下面这种情况:我们需要向不能使用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete这个指针。

shared_ptr<int>p(new int(42));//引用计数为1
int *q = p.get()//使用q时要记住不能让他管理的指针释放掉
{
	shared_ptr<int>(q)//离开这个作用域,q指向的内存会被销毁
}
int i = *p//p指向的内存已经释放

上面的例子,p和q指向同一块内存,由于他们时相互独立创建的,因此各自的计数为1,当q的程序块结束时,q会被销毁,这会导致q指向的内存会被释放,因此p变成一个空悬指针。当我们使用p时会出现未定义的操作,同时,当p被销毁时这块内存会被第二次delete。所以当我们使用get的时候,我们必须保证不会delete掉get得到的指针。特别是不能用get初始化或者赋值。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值