RAII和unique_ptr

RAII

RAII是Resource Acquisition Is
Initialization
的缩写,是在面向对象(object-oriented)语言中使用的一种编程习惯,主要是用来在C++中处理异常安全资源管理(exception-safe
resource management
)。

在RAII中,资源的获取和释放和对象的声明周期紧密联系在一起,当对象构造的时候,在构造函数中申请资源( resource
allocation),而在对象的析构函数中释放资源(resource
deallocation),当析构函数正确执行时,就不会有资源泄露,这让我想起了Linux内核驱动,在编写驱动的过程中,一般也是在init函数中申请资源,然后在exit函数中释放资源,能够避免资源泄露,因此这种思想普遍运用在编程中。

优点

RAII作为一种资源管理的技术主要有以下优点:

  1. 封装性,因为将异常处理的逻辑写到了构造和析构函数中,因此不用调用者关心怎么对资源进行管理。

  2. 异常安装,对于栈对象,当异常发生的时候,异常处理会在离开当前scope的时候,执行对象的析构函数,从而释放内存。

  3. 可定位,能够将资源申请和释放的逻辑集中写在构造和析构中,不会散乱在各个地方。

典型应用

RAII作为一种资源管理技术,主要运用于下面几个地方: 1.
在多线程环境下控制同步锁。在多线程中,为了线程间同步,经常要申请、释放锁,而有时候,经常会出现申请了,但是没有或者由于异常等原因没有释放锁,从而出现死锁。对于这种申请、释放锁的共走,可以交给对象的构造和析构函数,这样可以将申请、释放封装起来,并且保证了异常安全。
2. 文件处理,一般在处理文件的时候,我们都要先open-write/read-close,对于这种固定的结构,完成可以将read-write封装起来。 3.
动态对象的所有权问题。针对动态对象的所有权,C++提供了smart
pointer
,其中std::unique_ptr针对单拥有权问题,而std::shared_ptr则是针对共享对象。

例子:

#include <string>
#include <mutex>
#include <iostream>
#include <fstream>
#include <stdexcept>

void write_to_file (const std::string & message) {
// 互斥访问
static std::mutex mutex;

// 加锁
std::lock_guard<std::mutex> lock(mutex);

// 打开文件
std::ofstream file("example.txt");
if (!file.is_open())
throw std::runtime_error("unable to open file");

// 写
file << message << std::endl;

// 在离开scope的时候,ofstream在析构函数中关闭文件,
// lock_guard会在构造中加锁,在析构中解锁
}


GCC对于C的扩展


gnu针对C提供了一种非标准的扩展来支持RAII:"cleanup" variable attribute。下面是一个例子:

static inline void fclosep(FILE **fp) { if (*fp) fclose(*fp); }
#define _cleanup_fclose_ __attribute__((cleanup(fclosep)))
void example_usage() {
_cleanup_fclose_ FILE *logfile = fopen("logfile.txt", "w+");
fputs("hello logfile!", logfile);
}

下面对前面提到的unique_ptr进行介绍。

C++11: unique_ptr

unique_ptr是C++11新增的一个特性,作为一种新的smart pointer,主要用于管理只有单一拥有权的动态对象。

基本用法:

std::unique_ptr<foo> p( new foo(42) );

unique_ptr有着智能指针的优点,在析构函数中会自动释放资源。

在C++11之前有auto_ptr用来做动态对象的管理,但是复制auto_ptr会将控制权从右值转移到左值,原先的右值将不再拥有对象的控制权,这和传统的“复制”语义不符,而且由于这种复制语义,导致了auto_ptr不能被用在标准的容器中。在C++11中出现了unique_ptr开解决这个问题。使用unique_ptr的时候,unique_ptr可以存储在容器中,当容器析构的时候unique_ptr指向的对象也被释放。

那C++11是怎么解决不能存在在容器中的问题的呢?通过添加了rvalue reference和move语义。

什么是rvalue reference?

rvalue reference是C++的一个小扩展,rvalue
reference允许coder避免逻辑上不必要的复制,并且提供了一个完美的转发功能。主要目的是帮助设计高性能、健壮的库。在A
Brief Introduction to Rvalue References
中有对rvalue reference的介绍。那为什么要引入rvalue
reference,引入rvalue reference解决了什么问题呢?

Lvalues vs rvalues

传递给函数参数的时候,我们可以传值或者传引用。在C中,传引用是通过传递指针,而指针实际上就是传递的一个地址,在C/C++中,内存模型就是申请一个box,然后在这个box中存放数据,而传递的时候就是传递这个box的地址,对于这个box怎么管理从而引申出好多问题。但是不是所有的数据都有地址,譬如某些存放在寄存器中的数据,此时就没有地址了,因此,对于有地址的数据我们叫做lvalues,而没地址的数据叫rvalues。那实际中有哪些rvalues,譬如函数的返回值,数值计算式等。

下面是一个例子:

void f(int *pi);
int g();

f(&g()); //error
f(&(1+1)); // error


上面这个是无法通过的,因为传入的参数是右值,没有地址,一个简单的修改如下:

int temp1 = g();
f(&temp1);
int temp2 = 1 + 1;
f(&temp2;

那为什么编译器不帮我们创建临时变量,然后不再报错呢?

让我们想下,一般当参数是地址的时候,我们在函数内部是希望改变指向的对象的值,如:

void f(int *pi){ ++*pi; }

此时我们如果传入一个临时变量,对于这个函数内部的操作就变的没有意义了。因此,此时编译器就会报错,不允许将rvalue的地址传入。

Reference

在C++中,除了指针外,还加入了引用,于是,上面的代码可以重写如下:

void f(int &pi){ pi++; }
int g();

f(g()); //error
f((1+1)); // error


此时还是无法得到rvalue的引用(reference),但是有时候传递引用并不是为了改变其值,对于一些大的数据,传递引用可以减少开销,此时并不对lvalue和rvalue有要求。因此,编译器因该能够允许传递rvalue的reference。

Const reference

当我们不会改变传递进来的reference的值时,则传递进来lvalue或者rvalue都是可以的,因此,上面的代码如果改成下面的:

void f(const int &pi){ pi++; }
int g();

f(g()); //ok
f((1+1)); // ok


此时看起来一切都挺好的,但是引入ato_ptr后,就会带来一系列的问题。

现在总结下reference和lvalue和rvalue的关系:

  1. reference可以绑定到lvalues

  2. const reference可以绑定到lvalues和rvalues,但是不允许改变源值

上面看上去都挺好的,除了不能将reference绑定到rvalues,并且修改,但是谁又想要改变一个临时的值呢?

auto_ptr

auto_ptr是智能指针中的一员,用来自动释放指向的内存块,auto_ptr有value的语义,可以传递,在栈上创建,或者是其他数据的长成员,在传递给函数的时候是通过值传递,但是当auto_ptr进行值传递的时候,其指向的数据并不复制,这又像reference的行为,同时可以对auto_ptr使用*和->,就像指针一样。

考虑下面的代码:

auto_ptr<int> create() {
return auto_ptr<int>(new int(42));
}

auto_ptr<int> ap = create();


考虑上面的代码,在create()内部,当结束的时候,刚离开scope的时候,如果不做什么,编译器会调用auto_ptr的析构函数,从而释放内存;函数create返回的是一个rvalue。

对于第一点,调用析构函数,我们希望将其赋给ap,此时因该调用拷贝构造函数,而对于右值有下面的两个问题:

  1. 如果我们定义拷贝构造函数的source为const reference,则此时无法修改source,将拥有权转移。

  2. 如果定义非const的reference,此时不能够绑定到右值。

rvalue reference

对于上面的问题,我们需要的是rvalue
reference,能够绑定到rvalue,并且修改他。于是针对auto_ptr不能复制构造右值,出现了unique_ptrunique_ptr有下面的复制构造函数:

unique_ptr::unique_ptr(unique_ptr && src)

rvalue reference能够同时绑定到lvalue和rvalue,并且不阻止改变值。

why auto_ptr can‘t store auto_ptr objects in most containers

auto_ptr还有一点问题是,不能够存储在大多数容器中,为什么呢?考虑下面的一个例子:

 auto_ptr<Foo> pSrc(new Foo);
auto_ptr<Foo> pDest = pSrc; // 看起来像像拷贝,但是不具备拷贝的语义,会将源中指针置空 pSrc->method(); // 运行时错误,因为此时pSrc中指针为空

而在容器中,会有临时变量,一旦拷贝,则容器中的值将不再有所有权,这是严重的错误。

但是有时候我们又想要将控制权进行转移,此时我们应该明确的告知说我们现在要转移控制权了,因此不要再去使用原先的源了。

此处我们总结下rvalue和lvalue的不同,rvalue会在赋值后消失,而lvalue则保持不变。我们看到上面的unique_ptr的复制构造函数:

unique_ptr::unique_ptr(unique_ptr && src)

这会同时绑定rvalue好lvalue,因此我们需要下面的一个重载函数:

unique_ptr::unique_ptr(unique_ptr & src)

重载rvalue,此时我们要做的就是将rvalue的构造私有化,现在unique_ptr只保持转移控制权的语义了,而当我们想要将一个lvalue转移到unique_ptr的时候,此时使用move函数,

unique_ptr pSrc(new Foo);
unique_ptr pDest = move(pSrc);

明确的将lvalue变为rvalue,move作用就是将一个lvalue转成rvalue,

template <class T>
typename remove_reference<T>::type&& move(T&& t) {
return t;
}

下面我们接着介绍unique_ptr,unique_ptr中unique在此处是指什么呢?此处unique表示,当你创建一个对象的时候,只会有一份拷贝,只会有一个指针。

平时我们使用指针的时候,经常会碰到下面的情形:

foo *p = new foo("useful object");
make_use( p );

首先创建一个对象,然后将指针传递给make_use,此时在make_use会对指针做什么呢?make_use会拷贝指针,稍后使用嘛?或者make_use会释放指针吗?我们无法很好的回答这些问题,因为C++不能保证make_use对指针的使用规范,我们只能通过检查代码来保证不会错误的时候指针。这些问题可以通过unique_ptr解决,只保证一份拷贝,不会有其他拷贝。

现在我们使用指针的时候,将其放入到unique_ptr,保证拥有权是唯一的,不会隐式转移,要转移时通过明确的move函数将其转成rvalue,进行转移,此处,使用unique_ptr后,会带来的一点不同是,我们将会传递引用,将其当做值来传递,如下面:

void inc_baz( std::unique_ptr<foo> &p )
{
p->baz++;
}

总结

本文首先介绍了RAII,即Resource Acquisition Is
Initialization
一种资源安全管理的方式,然后引出了smart
point,随后又介绍了auto_ptr,早期对RAII的一种尝试,后来由于其存在的一些问题,通过介绍lvalue、rvalue的概念,最后在C++11中给出了解决方案,unique_ptr,并给出move语义,明确的将左值转换为rvalue,进行控制权的转移。

参考:

http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

http://www.drdobbs.com/cpp/c11-uniqueptr/240002708

http://bartoszmilewski.com/2008/10/18/who-ordered-rvalue-references-part-1/

RAII和unique_ptr,码迷,mamicode.com

时间: 2025-01-06 17:06:18

RAII和unique_ptr的相关文章

使用智能指针来管理对象 (基于RAII)

////一个简单的防止内存泄露的例子//void test() { //使用RAII的特性管理资源 //当智能指针unique_ptr被销毁时,它指向的对象也将被销毁 //这里test函数返回后 p将自动销毁 //unique_ptr<int[]> p( new int[200] ); //直接生成资源 //test函数返回后 p不能被正常销毁,就会造成资源泄露 //int* p = new int[200]; } int main() { while( 1 ) { test(); Sleep

浅谈RAII&智能指针

关于RAII,官方给出的解释是这样的"资源获取就是初始化".听起来貌似不是很懂的哈,其实说的通俗点的话就是它是一种管理资源,避免内存泄漏的一种方法.它可以保证在各种情况下,当你对对象进行使用时先通过构造函数来进行资源的分配和初始化,最后通过析构函数来进行清理,有效的保证了资源的正确分配和释放.(特别是在异常中,因为异常往往会改变代码正确的执行顺序,这就很容易引起资源管理的混乱和内存的泄漏) 其中智能指针就是RAII的一种实现模式,所谓的智能就是它可以自动化的来管理它所指向那份空间的资源

C++ 资源管理(RAII)--智能指针

1. 智能指针(Smart Pointer) i. 是存储指向动态分配(堆)对象指针的类 ii. 在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象 iii. RAII类模拟智能指针,见备注 2. C++11提供了以下几种智能指针,位于头文件<memory>,它们都是模板类 i. std::auto_ptr(复制/赋值) ii. std::unique_ptr  c++11 iii.std::shared_ptr  c++11 iv.std::weak_ptr    c++11

C++中的RAII介绍

摘要 RAII技术被认为是C++中管理资源的最佳方法,进一步引申,使用RAII技术也可以实现安全.简洁的状态管理,编写出优雅的异常安全的代码. 资源管理 RAII是C++的发明者Bjarne Stroustrup提出的概念,RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源.因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会

C++ Primer 学习笔记_56_STL剖析(十一)(原boost库):详解智能指针(unique_ptr(原scoped_ptr) 、shared_ptr 、weak_ptr源码分析)

注意:现在boot库已经归入STL库,用法基本上还和boost类似 在C++11中,引入了智能指针.主要有:unique_ptr, shared_ptr, weak_ptr. 这3种指针组件就是采用了boost里的智能指针方案.很多有用过boost智能指针的朋友,很容易地就能发现它们之间的关间: std boost 功能说明 unique_ptr scoped_ptr 独占指针对象,并保证指针所指对象生命周期与其一致 shared_ptr shared_ptr 可共享指针对象,可以赋值给shar

使用C++11新特性来实现RAII进行资源管理

方法一:借助auto.decltype.unique_ptr.Lambda表达式构造 sqlite3 *db = NULL; auto deleter = [](sqlite3 *pdb){sqlite3_close(pdb);} int nRet = sqlite3_open16(L"F:\\my.db",&db); std::unique_ptr<sqlite3,decltype(deleter)> pdb(db,deleter); if(nRet) {//失败

C++11之 unique_ptr

原文地址为:http://www.drdobbs.com/cpp/c11-uniqueptr/240002708 在C++11中加入了很多的新特性,unique_ptr一枝独秀,对于动态分配的内存对象,它简单有效.虽然它不是万能的,但是它做的已经够好了:利用简单的语法便可以管理动态分配的对象. 基本语法: unique_ptr<T> 是一个模板类,你可以很简单地构造一个unique_ptr的对象,如下: std::unique_ptr<foo> p( new foo(42) );

duang!!!为什么函数可以返回unique_ptr

C++虐我千百遍,我待C++如初恋 从智能指针说起 对高手而言,指针是上天入地的神器:对新手而言,那简直是灾难的源泉.高级语言如Java,C#都自动管理内存,你只管new,不必操心内存释放问题.Bjarne StroustrupC认为++加入垃圾回收机制将做不适合系统底层的开发,为此C++提倡使用RAII来管理资源.auto_ptr就是根据这种理念而诞生的智能指针,本意是想编写个效率接近原生指针,但具有资源所有权安全的智能指针.当发生赋值,拷贝构造时,所有权就发生转移.这就显的很鸡肋,不能放入S

duang!!!为什么函数能够返回unique_ptr

C++虐我千百遍,我待C++如初恋 从智能指针说起 对高手而言.指针是上天入地的神器.对新手而言,那简直是灾难的源泉.高级语言如Java,C#都自己主动管理内存.你仅仅管new.不必担心内存释放问题.Bjarne StroustrupC觉得++增加垃圾回收机制将做不适合系统底层的开发,为此C++提倡使用RAII来管理资源. auto_ptr就是依据这样的理念而诞生的智能指针,本意是想编写个效率接近原生指针,但具有资源全部权安全的智能指针.当发生赋值,拷贝构造时,全部权就发生转移. 这就显的非常鸡