C++智能指针

野指针与悬空指针

C++11智能指针

为什么要引入智能指针


普通指针的问题

普通指针总是牵扯到许多问题,例如指针所指对象的生命周期,悬空指针和内存泄露等;

悬空指针:悬空指针:指针最初指向的内存已经被释放的一种指针:

#include <stdlib.h>
#include <stdio.h>
int main()
{
	int *p1 = (int*)malloc(sizeof(int));
	int *p2 = p1;
	*p1 = 1;
	
	printf("p2 = %p\n",p2);	
	
	//释放内存空间
	free(p1);p1 = NULL;
	
	//p2指向的地址不变,p2成为了悬空指针,*p2却变成了垃圾值
	printf("p2 = %p\n",p2);	
	printf("*p2 = %d\n",*p2);
	return 0;
}

在这里插入图片描述
内存泄漏:从堆中申请了内存却不释放回去,就会发生内存泄露。例如在new和delete之间不小心加了一个提早return的语句,或者是有些错误的语句使得程序抛出异常(例如除以0,访问无效的地址等),那么内存就泄露了。
1)下面一段代码演示了函数提前返回造成的内存泄漏:

void Foo( )
{ 
    int* iPtr = new int;  
    ...
    if(...)
    	return;
    ...
    delete iPtr;
 }

2)下面一段代码演示了程序抛出异常而导致的内存泄漏:

// auto_ptr/normal_ptr_2.cpp
#include <iostream>
#include <memory>
using namespace std;

class Test
{
public:
	Test(int a = 0 ) : m_a(a){}
	~Test( ){cout<<"~Test()"<<endl;}
	int m_a;
};
//除法函数
double Fun(double a,double b )
{
	if( b == 0 )
	{
		throw "Invalid divisor";
	}
	return a/b;
}

int main( )
{
	try
	{
		Test* p = new Test(5); 
		Fun(1,0);
		//离开前不会调用~Test(),内存泄露
	}
	catch(...)
	{
		cout<<"Something has gone wrong"<<endl;
	}
}

在这里插入图片描述

auto_ptr


智能指针是一个RAII机制类模型,用来动态的分配内存,它提供所有普通指针的接口,却很少发生异常。它会自动释放已经分配的内存,避免内存泄露,还可以避免悬空指针的问题。智能指针使程序员能从手动管理动态内存的繁杂任务中解放出来。C++98引入了第一个智能指针auto_ptr。

auto_ptr解决的问题

避免内存泄露
1)利用智能指针从堆上申请空间,离开作用域时会自动释放,可以避免因为忘记释放而导致的内存泄露:

// auto_ptr/auto_ptr_1.cpp
//class Test的定义
...
int main()
{
	auto_ptr<Test> p(new Test(1));
	cout << p->m_a << endl;
	return 0;
	//函数离开作用域时自动释放智能指针p
}

2)下面例子中,尽管异常被抛出,但是指针仍然正确地被释放了:

// auto_ptr/auto_ptr_2.cpp
//class Test的定义
...
//除法函数
double Fun(double a,double b )
{
	if( b == 0 )
	{
		throw "Invalid divisor";
	}
	return a/b;
}

int main( )
{
	try
	{
		std::auto_ptr<Test> p( new Test(5) ); 
		Fun(1,0);
		//离开前会调用~Test()
	}
	catch(...)
	{
		cout<<"Something has gone wrong"<<endl;
	}
}

在这里插入图片描述

auto_ptr存在的问题

1.所有权转移问题:当把一个auto_ptr赋值给另外一个auto_ptr时,它的所有权也转移了,这种场景在函数传参时经常出现,导致野指针的出现:

// auto_ptr/auto_ptr_3.cpp
// auto_ptr所有权转移问题

//class Test的定义
...
//auto_ptr用作形参
void Fun(auto_ptr<Test> p1)
{
	cout << p1->m_a << endl;
}

int main( )
{
	auto_ptr<Test> p (new Test(1));
	Fun(p);
	//p成为野指针了,这里很容易出错
	cout << p->m_a << endl;	//编译能通过,但是运行时出现段错误
}

2.析构数组问题:auto_ptr不能指向一组对象,因为auto_ptr只能调用delete来析构,因此下面的代码会c产生内存泄漏:

void Fun()
{
	auto_ptr<Test> p (new Test[5]);
	//离开p的作用域时,会调用delete进行析构,也就是只会析构一次
}

3. auto_ptr不能和标准容器放到一起使用;
4. 任一时刻只能有一个auto_ptr指针拥有所有权。

shared_ptr


C++11提供了一组新的智能指针,废弃了auto_ptr指针,这组新的智能指针包括:shared_ptr、unique_ptr和shared_ptr。下面先介绍shared_ptr:

shared_ptr介绍

特点
shared_ptr的设计目的在于:多个shared_ptr指针指向同一个对象,当最后一个shared_ptr离开作用域时,内存才被释放,这样就解决了悬空指针的问题。

创建

shared _ptr<int> sptr1(new int);

//使用make_shared宏来加速创建
shared_ptr<int> sptr2 = make_shared<int>(100);

引用计数use_count()

// shared_ptr/shared_ptr_use_count.cpp
#include <iostream>
#include <memory>
using namespace std;

int main()
{
	shared_ptr<int> sp1(new int);
	shared_ptr<int> sp2(sp1);
	shared_ptr<int> sp3;
	sp3 = sp1;
	
	cout << sp1.use_count() << endl;		//3
	cout << sp2.use_count() << endl;		//3
	cout << sp3.use_count() << endl;		//3
}

管理对象数组
shared_ptr默认调用delete释放关联的资源。如果用户采用一个不一样的析构策略时,他可以自由指定构造这个shared_ptr的策略。用户可以构造自己的析构函数(例如一个lamba表达式),实现对象数组的析构(delete []):

shared_ptr<Test> sp1(new Test[5], [ ](Test* p){delete[] p});

其他一些接口
除了能像普通指针一样使用shared_ptr,还提供了下面的一些接口:

  • get():获取shared_ptr所绑定的资源;
  • reset():释放关联内存的所有权,如果是最后一个拥有该资源的shared_ptr,就释放这块内存;
  • unique():判断是否是唯一指向当前内存的shared_ptr。

shared_ptr存在的问题

1 不要用一个普通指针创建shared_ptr

// /shared_ptr/shared_ptr_normal_new.cpp
int main()
{
	Test * p = new Test;
	
	shared_ptr<Test> sp1(p);		//use_count() = 1
	shared_ptr<Test> sp2(p);		//use_count() = 1
	
	//假设sp1先离开作用域,那么sp1会调用析构函数并释放内存,此时sp2.use_count还是1
	//sp2离开作用域时,也尝试调用析构函数并释放内存,产生段错误
}

2 循环引用问题
下面例子中:会产生循环引用,原因见代码注释:

// /shared_ptr/circle_refer.cpp
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
	A():m_spB(NULL)	 {};
	~A()  {cout << "~A()" << endl;}
	shared_ptr<B> m_spB;
};

class B
{
public:
	B():m_spA(NULL)  {}
	~B() {cout << "~B()" << endl;}
	shared_ptr<A> m_spA;
};

int main()
{
	shared_ptr<A> spA( new A);
	shared_ptr<B> spB( new B);		
	spA->m_spB = spB;			//spB.use_count = 2
	spB->m_spA = spA;			//spA.use_count = 2
	return 0;
	//注意,只有当spA指向的对象被析构之后,spA->m_spB才“离开作用域”,其对应的引用计数才会减一
	//同理,只有当spB指向的对象被析构之后,spB->m_spA才“离开作用域”,其对应的引用计数才会减一
	
	//spA离开作用域时,spA.use_count()减为1,因此,不会调用析构函数
	//同理,spB离开作用域时,spB.use_count()减为1,因此,也不会调用析构函数
	
	//这样一来就形成了引用循环,离开作用域时不会调用任何析构函数,内存泄露
}

weak_ptr


针对上面shared_ptr的引用循环问题,引入weak_ptr;

介绍
weak_ptr拥有共享语义和不包含语义weak_ptr可以共享shared_ptr持有的资源,但是却不能拥有shared_ptr的资源(也就是不会增加use_count);

强引用与弱引用
前面我们所说的use_count其实就是强引用的计数,当一个weak_ptr共享shared_ptr的资源时,会增加它的弱引用计数,而弱引用计数对析构没有影响。

// /weak_ptr/use_weak_ptr.cpp
...
//class Test的定义

int main()
{
	shared_ptr<Test> sp(new Test);		//强引用计数=1,弱引用计数=0
	weak_ptr<Test> wp1(sp);				//强引用计数=1,弱引用计数=1
	weak_ptr<Test> wp2 = sp;			//强引用计数=1,弱引用计数=2
}

判断weak_ptr的有效性
如果shared_ptr的use_count减为0,那么指向它的weak_ptr会过期(expired),因此每次使用weak_ptr前必须要判断其有效性,有两种方法判断weak_ptr是否指向有效的资源:

  • 调用use-count()去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。
  • 调用expired()方法。比调用use_count()方法速度更快。

从weak_ptr转化到shared_ptr
默认情况下,weak_ptr不能访问所指向的资源,因此需要临时转化成shared_ptr来进行访问,方法是lock()

// /weak_ptr/use_weak_ptr.cpp
...
//class Test的定义

int main()
{
	shared_ptr<Test> sp(new Test(1));
	weak_ptr<Test> wp(sp);
	
	if(!wp.expired())
	{
		//错误,需要lock()才能访问
		//cout << wp->m_a << endl;
		
		//lock()再访问才合法
		cout << wp.lock()->m_a << endl;
		//使用完毕后use_count自动减一
		cout << sp.use_count() << endl;		//输出为1
	}
}

weak_ptr解决循环引用问题

// /weak_ptr/non_circle_refer.cpp
//解决shared_ptr存在的引用循环问题

class B;
class A
{
public:
	A() : m_a(5)  { };
	~A()    {cout<<" A is destroyed"<<endl;}
	void PrintSpB( );
	weak_ptr<B> m_sptrB;
	int m_a;
};

class B
{
public:
	B() : m_b(10) { };
	~B(){cout<<" B is destroyed"<<endl;}
	weak_ptr<A> m_sptrA;
	int m_b;
};

void A::PrintSpB( )
{
	if( !m_sptrB.expired() )
	{  
		cout<< m_sptrB.lock( )->m_b<<endl;
	}
}

int main( )
{
	shared_ptr<B> sptrB( new B );
	shared_ptr<A> sptrA( new A );
	sptrB->m_sptrA = sptrA;
	sptrA->m_sptrB = sptrB;
	sptrA->PrintSpB(); 
	//能够正常析构
}

unique_ptr


unique_ptr是对auto_ptr的替换。unique_ptr遵循着独占语义。在任何时间点,资源只能唯一地被一个unique_ptr占有。当unique_ptr离开作用域,所包含的资源被释放。

创建
基本的创建方法与shared_ptr一样:

unique_ptr<int> up(new int);

创建对象数组时,unique_ptr提供了创建对象数组的特殊方法,会调用delete[]进行析构:

unique_ptr<int []> up(new int[5]);

所有权转移
auto_ptr允许赋值语句,容易产生野指针,unique不允许赋值语义,只支持移动语义
1)reset:用于重置所有权:

void reset (nullptr_t p) noexcept;
// /unique_ptr/reset.cpp
#include <iostream>
#include <memory>
using namespace std;

class Test
{
public:
	Test(int a = 0 ) : m_a(a){}
	~Test( ){cout << m_a << "  ~Test()"<<endl;}
	int m_a;
};

int main()
{
	unique_ptr<Test> up(new Test(5));
	up.reset();					//5  ~Test()
	
	unique_ptr<Test> up1(new Test(10));
	unique_ptr<Test> up2;
	
	//以下操作不被允许,如果所有权需要转移,请使用swap
	//up1.reset(up2);
	
	//up1原本的内存被释放,并且它会取得new Test(20)的所有权
	up1.reset(new Test(20));	//10  ~Test()
	cout << up1->m_a << endl;	//20
	
	return 0;
	//20  ~Test()
}

2)swap:用于交换所有权:

void swap (unique_ptr& x) noexcept;
// /unique_ptr/swap.cpp
#include <iostream>
#include <memory>

int main () {
	std::unique_ptr<int> foo (new int(10));
	std::unique_ptr<int> bar (new int(20));

	foo.swap(bar);

	std::cout << "foo: " << *foo << '\n';	//20
	std::cout << "bar: " << *bar << '\n';	//10
}

3)release:与reset不同,release仅仅释放所有权但不释放资源,因此为了防止内存泄漏,往往需要其他指针来负责将资源释放,也就是“接管”资源,具体做法是通过返回值来“接管”:

pointer release() noexcept;
// /unique_ptr/release.cpp
#include <iostream>
#include <memory>
using namespace std;
int main () 
{
	unique_ptr<int> up1(new int);
	int * take_over_ptr;			//接管指针可以是普通指针
	
	*up1 = 10;
	
	//release返回其指向的资源,take_over_ptr会接管
	take_over_ptr = up1.release();
	cout << *take_over_ptr << endl;	 //10
	
	if(up1==NULL)		//条件成立,up1调用release后就是NULL了
		cout << "up1 is NULL now" << endl;
	
	//接管之后要记得负责释放
	delete take_over_ptr;
}
// /unique_ptr/release2.cpp
//与release.cpp基本上一样,不过这里讨论接管指针是unique_ptr时该如何接管
#include <iostream>
#include <memory>
using namespace std;
int main () 
{
	unique_ptr<int> up1(new int);
	unique_ptr<int> take_over_ptr;	//接管指针是unique_ptr
	
	*up1 = 10;
	
	//接管失败,原因是unique_ptr不支持赋值语句
	//take_over_ptr = up1.release();
	
	//因此,需要使用reset进行接管
	take_over_ptr.reset(up1.release());
	cout << *take_over_ptr << endl;	 //10
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

真的胜哥

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值