RAII和C ++中的智能指针

在C ++实践中,什么是RAII ,什么是智能指针 ,它们是如何在程序中实现的,以及使用RAII和智能指针有什么好处?

一个简单的(也许是过度使用的)RAII的例子是一个File类。 没有RAII,代码可能如下所示:

File file("/path/to/file"); // Do stuff with file file.close(); 

换句话说,我们必须确保在完成文件后closures文件。 这有两个缺点 – 首先,无论我们在哪里使用File,都必须调用File :: close() – 如果我们忘记这么做的话,我们会把文件放在比我们需要的更长的时间。 第二个问题是如果在我们closures文件之前抛出一个exception呢?

Java使用finally子句解决了第二个问题:

 try { File file = new File("/path/to/file"); // Do stuff with file } finally { file.close(); } 

C ++使用RAII解决了这两个问题 – 也就是在File的析构函数中closures文件。 只要File对象在正确的时间被销毁(它应该是无论如何),closures文件是为我们照顾的。 所以,我们的代码现在看起来像这样:

 File file("/path/to/file"); // Do stuff with file // No need to close it - destructor will do that for us 

在Java中无法完成的原因是我们无法保证对象何时被销毁,所以不能保证何时会释放文件等资源。

在智能指针上 – 很多时候,我们只是在堆栈上创build对象。 例如(并从另一个答案窃取示例):

 void foo() { std::string str; // Do cool things to or using str } 

这工作正常 – 但如果我们想要返回str呢? 我们可以写这个:

 std::string foo() { std::string str; // Do cool things to or using str return str; } 

那么,这有什么问题? 那么,返回types是std :: string – 这意味着我们正在返回值。 这意味着我们复制str并实际返回副本。 这可能是昂贵的,我们可能想要避免复制它的成本。 因此,我们可能会想出通过引用或指针返回的想法。

 std::string* foo() { std::string str; // Do cool things to or using str return &str; } 

不幸的是,这段代码不起作用。 我们正在返回一个指向str的指针 – 但是str是在堆栈上创build的,所以我们一旦退出foo()就会被删除。 换句话说,当调用者获得指针的时候,这是无用的(并且可以说比使用它更糟糕,因为使用它会导致各种各样的时髦错误)

那么,解决scheme是什么? 我们可以使用新的方法在堆上创buildstr – 这样,当foo()完成时,str将不会被销毁。

 std::string* foo() { std::string* str = new std::string(); // Do cool things to or using str return str; } 

当然,这个解决scheme也不完美。 原因是我们已经创build了str,但是我们从不删除它。 这在一个非常小的程序中可能不是问题,但总的来说,我们要确保将其删除。 我们可以说,调用者在完成对象之后必须删除对象。 缺点是调用者必须pipe理内存,这增加了额外的复杂性,并可能导致错误,导致内存泄漏,即使不再需要删除对象。

这是智能指针进来的地方。下面的例子使用shared_ptr – 我build议你看看不同types的智能指针来学习你真正想使用的。

 shared_ptr<std::string> foo() { shared_ptr<std::string> str = new std::string(); // Do cool things to or using str return str; } 

现在,shared_ptr会计算str的引用数量。 例如

 shared_ptr<std::string> str = foo(); shared_ptr<std::string> str2 = str; 

现在有两个引用相同的string。 一旦没有剩余的str的引用,它将被删除。 因此,您不必再担心自己删除它。

快速编辑:正如一些评论指出的,这个例子并不完美(至less!)两个原因。 首先,由于string的实现,复制一个string往往是廉价的。 其次,由于所谓的返回值优化,因为编译器可以做一些巧妙的事情来加快速度,所以按值返回可能并不昂贵。

所以,让我们使用我们的File类来尝试一个不同的例子。

假设我们想用文件作为日志。 这意味着我们想要以附加模式打开我们的文件:

 File file("/path/to/file", File::append); // The exact semantics of this aren't really important, // just that we've got a file to be used as a log 

现在,我们将我们的文件设置为其他几个对象的日志:

 void setLog(const Foo & foo, const Bar & bar) { File file("/path/to/file", File::append); foo.setLogFile(file); bar.setLogFile(file); } 

不幸的是,这个例子结束了可怕的 – 文件将被closures一旦这个方法结束,这意味着foo和酒吧现在有一个无效的日志文件。 我们可以在堆上构build文件,并将指向文件的指针传递给foo和bar:

 void setLog(const Foo & foo, const Bar & bar) { File* file = new File("/path/to/file", File::append); foo.setLogFile(file); bar.setLogFile(file); } 

但是,谁负责删除文件? 如果既不删除文件,也有内存和资源泄漏。 我们不知道foo或bar是否会先用文件结束,所以我们不能期望自己删除文件。 例如,如果foo在bar完成之前删除文件,bar现在有一个无效的指针。

所以,正如你可能已经猜到的那样,我们可以使用智能指针来帮助我们。

 void setLog(const Foo & foo, const Bar & bar) { shared_ptr<File> file = new File("/path/to/file", File::append); foo.setLogFile(file); bar.setLogFile(file); } 

现在,没有人需要担心删除文件 – 一旦foo和bar都完成,不再有任何文件的引用(可能是由于foo和bar被破坏),文件将被自动删除。

RAII这是一个简单但令人敬畏的概念的一个奇怪的名字。 更好的是名称范围绑定资源pipe理 (SBRM)。 这个想法是,你经常碰巧在块的开始分配资源,并且需要在块的出口处释放资源。 退出块可能会发生在正常的stream量控制下,跳出来,甚至出现exception。 为了涵盖所有这些情况,代码变得更加复杂和冗余。

只是一个没有SBRM的例子:

 void o_really() { resource * r = allocate_resource(); try { // something, which could throw. ... } catch(...) { deallocate_resource(r); throw; } if(...) { return; } // oops, forgot to deallocate deallocate_resource(r); } 

正如你所看到的,我们有很多方法可以实现。 这个想法是我们把资源pipe理封装到一个类中。 其对象的初始化获取资源(“资源获取初始化”)。 当我们退出块(块范围)时,资源再次被释放。

 struct resource_holder { resource_holder() { r = allocate_resource(); } ~resource_holder() { deallocate_resource(r); } resource * r; }; void o_really() { resource_holder r; // something, which could throw. ... if(...) { return; } } 

如果你有自己的课程,这不仅仅是为了分配/取消分配资源的目的。 分配只是让他们完成工作的额外关切。 但是,只要你想分配/取消分配资源,以上变得不方便。 你必须为你获得的每种资源编写一个包装类。 为了缓解这个问题,智能指针可以让你自动执行这个过程:

 shared_ptr<Entry> create_entry(Parameters p) { shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry); return e; } 

通常情况下,智能指针是新/删除周围的简单包装,当它们拥有的资源超出范围时,恰好会发生delete 。 一些智能指针,像shared_ptr允许你告诉他们所谓的删除,这是用来代替delete 。 例如,你可以pipe理窗口句柄,正则expression式资源和其他任意的东西,只要你告诉shared_ptr关于正确的删除器。

有不同的智能指针用于不同的目的:

的unique_ptr

是一个专门拥有一个对象的智能指针。 它没有提升,但可能会出现在下一个C ++标准中。 这是不可复制的,但支持所有权转让 。 一些示例代码(下一个C ++):

码:

 unique_ptr<plot_src> p(new plot_src); // now, p owns unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing. unique_ptr<plot_src> v(u); // error, trying to copy u vector<unique_ptr<plot_src>> pv; pv.emplace_back(new plot_src); pv.emplace_back(new plot_src); 

与auto_ptr不同的是,unique_ptr可以放在一个容器中,因为容器可以容纳不可复制的(但是可移动的)types,比如streams和unique_ptr。

使用scoped_ptr

是一个既不可复制也不可移动的智能提升指针。 当你想要确保指针在超出范围时被删除,这是一个完美的东西。

码:

 void do_something() { scoped_ptr<pipe> sp(new pipe); // do something here... } // when going out of scope, sp will delete the pointer automatically. 

shared_ptr的

是共享所有权。 因此,它是可复制和可移动的。 多个智能指针实例可以拥有相同的资源。 只要拥有资源的最后一个智能指针超出范围,资源就会被释放。 我的一个项目的一个真实世界的例子:

码:

 shared_ptr<plot_src> p(new plot_src(&fx)); plot1->add(p)->setColor("#00FF00"); plot2->add(p)->setColor("#FF0000"); // if p now goes out of scope, the src won't be freed, as both plot1 and // plot2 both still have references. 

正如你所看到的,plot-source(函数fx)是共享的,但是每一个都有一个单独的条目,我们在其中设置颜色。 有一个weak_ptr类用于代码需要引用智能指针所拥有的资源,但不需要拥有资源。 而不是传递一个原始的指针,然后你应该创build一个weak_ptr。 当它通知你试图通过weak_ptr访问path访问资源时,即使没有拥有资源的shared_ptr,它也会抛出exception。

前提和原因很简单,概念上。

RAII是确保variables处理构造函数中所有需要的初始化的devise范例, 并且在析构函数中都需要清理。 这将所有的初始化和清理操作都减less到一个步骤。

C ++不需要RAII,但越来越多的人认为使用RAII方法会产生更强大的代码。

RAII在C ++中很有用的原因是C ++在进入和离开作用域时,通过正常的代码stream或通过由exception触发的堆栈展开来本质上pipe理variables的创build和销毁。 这是一个免费的C ++。

通过将所有的初始化和清理绑定到这些机制,您可以确保C ++也能为您处理这项工作。

在C ++中谈论RAII通常会导致对智能指针的讨论,因为指针在清理时特别脆弱。 在pipe理从malloc或new获取的堆分配内存时,程序员通常负责在指针销毁之前释放或删除该内存。 智能指针将使用RAII哲学来确保指针variables被销毁时堆分配的对象被销毁。

智能指针是RAII的变体。 RAII意味着资源获取是初始化。 智能指针在使用前获取资源(内存),然后在析构函数中自动抛出。 有两件事情发生:

  1. 在使用之前,我们会分配内存 ,即使我们不喜欢它,也很难用智能指针来做另一种方式。 如果这没有发生,你将尝试访问NULL内存,导致崩溃(非常痛苦)。
  2. 即使出现错误,我们也可以释放内存 。 没有记忆被留下。

例如,另一个例子是networking套接字RAII。 在这种情况下:

  1. 在我们使用之前,我们会打开networking套接字 ,即使我们不这么认为 – 使用RAII很难做到这一点。 如果您尝试在没有RAII的情况下执行此操作,则可能会打开空连接,例如MSN连接。 那么像“今晚就这样做”的消息可能不会被转移,用户不会被打破,你可能会被解雇。
  2. 即使出现错误,我们也会closuresnetworking套接字 。 没有sockets被挂起,因为这可能会阻止响应消息“肯定不好”在打回发送。

现在,正如你所看到的,RAII在大多数情况下是一个非常有用的工具,因为它可以帮助人们奠定基础。

智能指针的C ++源在networking中有数以百万计,包括我之上的响应。

Boost有一些包括在Boost.Interprocess中的共享内存。 这极大地简化了内存pipe理,尤其是当你有5个进程共享相同的数据结构时,头痛的情况下:当每个人都完成了大量的内存,你希望它自动获得释放,而不必坐在那里试图找出谁应该负责在一块内存上调用delete ,以免最后发生内存泄漏,或者被错误地释放了两次,可能会损坏整个堆的指针。

 void foo()
 {
    std :: string bar;
    //
    //更多代码在这里
    //
 }

无论发生什么情况,只要foo()函数的作用域被留下,bar就会被正确删除。

内部的std :: string实现通常使用引用计数的指针。 所以当string的一个副本改变时,只需要复制内部string。 因此,引用计数的智能指针可以在必要时仅复制某些内容。

此外,内部引用计数可以在内部string的副本不再需要时正确删除内存。