智能指针 的理解

智能指针主要解决的问题

1、内存泄露:内存手动释放,使用智能指针可以自动释放malloc free ;new delete 
2、共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题

C++里面的四种智能指针:auto_ptr,shared_ptr,unique_ptr,weak_ptr其中后三个是C++支持,并且第一个已经被弃用

  • unique_ptr 独占对象的所有权,由于没有引用计数,因此性能比较好
  • shared_ptr 共享对象的所有权,但性能比较差
  • weak_ptr 配合shared_ptr ,解决循环引用问题。

shared_ptr内存模型

shared_ptr内存模型
shared_ptr 内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个引用计数(reference count),一个弱计数(weak count)和其它一些数据。
在这里插入图片描述

智能指针使用场景案例

使用智能指针可以自动释放占用的空间

shared_ptr<Buffer> buf = make_shared<Buffer>("auto free memory"); // buf 对象分配在堆上,但能自动释放
//	对比
Buffer *buf = new Buffer("auto free memory");//buf对象分配在堆上,但需要手动delete释放

共享所有权指针的传播和释放

在这里插入图片描述

shared_ptr 共享的智能指针

std::shared_ptr 使用引用计数器,每一个shared_ptr的拷贝都指向相同的内存。再最后一个shared_ptr析构的时候,内存才会被释放。
shared_ptr 共享被管理对象,同一时刻可以有多个shared_ptr 拥有对象的所有权,当最后一个shared_ptr对象销毁时,被管理对象自动销毁。

简单说,shared_ptr 实现包含了两部分,

  • 一个指向堆上创建的对象的裸指针,raw_ptr
  • 一个指向内部隐藏的、共享的管理对象。share_count_obj

shared_ptr 的基本用法和常用函数

  • s.get():返回shared_ptr中保存的裸指针;
  • s.reset():重置shared_ptr;
  • reset() :不带参数时,若智能指针s是唯一指向该对象的指针,则释放,并置空。若智能指针P不是唯一指向该对象的指针,则引用计数减少1,同时将P置空。
  • reset():带参数时,若智能指针s是唯一指向对象的指针,则释放并指向新的对象。若P不是唯一的指针,则只减少引用计数,并指向新的对象。
auto s = make_shared<int>(100);
s.reset(new int(200));

s.use_count():返回shared_ptr的强引用计数;
s.unique():若use_count为1,返回true,否则返回false。

初始化make_shared/reset

通过构造函数、std::shared_ptr辅助函数和reset方法来初始化shared_ptr,代码如下:

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
if(p3) {
	cout << "p3 is not null";
}

用make_shared来构建智能指针,因为make_shared比较高效。

auto sp1 = make_shared<int>(100);
//或
shared_ptr<int> sp1 = make_shared<int>(100);
//相当于
shared_ptr<int> sp1(new int(100));

不能将一个原始指针直接赋值给一个智能指针,例如:

std::shared_ptr<int> p = new int(1);

shared_ptr 不能通过“直接将原始这种赋值”来初始化,需要通过构造函数和辅助方法来初始化

  • 对于一个未初始化的智能指针,可以通过reset方法来初始化;
  • 当智能指针有值的时候调用reset会引起引用计数减1

另外智能指针可以通过重载的bool类型操作符来判断。

int main {
	std::shared_ptr<int> p1;
	p1.reset(new int(1));
	std::shared_ptr<int> p2 = p1;
	// p2.use_count() = 2
	cout << "p2.use_count() = " << p2.use_count() << endl;
	p1.reset();
	cout << "p1.reset()\n";
	// p2.use_count() = 1
	cout << "p2.use_count()= " << p2.use_count() << endl;
	if (!p1) {
		cout << "p1 is empty\n"; // 执行了
	}
	if (!p2) {
		cout << "p2 is empty\n";
	}
	p2.reset();
	cout << "p2.reset()\n";
	// p2.use_count() = 0
	cout << "p2.use_count()= " << p2.use_count() << endl;
	if (!p2) {
		cout << "p2 is empty\n"; // 执行了
	}
}

获取原始指针get

当需要获取原始指针时,可以通过get方法来返回原始指针:

std::shared_ptr<int> ptr(new int(1));
int *p = ptr.get(); // 获取

尽量不要使用p.get()的返回值,如果不知道其危险性则永远不要调用get() 函数。

指定删除器

如果用shared_ptr 管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。

#include <iostream>
#include <memory>
using namespace std;
void DeleteIntPtr(int *p) {
	cout <<"call DeleteIntPtr" << endl;
	delete p;
}
int main() {
	std::shared_ptr<int> p(new int(1),DeleteIntPtr);
	return 0;
}

当p的引用计数为0时,自动调用删除器DeleteIntPtr来释放对象的内存。删除器可以是一个lambda表达式,如:

std::shared_ptr<int> p(new int(1),[](int *p) {
	cout << "call lambda delete p " << endl;
	delete p;
})

当用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象,代码如:

std::shared_ptr<int> p3(new int[10],[](int *p) {delete [] p;})

使用shared_ptr 要注意的问题

不要用一个原始指针初始化多个shared_ptr

int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int>	p2(ptr); // 逻辑错误

不要在函数实参中创建shared_ptr

func(shared_ptr<int>(new int),g()); // 有缺陷

因为c++的函数参数的计算顺序在不同的编译器不同的约定下可能不一样的。 一般是从右到左,但也可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建,则int内存就泄露了, 正确写法如下:

shared_ptr<int> p(new int);
func(p,g());

通过shared_from_this() 返回this指针

不要将this指针作为shared_ptr 返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构,如:

#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
shared_ptr<A> GetSelf()
{
return shared_ptr<A>(this); // 不要这么做
}
~A()
{
cout << "Destructor A" << endl;
}
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf();
return 0;
}

运行后调用了两次析构函数。
在这里插入图片描述
在例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。
正确返回this和shared_ptr的做法是: 让目标类通过std::enable_shared_from_this类,然后使用基类的成员函数 shared_from_this()来返回this的shared_ptr,如下:

#include <iostream>
#include <memory>
using namespace std;
class A: public std::enable_shared_from_this<A>
{
public:
shared_ptr<A>GetSelf()
{
return shared_from_this(); //
}
~A()
{
cout << "Destructor A" << endl;
}
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf(); // ok
return 0;
}

在weak_ptr章节我们继续讲解使用shared_from_this() 的原因。

避免循环引用

循环引用会导致内存泄露,如:

#include <iostream>
#include <memory>

using namespace std;

class A;
class B;
class A {
public:
	std::shared_ptr<B> bptr;
	~A() {
		cout << "A is deleted" << endl;
	}
};
class B {
public:
	std::shared_ptr<A> aptr;
	~B() {
	cout << "B is deleted" << endl;
	}
};
int main() {
	{
		std::shared_ptr<A> ap(new A);
		std::shared_ptr<A> bp(new B);
		ap->bptr = bp;
		bp->aptr = ap;
	}
	cout << "main leave" << endl; // 循环引用导致ap bp 退出了作用域都没有析构
	return 0;
}

循环引用导致ap和bp的应用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不会减0,导致两个指针都不会被析构,产生内存泄露。
解决的办法是把A和B任何一个成员变量改为weak_ptr,具体方法见weak_ptr

unique_ptr 独占的智能指针

  1. unique_ptr 是一个独占型的智能指针,不能将其赋值给另一个unique_ptr
  2. unique_ptr 可以指向一个数组
  3. unique_ptr 需要确定删除器的类型

unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr,如:

unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制

unique_ptr不允许复制,但可以通过函数返回给其它unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了,如:

unique_ptr<T> my_ptr(new T) ; // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确
unique_ptr<T> ptr = my_ptr; //报错,不能复制

std::make_shared 是c++的一部分,但std::make_unique不是。

auto upw1(std::make_unique<widget>());
std::unique_ptr<widget> upw2(new widget);

除了unique_ptr 的独占性,unique_ptr和shared_ptr 还有一些区别,比如:

  • unique_ptr可以指向一个数组,代码如下:
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
  • unique_ptr 指定删除器和shared_ptr 有区别
std::shared_ptr<int> ptr3(new int(1),[](int *p){delete p;}) // 正确
std::unique_ptr<int> ptr4(new int(9),[](int *p) {delete p;})// 错误
  • unique_ptr需要确定删除器的类型,所以不能像shared_ptr 那样直接指定删除器,可以这样写:
std::unique_ptr<int,void(*)(int *)>ptr5(new int(1),[](int *p){delete p;}) // 正确

关于shared_ptr 和unique_ptr 的使用场景是要根据实际应用需求来选择。

如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

weak_ptr 弱引用的智能指针

weak_ptr 是一种 不控制对象生命周期的智能指针,它指向一个shared_ptr 管理的对象,进行该对象的内存管理的那个强引用的shared_ptr,weak_ptr只提供了对管理对象的一种访问手段。

weak_ptr 设计的目的是为配合shared_ptr 而引入的一种智能指针来协助shared_ptr 工作,它只可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。

weak_ptr 的基本用法

  1. 通过use_count() 方法获取当前观察资源的引用计数,如下:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl; // 结果输出1
  1. 通过expired() 方法判断所观察资源是否已经释放,如:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
if(wp.expired()) {
	cout << "weak_ptr 无效资源, 资源已经被释放";
}else {
	cout << "weak_ptr  有效";
}
  1. 通过lock方法获取监视的weak_ptr,如:
std::weak_ptr<int> gw1;
void f()
{
	auto spt = gw1.lock();
	if (gw1.expired()) {
		cout << "gw无效,资源已释放";
	}
	else {
		cout << "gw有效, *spt = " << *spt << endl;
	}
}

int main()
{
	{
		auto sp = std::make_shared<int>(42);
		gw1 = sp;
		f();
	}
	f();
	return 0;
}

结果输出:

gw有效, *spt = 42
gw无效,资源已释放

weak_ptr 返回this指针

shared_ptr章节中提到不能直接将this指针返回shared_ptr,需要通过派生
std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针,原因是
std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用
shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回,再看
前面的范例:

#include <iostream>
#include <memory>
using namespace std;
class A: public std::enable_shared_from_this<A>
{
public:
	shared_ptr<A>GetSelf()
	{
		return shared_from_this(); //
	}
	~A()
	{
		cout << "Destructor A" << endl;
	}
};
int main()
{
	shared_ptr<A> sp1(new A);
	shared_ptr<A> sp2 = sp1->GetSelf(); // ok
	return 0;
}


输出结果如下:
Destructor A

在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内
部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0,A对象会
被析构,不会出现A对象被析构两次的问题。

需要注意的是,获取自身智能指针的函数尽在shared_ptr的构造函数被调用之后才能使用,因为
enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。

weak_ptr 解决循环引用问题

在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过
weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr

#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A {
public:
	std::weak_ptr<B> bptr; // 修改为weak_ptr
	~A() {
		cout << "A is deleted" << endl;
	}
};
class B {
public:
	std::shared_ptr<A> aptr;
	~B() {
		cout << "B is deleted" << endl;
	}
};
int main()
{
	{
		std::shared_ptr<A> ap(new A);
		std::shared_ptr<B> bp(new B);
		ap->bptr = bp;
		bp->aptr = ap;
	}
	cout<< "main leave" << endl;
	return 0;
}

这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是weak_ptr,它并不会增加引用计数,所
以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,A指针会被析构,析构后其内
部的bptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。

weak_ptr 使用注意事件

weak_ptr 在使用前需要检查合法性。

weak_ptr<int> wp;
{
shared_ptr<int> sp(new int(1)); //sp.use_count()==1
wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0

因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了。
得到了一个容纳NULL指针的sp_null对象。在使用wp前需要调用wp.expired()函数判断一下。
因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后
一个weak_ptr对象被析构,这块“堆”存储块才能被回收。否则weak_ptr无法直到自己所容纳的那个指针
资源的当前状态。

如果shared_ptr sp_ok和weak_ptr wp;属于同一个作用域呢?如下所示:

	weak_ptr<int> wp;
	shared_ptr<int> sp_ok;
	{
		shared_ptr<int> sp(new int(1)); //sp.use_count()==1
		wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
		sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
	}
	if(wp.expired()) {
		cout << "shared_ptr is destroy" << endl;
	} else {
		cout << "shared_ptr no destroy" << endl;
	}

智能指针安全性问题

引用计数本身是安全的,至于智能指针是否安全需要结合实际使用分情况讨论:

情况1:多线程代码操作的是同一个shared_ptr的对象,此时是不安全的。
比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr

情况2:多线程代码操作的不是同一个shared_ptr的对象
这里指的是管理的数据是同一份,而shared_ptr不是同一个对象。比如多线程回调的lambda的是按值捕
获的对象。

void fn(shared_ptr<A>sp) {
...
}
..
std::thread td(fn, sp1);

这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的
对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。
也就是说,如下操作是安全的。

void fn(shared_ptr<A>sp) {
...
	if(..){
		sp = other_sp;
	} else {
		sp = other_sp2;
	}
}

需要注意:所管理数据的线程安全性问题。显而易见,所管理的对象必然不是线程安全的,必然 sp1、
sp2、sp3智能指针实际都是指向对象A, 三个线程同时操作对象A,那对象的数据安全必然是需要对象
A自己去保证。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值