您的位置:首页 > 大数据 > 人工智能

剖析智能指针

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

首先我们来看一段代码:  

如:

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++ 区别
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息