剖析智能指针

这篇文章最主要的是 为了 日后供自己复习智能指针使用, 为了自己看着,复习方便。所以大家看来逻辑排版可能较乱,深感抱歉

首先我们来看一段代码:  


如:

int* p1 = new int(2);
bool isEnd = true;
......
if ( true == isEnd )
{
	return;
}
......
delete p1;


我们在堆上 new 出空间后, 本来应该在下面 delete 掉我们开辟的空间 ,但中间, 因为条件满足, 我们 return 了 , 没有 delete 掉该释放的空间, 此时就存在资源泄露的问题。

忘了回收空间---


这就引出了智能指针  -->  首先看它的简易概念  -->  智能指针(模板类)   将指针释放权交给智能指针来管理(管理指针指向对象的释放问题)(可以像指针一样用起来)  智能指针模仿原生指针


我们再看下  RAII:资源获得即初始化利用类的构造和析构函数   构造时初始化,析构时自动清理
RAII != 智能指针 。 智能指针只是RAII的一种应用,   RAII是解决一类问题



1. 我们来看第一个出现的智能指针 AutoPtr  并自己来模拟实现它, 更好的理解智能指针的作用及使用

template<class T>
class AutoPtr
{
public:
	AutoPtr( T* ptr )
		: _ptr( ptr )							//构造函数,我们用, 要管理的指针初始化我们的 类成员_ptr
	{}											//delete 与 free  -->  NULL 没问题

	AutoPtr( AutoPtr<T>& ap )					//拷贝构造 没返回值 别和 赋值运算符 重载 混淆了
	{
		//AutoPtr 的拷贝构造函数  方式是  -->  管理权转移
		//将 以前由 ap 智能指针对象管理的指针 交由 this 指针指向对象来管理(管理权转移), 这就导致了, 永远只有一个对象来管理它
		this->_ptr = ap._ptr;
		ap._ptr = NULL;

		//但这种设计是有问题的
		//虽然我们已经将 ap对象的管理权转移给当前对象,   但在类外,依然可以访问 qp对象, 访问ap时会出错(因为此时ap已经为NULL)。  外界ap还是可以访问的。    我们这样只是让ap类中的 _ptr 为NULL, 我们知道ap这个对象不能在管理指针了,但我们无法控制ap不被使用到。
	}

	AutoPtr<T>& operator=( AutoPtr<T>& ap )		//operator=( ) 与 拷贝构造Fun  参数都是 类类型(即 --> 模板参数)。  operator=( )返回值也是类类型。     并不是所有的operator=( )都要  "const xxxx&",根据具体的类而定, 别犯定视错误!
	{
		if ( this != &ap )						//AutoPtr 智能指针 的 赋值运算符重载, 也是采用管理权转移的方式
		{
			delete this->_ptr;

			this->_prt = ap._ptr;
			ap._ptr = NULL;
		}

		return *this;
	}

	~AutoPtr()
	{
		delete _ptr;
	}

	T& operator*( )
	//因为我们想像原生指针一样使用这个对象, 所以 operator*( )方法 应该返回 "值".
	//两种调用方法:ap1.operator*( /*&ap1*(隐含传递的参数 --> this 指针)/ )  or  *ap1
	//如果要修改它(指针解引用后值)  返回T 时  通过*_ptr拷贝构造出临时对象再返回。   
	//所以要修改要加引用  , 加引用出作用域后 _ptr 还存在(它是 new 出来的,所以还存在)
	{
		return *_ptr;
	}

	T* operator->( )
	{
		return _ptr;
	}

protected:
	T* _ptr;
};


我们来 使用下 AutoPtr:

void TestAutoPtr( )
{
	int* p1 = new int(10);

	//如果代码中间有 1.return(则需要在return之前进行释放p1)  或  2.抛异常 (则需catch 后释放p1 再抛出去) 
	//则需要执行括号里面的事, 但是 这样很容易遗忘释放这件事, 造成资源泄露,编写程序容易出错
	
	DoSomeThing( true );						//假如这里调用这个Fun( )

//void DoSomeThing( bool isThrow )
//{
//	if ( isThrow )
//	{
//		throw string( "发生了错误" );
//	}
//	else
//	{
//		cout << "正常运行" << endl;
//	}
//}

	delete p1;
}

//我们使用 AutoPtr 智能指针来解决这个问题:

void TestAutoPtr( )
{
	AutoPtr<int> ap1( new int(10) );			//我们使用 运算符重载 然后像使用原生指针一样 使用ap1

	//此时我们根本不用担心 new 出来的空间释放问题, 因为我们将这个 空间的释放权交给了 智能指针 AutoPtr 也就是说 ,当这个程序结束的时候, 
	//这个智能指针对象会销毁, 调用它的析构函数, 完成空间释放
}

我们再来使用下 AutoPtr 智能指针 管理 一个 指向类的指针时情况, 假设有如下类:

struct AA
{
	int a1;
	int a2;
};
AutoPtr<AA> ap2( new AA );
(*ap2).a1 = 34;
ap2->a1 = 35;									
//本该是两个->的.  ap2->( ap2.operator->( ) )先获得 指向AA类的指针, 再使用 -> 访问它的a1 成员
//但看着不舒服,所以编译器做了优化,一个->.  ap2->->a1   特殊处理,增强程序的可读性
上面是智能指针版本,接下来我们看看原生指针版本, 因为智能指针式模仿实现原生指针的功能, 大家会发现,两者特别相似:

AA* pa = new AA;
(*pa).a1 = 30;
pa->a1 = 25;

构造(初始化)和析构( 释放资源 ) ---> RAII
内存泄漏是  找不到内存了  所以指针丢了


总结:

AutoPtr:
1:管理指针指向对象的释放   利用 RAII  构造函数初始化 (保存指针),析构函数释放管理对象(出了对象作用域自动调用析构函数,所以 不管是 return  还是 抛出异常,  都会调用对象的析构函数)//1.构造(初始化)和析构( 释放资源 ) ---> RAII.
2:重载operator* 和 operator->   让我们像使用原生指针一样使用智能指针访问管理对象(只有管理的是自定义类型时,才用箭头)


AutoPtr<int> ap1( new int );  这时 用 *( operator*( ) )


AutoPtr<int> ap2(ap1);

拷贝构造,因为c++ 默认为浅拷贝。AutoPtr   解决浅拷贝方法 是 管理权的转移   存在严重缺陷,尽量不要使用AutoPtr.


因为AutoPtr存在缺陷我们就引出了二种改进版的智能指针,ScopedPtr



2.  ScopedPtr解决拷贝构造的方法简单粗暴它防拷贝(只声明,不定义)因为系统必须有拷贝构造函数和赋值运算符重载(如果没有,系统就生产默认的),所以我们必须实现这两个方法,但又不知道怎么写,所以我只声明不定义(这种方法也有缺陷, 如果我就是想拷贝, 这时候就坑了) 


ScopedPtr:

template<class T>
class Scoped
{
public:
	Scoped( T* ptr )
		:_ptr( ptr )
	{}

	~Scoped( )
	{
		delete _ptr;
	}

	T& operator*( )						
	{
		return *_ptr;
	}

	T* operator->( )
	{
		return _ptr;
	}

protected:
	ScopedPtr( ScopedPtr<T>& rhs );
	ScopedPtr<T> operator=( ScopedPtr<T>& rhs );

protected:
	T* _ptr;
};

为了防止别人恶意定义这个方法,所以我声明为保护或私有成员,这样如果你在类外面实现定义,你也调用不到我.
这是一种思想,比如我的类就是不想让你拷贝.
就可以这样做


new/malloc/fopen/lock

/* 如果在这里面 return 或者 抛异常了未调用下面函数, 就坑了, 内存泄漏*/

delete/free/fclose/unclock


使用:

Scoped<int> sp1( new int( 10 ) );
*sp1 = 12;

Scoped<AA> sp2( new AA );
sp2->a1 = 12;
sp2->a2 = 13;


因为 ScopedPtr 不能拷贝, 也存在缺陷, 我们又有了一种改进的智能指针 , SharedPtr:


3.SharedPtr

//定制删除器, 因为指针有可能使用两种或多种方式释放
template<class T>
class DeleteArray
{
	void operator( )( T* ptr )
	{
		delete[] ptr;
	}
};

template<class T>
class Delete
{
	void operator( )( T* ptr )
	{
		delete ptr;
	}
};


template<class T, class Del = Delete<T>>
class SharedPtr
{
public:
	SharedPtr( T* ptr )
		:_ptr( ptr )
		,_refCount( new int( 1 ) )				//构造函数中初始化, 所以有一个管理那块空间的对象了
	{}

	~SharedPtr( )
	{
		Release( );
	}

	inline void Release( )						//小技巧,因为需要频繁调用它,有函数栈帧,开销。 所以用内联函数,因为代码少,所以让它展开。
	{
		//先看我是不是最后一个管理这块空间的对象, 如果是 ,再释放, 同时释放 引用计数的指针。
		if ( 0 == --*_refCount )				//小技巧,因为它总要减减,所以我先减减,再和 0 比较!
		{
			cout << "Release ptr( ):OX" << _ptr << endl;
			delete _refCount;
			//delete _ptr;						//别这样
			_del( _ptr );						//利用仿函数
		}
	}

	//ap2( ap1 )								拷贝构造
	SharedPtr( const SharedPtr<T>& sp )
		:_ptr( sp._ptr )
		,_refCount( sp._refCount )
	{
		++(*_refCount);
	}

	//sp1 = sp4									赋值运算符重载
	SharedPtr<T> operator=( const SharedPtr<T>& sp )
	{
		//好好考虑下面的代码代表的情况
		//1.自己给自己赋值  2.两个对象管理着同一块空间( 小心,虽然不会出错,但可能会做无用功! 先减减,再加加。 如果用 this != &sp )  3.管理着不同的空间
		if ( _ptr != sp._ptr )
		{
			this->Release( );
			//Release( );

			_ptr = sp._ptr;
			++(*sp._refCount);
			_refCount = sp._refCount;
		}

		return *this;							//别忘了返回
	}

	T& operator*( )						
	{
		return *_ptr;
	}

	T* operator->( )
	{
		return _ptr;
	}

protected:
	T* _ptr;
	int* _refCount;								//保证, 拷贝构造 或 赋值 都是同一个 (*_refCount) 后值!
	//int _refCount;							//引用计数 -->  这样定义有坑,因为每个对象都有一个 自己的_refCount, 且各个对象间_refCount互不影响。
	//这里也不能用static变量来表示引用计数 --> 因为static变量是所有对象共享的,不属于哪个特定成员。假设这样一种情景:三个shared_ptr对象管理内存块A,此时引用计数为3.  又有三个shared_ptr对象管理内存块B,因为static变量是所有对象共享的。 此时引用计数+3为6。 如果管理A内存块的三个对象的生命周期到了,A内存块本来应该被析构了。但此时引用计数6-3为3.A内存块不析构,所以也不能用stati变量。
	//所以我们用指针来 表示引用计数   有几个对象指向那块地址 count就为几
	Del _del;
};
仿函数(函数对象):

template<class T>
struct Less
{
	bool operator( )( const T& l, const T& r )
	{
		return l < r;
	}
};

int i1 = 10;
int i2 = 12;
cout << i1 < i2 << endl;

Less<int> less;
cout << less( i1, i2 ) << endl; 


如果我们用 delete 来释放_ptr,程序有可能会出问题, 如下:
SharedPtr<int> sp1( new int( 10 ) );
SharedPtr<int> sp2( sp1 );
SharedPtr<int> sp3( new int[10] );				//不会挂
SharedPtr<string> sp4( new string[3] );			//挂了,这里程序会挂掉

内置类型的 new[ ] 可以delete来释放, 但自定义类型不可以

如; new string[ ] 只能用 delete[ ] 而不能用 delete

new int[10]   int为内置类型。  开 4 * 10  == 40 个字节 空间

自定义类型 new []  如string  多开四个字节 存放 string 对象 个数 以免delete[]不知道 释放多少个。 即先是四个字节空间, 再是10个 string 对象空间。    1....      2...........       

对于 内置类型, 不会多开这四个字节空间, 如上面的 int 直接开 40个 字节。

所以对于 new[ ]  出来的 内置类型, 因为没有多开空间, 所以 调用delete 和 delete[]都可以

但是对于 自定义类型的 new[ ], 会先开四个字节空间(int)存放对象个数, 所以delete[ ] 从 真正对象开始地方析构, 而delete 从 第一个四个字节那开始析构, 释放位置不对,

程序会出问题, 所以我们不能只用 delete来 释放_ptr, 必须定制删除器。

new调用 operator new( ) 调用malloc

new/new[] 底层 --> malloc

delete/delete[] 底册 --> free

因为它们底层都是一样的, 只是上层封装实现方式有些不同:


因为我们上面已经定制了 删除器DeleteArray

所以我们采用如下方式 使用shared_ptr智能指针对象,和  删除器

SharedPtr<string, DeleteArray<string>> sp4( new string[3] );(此时我们模拟实现的 shared_ptr中 已经使用定制的删除器来释放特有空间, 而不是 默认的delete来释放所有空间)

之前我们是这样调用的(错误):SharedPtr<string> sp4( new string[3] );(这时,我们的类还未修改, 依然使用 delete 来释放管理的所有指针)

当然了, 因为我们要管理的指针多种多样,这两种删除器是完全不够的, 如下面两种药管理的指针:

SharedPtr<int/*, Free<int>*/> sp5( (int*)malloc( sizeof(int) * 10 ) );
SharedPtr<FILE/*, //Fclose<FILE>*/> sp100( fopen("test.txt", "w") );

我们就需要再定制删除器:

定制删除器: ( 删除方式 )  ->  通过仿函数完成

template<class T>
struct Free
{
	void operator( )( T* ptr )
	{
		free( ptr );
	}
};

template<class T>
struct Fclose
{
	void operator( )( T* ptr )
	{
		fclose( ptr );
	}
};


后定义的对象先释放(析构)
因为栈 开辟 依次向下, 然后, 释放时从最下面开始


仿函数 -> 通过仿函数--定制智能指针的释放方式


1.RAII  2. operator* / operator->  3.解决拷贝问题 -> AutoPtr -> ScopedPtr -> SharedPtr


我们这里模拟的智能指针是使用驼峰法命名,而库里面是 采用小写加下划线

SharedPtr 共享,引用计数 //功能强大,复杂,但可能会循环引用


智能指针发展历史:
1 auto_ptr c++98/03 存在严重缺陷设计
2 scoped_ptr/shared_ptr/weak_ptr boost
3 unique_ptr/shared_ptr/weak_ptr c++11


我们使用了自己模拟的 智能指针后, 来使用下库中的智能指针


头文件:

#include <iostream>
#include <memory>
using namespace std;
:

int main( )
{
	auto_ptr<int> ap1( new int( 10 ) );
	auto_ptr<int> ap2 = ap1;  /*或者*/  	auto_ptr<int> ap2( ap1 ); //后
	*ap1 = 10;  //这样就会出错。
 
	//注意,有的编译器无法访问unique_ptr。 因为这个智能指针是 c++11的 编译器必须是 c++11标准之后 出的才可以访问.
	unique_ptr<int> ap3( new int( 2 ) );
	unique_ptr<int> ap4( ap3 );	//有问题,因为它是不允许拷贝的
	*ap3 = 10;

	//shared_ptr  中的 use_count方法 返回当前引用计数
	shared_ptr<int> ap5( new int( 30 ) );
	cout << ap5.use_count( ) << endl;

	shared_ptr<int> ap6( ap5 );
	cout << ap5.use_count( ) << endl;

	*ap5 = 10;

	return 0;
}

share_ptr 虽然强大,但也存在缺陷; 可能循环引用:

假设我们有如下的 双向链表节点:

struct ListNode
{
	int _data;
	ListNode* _prev;
	ListNode* _next;

	ListNode( int x )
		:_data( x )
		,_prev( NULL )							//weak_ptr 时,就不能给成 NULL 了
		,_next( NULL )
	{}

	~ListNode( )
	{
		cout << "~ListNode" <<endl;
	}
};

int main( )
{
	/*两个节点 一个cur 一个next
	不用我们去主动释放(delete)这两个节点,让智能指针去做
	下面的两行代码,相当于创建两个结点交给 智能指针去管理*/
	shared_ptr<ListNode> cur( new ListNode( 1 ) );
	shared_ptr<ListNode> next( new ListNode( 2 ) );

	cout << "cur:" << cur.use_count( ) << endl;
	cout << "next:" << next.use_count( ) << endl;

	//两个结点链接起来
	cur->_next = next;
	next->_prev = cur;
	//此时编译不通过, 因为后面的 next 是智能指针对象, 而第一个 cur->_next 是一个原生指针。




	//解决办法:1.  将结点定义中的:ListNode* _prev  ---->   shared_ptr<ListNode> _prev
	//链接后,两个智能指针对象的引用计数均变为2, 两个内存块都由两个智能指针对象管理它,但此时没有析构它:shared_ptr 虽然强大,但是有一个缺陷:循环引用  (循环引用是怎么回事,怎么解决))  
	//赋值时,是 智能指针对象 对 智能指针对象 赋值  。    智能指针对象那个内存块中 开辟一个 小空间保存引用计数 值。 赋值后, 两个对象引用计数均为2.   出了作用域后 -> 调用析构函数.  cur 和 next 调用析构函数。  所以cur 和 next都不指向它们原本指向的内存块(即 第一次管理这两个结点的 那两个智能指针对象, 析构)。 引用计数均变为1。  但此时它们两个内存块释放依赖于对方智能指针析构( next对象释放依赖于prev中的 _next智能指针对象, 同理 ), 减引用计数为0.      cur 原本指向的内存块中有 _next 智能指针, 指向 next 原本指向内存块。   
	//看图:
	//怎么解决呢?
	
	cout << "cur:" << cur.use_count( ) << endl;
	cout << "next:" << next.use_count( ) << endl;
	return 0;
}

这就是shared_ptr存在的问题, 循环引用.


解决方法: weak_ptr

weak_ptr 可以看做 shared_ptr附属,小跟班(解决shared_ptr缺陷(循环引用))
解决方法: shared_ptr<ListNode> _prev;  ->  weak_ptr<ListNode> prev;
产生循环引用场景: 两个智能指针对象, 并且对象成员里有智能指针指向双方 -> 用 weak_ptr 解决(不增加引用计数)(配合解决 shared_ptr 缺陷).  出了作用域自己管理释放.  

weak_ptr我们只介绍原理作用,并不实现它。


int main( )
{
	//其余智能指针不支持定制删除器,因为他们本来功能就不全,没必要支持定制删除器,如果想用其他方式释放(定制删除器)就用 shared_ptr
	std::shared_ptr<int> ap1( new int[10] );

	shared_ptr<string> ap1( new string[10] );	//需要定制删除器,用 delete[] 释放,否则程序出错

	//库里面这样定制删除器-->
	DeleteArray<string> del1;
	shared_ptr<string> ap1( new string[10], del1 );

	//模板好处,假如还有一个 int 类型

	DeleteArray<int> del2;
	shared_ptr<int> ap2( new int[10], del2 );
	
	//但 del1 与 del2 起名不方便,  再改进, del是一个类型, 干脆我传一个匿名对象
	shared_ptr<string> ap1( new string[10], DeleteArray<string>( ) );
	shared_ptr<int> ap2( new int[10], DeleteArray<int>( ) );
	shared_ptr<FILE> ap3( fopen( "test.txt", "w"/*以读的方式打开会失败,如果文件不存在。但写的方式,会自己创建一个*/ )/*不写也会奔溃,因为默认的参数是DeleteArray*/ );
	shared_ptr<FILE> ap3( fopen( "test.txt", "w"/*以读的方式打开会失败,因为文件不存在。但写的方式,会自己创建一个*/ ), Fclose( ) );
}

template<class T>
struct DeleteArray
{
	void operator( )( T* ptr )
	{
		cout << ptr << endl;

		delete[] ptr;
	}
};

struct Fclose
{
	void operator( )( FILE* ptr )
	{
		cout << ptr << endl;

		fclose( ptr );
	}
};

如果我们编译器就是 vs2008 想用 shared_ptr 智能指针(C++11)怎么办 -> 借用boost库

#include <boost\shared_ptr.hpp>//工程属性 -> 配置属性 -> C\C++ -> 常规 -> 附加包含目录(需要包第三方库, 第三方路径放这, 除了去系统库目录下找,还会在这找) -> 把 boost 全部目录包进来 //用静态库,动态库 需要 配置(.lib , .dll)
然后我们需要的是 这个目录下的 boost 里的 shared_ptr  所以, boost\shared_ptr


shared_ptr<int> ap3( new int( 10 ) );//编译不通过, 找不到头文件
因为 boost 命名空间 using namespace boost;  or    boost::


shared_ptr<int> sp1( new int( 10 ) );
cout << sp1.use_count( ) << endl;
shared_ptr<int> sp2( sp1 );
cout << sp1.use_count( ) << endl;


1.循环引用 -> weak_ptr  典型场景 双向链表
2.定制删除器 


注意  shared_ptr  如果 在支持 C++ 11 编译器下  using namespace std and using namespace boost  名字冲突 , 解决办法 -> boost::shared_ptr....     std::shared_ptr....  如果 两个都using 则默认找 std中的 (所以编译报错,二义性) 

boost库中还有 shared_array:

boost::shared_array<string> spArray( new string[10] );
*spArray = 10; 不支持 ,因为你在解引用哪个对象(它是数组)? 没意义


spArray[0] = "111";
spArray[1] = "211";


shared_array只有 boost 有  c++11没有, 所以你想释放数组就定制删除器.

智能指针总结:


1.auto_ptr  管理权转移  --  带有缺陷的设计 -- c++98/03
2.scoped_ptr(boost) unique_ptr(c++11)   防拷贝 -- 简单粗暴设计  --  功能不全
3.shared_ptr(boost/c++11)   引用计数  -- 功能强大(支持拷贝,支持定制删除器)  缺陷: 循环引用(weak_ptr配合解决(不增加它引用计数))


RAII是一种解决问题思想 (抛异常,return --> 资源泄露)   智能指针是RAII一种应用


1.构造函数初始化(把指针保留起来),对象销毁时,析构函数自动调用 --> 释放资源(RAII核心思想)。
2.像指针一样使用 --> operator*/operator->/      operator[](这个在特殊场景下使用 -> 指针指向数组时) 
3.为了支持的正确赋值与拷贝构造,而且保证只释放一次 -> 产生了各种类型智能指针


如果自己实现 简单智能指针  最好实现  scoped_ptr  因为它没有大的缺陷,而且简单。


c++最爱考 : c 和 c++ 区别

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章