您的位置:首页 > 编程语言 > C语言/C++

C++11 智能指针

2016-03-14 16:34 288 查看
原作者:Babu_Abdulsalam

本文翻译自CodeProject,转载请注明出处。

引入

Ooops. 尽管有另外一篇文章说
C++11
里的智能指针了。近来,我听到许多人谈论
C++
新标准,就是所谓的
C++0x/C++11
。 我研究了一下
C++11
的一些语言特性,发现确实它确实做出了一些惊奇的改变。我将重点关注
C++11
的智能指针部分。

背景

普通指针(normal/raw/naked pointers)的问题?

让我们一个接一个的讨论。

如果不恰当处理指针就会带来许多问题,所以人们总是避免使用它。这也是许多新手程序员不喜欢指针的原因。指针总是会扯上很多问题,例如指针所指向对象的生命周期,挂起引用(
dangling references
)以及内存泄露。

如果一块内存被多个指针引用,但其中的一个指针释放且其余的指针并不知道,这样的情况下,就发生了挂起引用。而内存泄露,就如你知道的一样,当从堆中申请了内存后不释放回去,这时就会发生内存泄露。

有人说,我写了清晰并且带有错误验证的代码,为什么我还要使用智能指针呢?一个程序员也问我:“嗨,下面是我的代码,我从堆(
heap
)中申请了一块内存,使用完后,我又正确的把它归还给了堆,那么使用智能指针的必要在哪里?”

void Foo( )
{
int* iPtr = new int[5];

//manipulate the memory block
.
.
.

delete[ ] iPtr;
}


理想状况下,上面这段代码确实能够工作的很好,内存也能够恰当的释放回去。但是仔细思考一下实际的工作环境以及代码执行条件。在内存分配和释放的间隙,程序指令确实能做许多糟糕的事情,比如访问无效的内存地址,除以
0
,或者有另外一个程序员在你的程序中修改了一个bug,他根据一个条件增加了一个过早的返回语句。

在以上所有情况下,你的程序都走不到内存释放的那部分。前两种情况下,程序抛出了异常,而第三种情况,内存还没释放,程序就过早的return了。所以程序运行时,内存就已经泄露了。

解决以上所有问题的方法就是使用智能指针[如果它们足够智能的话]。

什么是智能指针?

智能指针是一个
RAII
Resource Acquisition is initialization
)类模型,用来动态的分配内存。它提供所有普通指针提供的接口,却很少发生异常。在构造中,它分配内存,当离开作用域时,它会自动释放已分配的内存。这样的话,程序员就从手动管理动态内存的繁杂任务中解放出来了。

C++98
提供了第一种智能指针:
auto_ptr


auto_ptr

让我们来见识一下
auto_ptr
如何解决上述问题的吧。

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

void main( )
{
std::auto_ptr<Test> p( new Test(5) );
cout<<p->m_a<<endl;
}


上述代码会智能地释放与指针绑定的内存。作用的过程是这样的:我们申请了一块内存来放
Test
对象,并且把他绑定到
auto_ptr
p
上。所以当
p
离开作用域时,它所指向的内存块也会被自动释放。

//***************************************************************
class Test
{
public:
Test(int a = 0 ) : m_a(a)
{
}
~Test( )
{
cout<<"Calling destructor"<<endl;
}
public:
int m_a;
};
//***************************************************************
void Fun( )
{
int a = 0, b= 5, c;
if( a ==0 )
{
throw "Invalid divisor";
}
c = b/a;
return;
}
//***************************************************************
void main( )
{
try
{
std::auto_ptr<Test> p( new Test(5) );
Fun( );
cout<<p->m_a<<endl;
}
catch(...)
{
cout<<"Something has gone wrong"<<endl;
}
}


上面的例子中,尽管异常被抛出,但是指针仍然正确地被释放了。这是因为当异常抛出时,栈松绑(
stack unwinding
),当
try
块中的所有对象destroy后,
p
离开了该作用域,所以它绑定的内存也就释放了。

Issue1:

目前为止,
auto_ptr
还是足够智能的,但是它还是有一些根本性的破绽的。当把一个
auto_ptr
赋给另外一个
auto_ptr
时,它的所有权(ownship)也转移了。当我在函数间传递
auto_ptr
时,这就是一个问题。话说,我在
Foo()
中有一个
auto_ptr
,然后在
Foo()
中我把指针传递给了
Fun()
函数,当
Fun()
函数执行完毕时,指针的所有权不会再返还给
Foo


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

//***************************************************************
void Fun(auto_ptr<Test> p1 )
{
cout<<p1->m_a<<endl;
}
//***************************************************************
void main( )
{
std::auto_ptr<Test> p( new Test(5) );
Fun(p);
cout<<p->m_a<<endl;
}


由于
auto_ptr
的野指针行为,上面的代码导致程序崩溃。在这期间发生了这些细节,p拥有一块内存,当
Fun
调用时,
p
把关联的内存块的所有权传给了
auto_ptr`` p1
,
p1
p
的copy(注:这里从
Fun
函数的定义式看出,函数参数时值传递,所以把
p
的值拷进了函数中),这时
p1
就拥有了之前
p
拥有的内存块。目前为止,一切安好。现在
Fun
函数执行完了,
p1
离开了作用域,所以
p1
关联的内存块也就释放了。那么
p
呢?
p
什么都没了,这就是crash的原因了,下一行代码还试图访问
p
,好像
p
还拥有什么资源似的。

Issue2:

还有另外一个缺点。auto_ptr不能指向一组对象,就是说它不能和操作符new[]一起使用。

//***************************************************************
void main( )
{
std::auto_ptr<Test> p(new Test[5]);
}


上面的代码将产生一个运行时错误。因为当
auto_ptr
离开作用域时,
delete
被默认用来释放关联的内存空间。当
auto_ptr
只指向一个对象时,这当然是没问题的,但是在上面的代码里,我们在堆里创建了一组对象,应该使用
delete[]
来释放,而不是
delete
.

Issue3:

auto_ptr
不能和标准容器(vector,list,map….)一起使用。

因为
auto_ptr
容易产生错误,所以它也将被废弃了。
C++11
提供了一组新的智能指针,每一个都各有用武之地。

shared_ptr

unique_ptr

weak_ptr

shared_ptr

好吧,准备享受真正的智能。第一种智能指针是
shared_ptr
,它有一个叫做共享所有权(sharedownership)的概念。
shared_ptr
的目标非常简单:多个指针可以同时指向一个对象,当最后一个
shared_ptr
离开作用域时,内存才会自动释放。

创建:

void main( )
{
shared_ptr<int> sptr1( new int );
}


使用
make_shared
宏来加速创建的过程。因为
shared_ptr
主动分配内存并且保存引用计数(
reference count
),
make_shared
以一种更有效率的方法来实现创建工作。

void main( )
{
shared_ptr<int> sptr1 = make_shared<int>(100);
}


上面的代码创建了一个
shared_ptr
,指向一块内存,该内存包含一个整数
100
,以及引用计数
1
.如果通过
sptr1
再创建一个
shared_ptr
,引用计数就会变成2. 该计数被称为
强引用(strong reference)
,除此之外,
shared_ptr
还有另外一种引用计数叫做
弱引用(weak reference)
,后面将介绍。

通过调用
use_count()
可以得到引用计数, 据此你能找到
shared_ptr
的数量。当debug的时候,可以通过观察
shared_ptr
strong_ref
的值得到引用计数。



析构

shared_ptr
默认调用
delete
释放关联的资源。如果用户采用一个不一样的析构策略时,他可以自由指定构造这个
shared_ptr
的策略。下面的例子是一个由于采用默认析构策略导致的问题:

class Test
{
public:
Test(int a = 0 ) : m_a(a)
{
}
~Test( )
{
cout<<"Calling destructor"<<endl;
}
public:
int m_a;
};
void main( )
{
shared_ptr<Test> sptr1( new Test[5] );
}


在此场景下,
shared_ptr
指向一组对象,但是当离开作用域时,默认的析构函数调用
delete
释放资源。实际上,我们应该调用
delete[]
来销毁这个数组。用户可以通过调用一个函数,例如一个
lamda
表达式,来指定一个通用的释放步骤。

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


通过指定delete[]来析构,上面的代码可以完美运行。

接口

就像一个普通指针一样,
shared_ptr
也提供解引用操作符
*
,
->
。除此之外,它还提供了一些更重要的接口:

get()
: 获取
shared_ptr
绑定的资源.

reset()
: 释放关联内存块的所有权,如果是最后一个指向该资源的
shared_ptr
,就释放这块内存。

unique
: 判断是否是唯一指向当前内存的
shared_ptr
.

operator bool
: 判断当前的
shared_ptr
是否指向一个内存块,可以用if 表达式判断。

OK,上面是所有关于
shared_ptr
的描述,但是
shared_ptr
也有一些问题:

Issues:

void main( )
{
shared_ptr<int> sptr1( new int );
shared_ptr<int> sptr2 = sptr1;
shared_ptr<int> sptr3;
sptr3 =sptr


Issues:

下表是上面代码中引用计数变化情况:



所有的
shared_ptrs
拥有相同的引用计数,属于相同的组。上述代码工作良好,让我们看另外一组例子。

void main( )
{
int* p = new int;
shared_ptr<int> sptr1( p);
shared_ptr<int> sptr2( p );
}


上述代码会产生一个错误,因为两个来自不同组的
shared_ptr
指向同一个资源。下表给你关于错误原因的图景:



避免这个问题,尽量不要从一个裸指针
(naked pointer)
创建
shared_ptr
.

class B;
class A
{
public:
A(  ) : m_sptrB(nullptr) { };
~A( )
{
cout<<" A is destroyed"<<endl;
}
shared_ptr<B> m_sptrB;
};
class B
{
public:
B(  ) : m_sptrA(nullptr) { };
~B( )
{
cout<<" B is destroyed"<<endl;
}
shared_ptr<A> m_sptrA;
};
//***********************************************************
void main( )
{
shared_ptr<B> sptrB( new B );
shared_ptr<A> sptrA( new A );
sptrB->m_sptrA = sptrA;
sptrA->m_sptrB = sptrB;
}


上面的代码产生了一个循环引用.
A
B
有一个
shared_ptr
,
B
A
也有一个
shared_ptr
,与
sptrA
sptrB
关联的资源都没有被释放,参考下表:



sptrA
sptrB
离开作用域时,它们的引用计数都只减少到1,所以它们指向的资源并没有释放!!!!!

如果几个
shared_ptrs
指向的内存块属于不同组,将产生错误。

如果从一个普通指针创建一个
shared_ptr
还会引发另外一个问题。在上面的代码中,考虑到只有一个
shared_ptr
是由
p
创建的,代码可以好好工作。万一程序员在智能指针作用域结束之前删除了普通指针
p
。天啦噜!!!又是一个crash。

循环引用:如果共享智能指针卷入了循环引用,资源都不会正常释放。

为了解决循环引用,
C++
提供了另外一种智能指针:
weak_ptr


Weak_Ptr

weak_ptr
拥有共享语义(
sharing semantics
)和不包含语义(
not owning semantics
)。这意味着,
weak_ptr
可以共享
shared_ptr
持有的资源。所以可以从一个包含资源的
shared_ptr
创建
weak_ptr


weak_ptr
不支持普通指针包含的
*
->
操作。它并不包含资源所以也不允许程序员操作资源。既然如此,我们如何使用
weak_ptr
呢?

答案是从
weak_ptr
中创建
shared_ptr
然后再使用它。通过增加强引用计数,当使用时可以确保资源不会被销毁。当引用计数增加时,可以肯定的是从
weak_ptr
中创建的
shared_ptr
引用计数至少为
1
.否则,当你使用
weak_ptr
就可能发生如下问题:当
shared_ptr
离开作用域时,其拥有的资源会释放,从而导致了混乱。

创建

可以以
shared_ptr
作为参数构造
weak_ptr
.从
shared_ptr
创建一个
weak_ptr
增加了共享指针的弱引用计数(
weak reference
),意味着
shared_ptr
与其它的指针共享着它所拥有的资源。但是当
shared_ptr
离开作用域时,这个计数不作为是否释放资源的依据。换句话说,就是除非强引用计数变为
0
,才会释放掉指针指向的资源,在这里,弱引用计数(
weak reference
)不起作用。

void main( )
{
shared_ptr<Test> sptr( new Test );
weak_ptr<Test> wptr( sptr );
weak_ptr<Test> wptr1 = wptr;
}


可以从下图观察
shared_ptr
weak_ptr
的引用计数:



将一个
weak_ptr
赋给另一个
weak_ptr
会增加弱引用计数(
weak reference count
)。

所以,当
shared_ptr
离开作用域时,其内的资源释放了,这时候指向该
shared_ptr
weak_ptr
发生了什么?
weak_ptr
过期了(
expired
)。

如何判断
weak_ptr
是否指向有效资源,有两种方法:

调用
use-count()
去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。

调用
expired()
方法。比调用
use_count()
方法速度更快。

weak_ptr
调用
lock()
可以得到
shared_ptr
或者直接将
weak_ptr
转型为
shared_ptr


void main( )
{
shared_ptr<Test> sptr( new Test );
weak_ptr<Test> wptr( sptr );
shared_ptr<Test> sptr2 = wptr.lock( );
}


如之前所述,从
weak_ptr
中获取
shared_ptr
增加强引用计数。

现在让我们见识一下
weak_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;
}
}

void 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
离开作用域,所包含的资源被释放。如果资源被其它资源重写了,之前拥有的资源将被释放。所以它保证了他所关联的资源总是能被释放。

创建

unique_ptr
的创建方法和
shared_ptr
一样,除非创建一个指向数组类型的
unique_ptr


unique_ptr<int> uptr( new int );


unique_ptr
提供了创建数组对象的特殊方法,当指针离开作用域时,调用
delete[]
代替
delete
。当创建
unique_ptr
时,这一组对象被视作模板参数的部分。这样,程序员就不需要再提供一个指定的析构方法,如下:

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


当把
unique_ptr
赋给另外一个对象时,资源的所有权就会被转移。

记住
unique_ptr
不提供复制语义(拷贝赋值和拷贝构造都不可以),只支持移动语义(
move semantics
).

在上面的例子里,如果
upt3
upt5
已经拥有了资源,只有当拥有新资源时,之前的资源才会释放。

接口

unique_ptr提供的接口和传统指针差不多,但是不支持指针运算。

unique_ptr
提供一个
release()
的方法,释放所有权。
release
reset
的区别在于,
release
仅仅释放所有权但不释放资源,
reset
也释放资源。

使用哪一个?

完全取决于你想要如何拥有一个资源,如果需要共享资源使用
shared_ptr
,如果独占使用资源就使用
unique_ptr
.

除此之外,
shared_ptr
unique_ptr
更加重,因为他还需要分配空间做其它的事,比如存储强引用计数,弱引用计数。而
unique_ptr
不需要这些,它只需要独占着保存资源对象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++ 智能指针