简体   繁体   English

RAII和C ++中的智能指针

[英]RAII and smart pointers in C++

在使用C ++的实践中,什么是RAII ,什么是智能指针 ,如何在程序中实现这些以及将RAII与智能指针一起使用有什么好处?

A simple (and perhaps overused) example of RAII is a File class. RAII的一个简单(也许是过度使用)的例子是File类。 Without RAII, the code might look something like this: 没有RAII,代码可能看起来像这样:

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

In other words, we must make sure that we close the file once we've finished with it. 换句话说,我们必须确保在完成文件后关闭文件。 This has two drawbacks - firstly, wherever we use File, we will have to called File::close() - if we forget to do this, we're holding onto the file longer than we need to. 这有两个缺点 - 首先,无论我们在哪里使用File,我们都必须调用File :: close() - 如果我们忘记这样做,我们将保留文件的时间比我们需要的长。 The second problem is what if an exception is thrown before we close the file? 第二个问题是如果在关闭文件之前抛出异常会怎样?

Java solves the second problem using a finally clause: Java使用finally子句解决了第二个问题:

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

or since Java 7, a try-with-resource statement: 或者从Java 7开始,尝试使用资源语句:

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

C++ solves both problems using RAII - that is, closing the file in the destructor of File. C ++使用RAII解决了这两个问题 - 也就是说,在File的析构函数中关闭文件。 So long as the File object is destroyed at the right time (which it should be anyway), closing the file is taken care of for us. 只要File对象在正确的时间被销毁(无论如何都应该被销毁),关闭文件将由我们处理。 So, our code now looks something like: 所以,我们的代码现在看起来像:

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

This cannot be done in Java since there's no guarantee when the object will be destroyed, so we cannot guarantee when a resource such as file will be freed. 这不能在Java中完成,因为无法保证何时销毁对象,因此我们无法保证何时释放诸如文件之类的资源。

Onto smart pointers - a lot of the time, we just create objects on the stack. 在智能指针上 - 很多时候,我们只是在堆栈上创建对象。 For instance (and stealing an example from another answer): 例如(并从另一个答案窃取一个例子):

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

This works fine - but what if we want to return str? 这很好 - 但是如果我们想要返回str呢? We could write this: 我们可以这样写:

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

So, what's wrong with that? 那么,那有什么不对? Well, the return type is std::string - so it means we're returning by value. 好吧,返回类型是std :: string - 所以这意味着我们按值返回。 This means that we copy str and actually return the copy. 这意味着我们复制str并实际返回副本。 This can be expensive, and we might want to avoid the cost of copying it. 这可能很昂贵,我们可能希望避免复制它的成本。 Therefore, we might come up with idea of returning by reference or by pointer. 因此,我们可能想出通过引用或指针返回的想法。

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

Unfortunately, this code doesn't work. 不幸的是,这段代码不起作用。 We're returning a pointer to str - but str was created on the stack, so we be deleted once we exit foo(). 我们正在返回一个指向str的指针 - 但str是在堆栈上创建的,所以一旦我们退出foo()就会被删除。 In other words, by the time the caller gets the pointer, it's useless (and arguably worse than useless since using it could cause all sorts of funky errors) 换句话说,当调用者获得指针时,它是无用的(并且可能比无用更糟,因为使用它可能会导致各种各样的时髦错误)

So, what's the solution? 那么,解决方案是什么? We could create str on the heap using new - that way, when foo() is completed, str won't be destroyed. 我们可以使用new在堆上创建str - 这样,当foo()完成时,str不会被销毁。

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

Of course, this solution isn't perfect either. 当然,这种解决方案也不完美。 The reason is that we've created str, but we never delete it. 原因是我们创建了str,但我们从不删除它。 This might not be a problem in a very small program, but in general, we want to make sure we delete it. 这可能不是一个非常小的程序中的问题,但一般来说,我们希望确保删除它。 We could just say that the caller must delete the object once he's finished with it. 我们可以说调用者必须在完成后删除该对象。 The downside is that the caller has to manage memory, which adds extra complexity, and might get it wrong, leading to a memory leak ie not deleting object even though it is no longer required. 缺点是调用者必须管理内存,这会增加额外的复杂性,并且可能会出错,导致内存泄漏,即不再删除对象,即使不再需要它。

This is where smart pointers come in. The following example uses shared_ptr - I suggest you look at the different types of smart pointers to learn what you actually want to use. 这是智能指针的用武之地。以下示例使用shared_ptr - 我建议您查看不同类型的智能指针,以了解您实际想要使用的内容。

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

Now, shared_ptr will count the number of references to str. 现在,shared_ptr将计算str的引用数。 For instance 例如

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

Now there are two references to the same string. 现在有两个对同一个字符串的引用。 Once there are no remaining references to str, it will be deleted. 一旦没有对str的剩余引用,它将被删除。 As such, you no longer have to worry about deleting it yourself. 因此,您不必再担心自己删除它。

Quick edit: as some of the comments have pointed out, this example isn't perfect for (at least!) two reasons. 快速编辑:正如一些评论所指出的那样,这个例子并不完美(至少!)两个原因。 Firstly, due to the implementation of strings, copying a string tends to be inexpensive. 首先,由于字符串的实现,复制字符串往往是便宜的。 Secondly, due to what's known as named return value optimisation, returning by value may not be expensive since the compiler can do some cleverness to speed things up. 其次,由于所谓的命名返回值优化,按值返回可能并不昂贵,因为编译器可以做一些聪明来加快速度。

So, let's try a different example using our File class. 所以,让我们尝试使用File类的不同示例。

Let's say we want to use a file as a log. 假设我们想要将文件用作日志。 This means we want to open our file in append only mode: 这意味着我们想要以仅附加模式打开我们的文件:

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

Now, let's set our file as the log for a couple of other objects: 现在,让我们将文件设置为其他几个对象的日志:

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

Unfortunately, this example ends horribly - file will be closed as soon as this method ends, meaning that foo and bar now have an invalid log file. 不幸的是,这个例子结尾可怕 - 文件将在此方法结束时立即关闭,这意味着foo和bar现在具有无效的日志文件。 We could construct file on the heap, and pass a pointer to file to both foo and bar: 我们可以在堆上构造文件,并将指向文件的指针传递给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);
}

But then who is responsible for deleting file? 但是谁负责删除文件? If neither delete file, then we have both a memory and resource leak. 如果既不删除文件,那么我们既有内存又有资源泄漏。 We don't know whether foo or bar will finish with the file first, so we can't expect either to delete the file themselves. 我们不知道foo或bar是否会先完成文件,所以我们不能指望自己删除文件。 For instance, if foo deletes the file before bar has finished with it, bar now has an invalid pointer. 例如,如果foo在bar完成之前删除了该文件,则bar现在具有无效指针。

So, as you may have guessed, we could use smart pointers to help us out. 所以,正如您可能已经猜到的那样,我们可以使用智能指针来帮助我们。

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);
}

Now, nobody needs to worry about deleting file - once both foo and bar have finished and no longer have any references to file (probably due to foo and bar being destroyed), file will automatically be deleted. 现在,没有人需要担心删除文件 - 一旦foo和bar都完成并且不再有任何文件引用(可能是由于foo和bar被销毁),文件将自动被删除。

RAII This is a strange name for a simple but awesome concept. RAII这是一个简单但令人敬畏的概念的奇怪名称。 Better is the name Scope Bound Resource Management (SBRM). 更好的是Scope Bound Resource Management (SBRM)。 The idea is that often you happen to allocate resources at the begin of a block, and need to release it at the exit of a block. 我们的想法是,您经常在块的开头分配资源,并且需要在块的出口处释放它。 Exiting the block can happen by normal flow control, jumping out of it, and even by an exception. 退出块可以通过正常的流量控制,跳出它,甚至是异常来实现。 To cover all these cases, the code becomes more complicated and redundant. 为了涵盖所有这些情况,代码变得更加复杂和冗余。

Just an example doing it without SBRM: 只是一个没有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);
}

As you see there are many ways we can get pwned. 如你所见,我们可以通过很多方式进行实践。 The idea is that we encapsulate the resource management into a class. 我们的想法是将资源管理封装到一个类中。 Initialization of its object acquires the resource ("Resource Acquisition Is Initialization"). 其对象的初始化获取资源(“资源获取是初始化”)。 At the time we exit the block (block scope), the resource is freed again. 在我们退出块(块范围)时,资源再次被释放。

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; }
}

That is nice if you have got classes of their own which are not solely for the purpose of allocating/deallocating resources. 如果你有自己的类,这不仅仅是为了分配/解除分配资源的目的,这是很好的。 Allocation would just be an additional concern to get their job done. 分配只是他们完成工作的另一个问题。 But as soon as you just want to allocate/deallocate resources, the above becomes unhandy. 但是,只要您想分配/解除分配资源,上述内容就变得不合适了。 You have to write a wrapping class for every sort of resource you acquire. 您必须为您获得的每种资源编写一个包装类。 To ease that, smart pointers allow you to automate that process: 为了简化这一点,智能指针允许您自动执行该过程:

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

Normally, smart pointers are thin wrappers around new / delete that just happen to call delete when the resource they own goes out of scope. 通常,智能指针是new / delete周围的瘦包装器,当它们拥有的资源超出范围时恰好会调用delete Some smart pointers, like shared_ptr allow you to tell them a so-called deleter, which is used instead of delete . 一些智能指针,比如shared_ptr,允许你告诉他们一个所谓的删除器,它被用来代替delete That allows you, for instance, to manage window handles, regular expression resources and other arbitrary stuff, as long as you tell shared_ptr about the right deleter. 例如,只要告诉shared_ptr关于正确的删除器,就可以管理窗口句柄,正则表达式资源和其他任意内容。

There are different smart pointers for different purposes: 有不同的智能指针用于不同的目的:

unique_ptr 的unique_ptr

is a smart pointer which owns an object exclusively. 是一个智能指针,专门拥有一个对象。 It's not in boost, but it will likely appear in the next C++ Standard. 它不是在提升,但它可能会出现在下一个C ++标准中。 It's non-copyable but supports transfer-of-ownership . 它是不可复制的,但支持所有权转让 Some example code (next C++): 一些示例代码(下一个C ++):

Code: 码:

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

Unlike auto_ptr, unique_ptr can be put into a container, because containers will be able to hold non-copyable (but movable) types, like streams and unique_ptr too. 与auto_ptr不同,unique_ptr可以放入容器中,因为容器将能够保存不可复制(但可移动)的类型,例如streams和unique_ptr。

scoped_ptr 使用scoped_ptr

is a boost smart pointer which is neither copyable nor movable. 是一个提升智能指针,既不可复制也不可移动。 It's the perfect thing to be used when you want to make sure pointers are deleted when going out of scope. 当你想要确保指针在超出范围时被删除时,这是一个完美的选择。

Code: 码:

 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的

is for shared ownership. 是为了共享所有权。 Therefor, it's both copyable and movable. 因此,它既可复制又可移动。 Multiple smart pointer instances can own the same resource. 多个智能指针实例可以拥有相同的资源。 As soon as the last smart pointer owning the resource goes out of scope, the resource will be freed. 一旦拥有资源的最后一个智能指针超出范围,资源就会被释放。 Some real world example of one of my projects: 我的一个项目的一些现实世界的例子:

Code: 码:

 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. 

As you see, the plot-source (function fx) is shared, but each one has a separate entry, on which we set the color. 如您所见,plot-source(函数fx)是共享的,但每个都有一个单独的条目,我们在其上设置颜色。 There is a weak_ptr class which is used when code needs to refer to the resource owned by a smart pointer, but doesn't need to own the resource. 有一个weak_ptr类,当代码需要引用智能指针所拥有的资源时使用,但不需要拥有该资源。 Instead of passing a raw pointer, you should then create a weak_ptr. 您应该创建一个weak_ptr,而不是传递原始指针。 It will throw an exception when it notices you try to access the resource by an weak_ptr access path, even though there is no shared_ptr anymore owning the resource. 当它注意到你试图通过weak_ptr访问路径访问资源时会抛出异常,即使没有shared_ptr拥有该资源。

The premise and reasons are simple, in concept. 在概念上,前提和原因很简单。

RAII is the design paradigm to ensure that variables handle all needed initialization in their constructors and all needed cleanup in their destructors. RAII是一种设计范例,用于确保变量在其构造函数中处理所有需要的初始化,并在其析构函数中处理所有需要的清理。 This reduces all initialization and cleanup to a single step. 这将所有初始化和清理减少到一个步骤。

C++ does not require RAII, but it is increasingly accepted that using RAII methods will produce more robust code. C ++不需要RAII,但越来越多的人认为使用RAII方法会产生更强大的代码。

The reason that RAII is useful in C++ is that C++ intrinsically manages the creation and destruction of variables as they enter and leave scope, whether through normal code flow or through stack unwinding triggered by an exception. RAII在C ++中有用的原因是C ++在进入和离开作用域时,无论是通过正常的代码流还是通过异常触发的堆栈展开,本质上都会管理变量的创建和销毁。 That's a freebie in C++. 这是C ++中的免费赠品。

By tying all initialization and cleanup to these mechanisms, you are ensured that C++ will take care of this work for you as well. 通过将所有初始化和清理与这些机制联系起来,您可以确保C ++也将为您处理这项工作。

Talking about RAII in C++ usually leads to the discussion of smart pointers, because pointers are particularly fragile when it comes to cleanup. 在C ++中谈论RAII通常会导致对智能指针的讨论,因为指针在清理时特别脆弱。 When managing heap-allocated memory acquired from malloc or new, it is usually the responsibility of the programmer to free or delete that memory before the pointer is destroyed. 当管理从malloc或new获取的堆分配的内存时,程序员通常负责在销毁指针之前释放或删除该内存。 Smart pointers will use the RAII philosophy to ensure that heap allocated objects are destroyed any time the pointer variable is destroyed. 智能指针将使用RAII原理确保在销毁指针变量时销毁堆分配的对象。

Smart pointer is a variation of RAII. 智能指针是RAII的变体。 RAII means resource acquisition is initialization. RAII意味着资源获取是初始化。 Smart pointer acquires a resource (memory) before usage and then throws it away automatically in a destructor. 智能指针在使用之前获取资源(内存),然后在析构函数中自动将其抛出。 Two things happen: 发生了两件事:

  1. We allocate memory before we use it, always, even when we don't feel like it -- it's hard to do another way with a smart pointer. 我们在使用它之前总是分配内存 ,即使我们不喜欢它 - 用智能指针做另一种方式很难。 If this wasn't happening you will try to access NULL memory, resulting in a crash (very painful). 如果没有发生这种情况,您将尝试访问NULL内存,从而导致崩溃(非常痛苦)。
  2. We free memory even when there's an error. 即使出现错误,我们也会释放内存 No memory is left hanging. 没有记忆悬空。

For instance, another example is network socket RAII. 例如,另一个例子是网络套接字RAII。 In this case: 在这种情况下:

  1. We open network socket before we use it,always, even when we don't feel like -- it's hard to do it another way with RAII. 我们在使用之前打开网络套接字 ,总是,即使我们不喜欢这样 - 用RAII做另一种方式很难。 If you try doing this without RAII you might open empty socket for, say MSN connection. 如果您尝试在没有RAII的情况下执行此操作,则可能会打开空插槽,例如MSN连接。 Then message like "lets do it tonight" might not get transferred, users will not get laid, and you might risk getting fired. 那么像今晚“让我们这样做”的消息可能不会被转移,用户也不会被放下,你可能会被解雇。
  2. We close network socket even when there's an error. 即使出现错误,我们也会关闭网络套接字 No socket is left hanging as this might prevent the response message "sure ill be on bottom" from hitting sender back. 没有任何套接字被挂起,因为这可能会阻止响应消息“肯定会在底部”发送回来。

Now, as you can see, RAII is a very useful tool in most cases as it helps people to get laid. 现在,正如您所看到的,RAII在大多数情况下是一个非常有用的工具,因为它可以帮助人们铺设。

C++ sources of smart pointers are in millions around the net including responses above me. 智能指针的C ++来源在网络上有数百万,包括我之上的响应。

Boost has a number of these including the ones in Boost.Interprocess for shared memory. Boost有许多这些,包括Boost.Interprocess中的共享内存。 It greatly simplifies memory management, especially in headache-inducing situations like when you have 5 processes sharing the same data structure: when everyone's done with a chunk of memory, you want it to automatically get freed & not have to sit there trying to figure out who should be responsible for calling delete on a chunk of memory, lest you end up with a memory leak, or a pointer which is mistakenly freed twice and may corrupt the whole heap. 它极大地简化了内存管理,特别是在令人头疼的情况下,例如当你有5个进程共享相同的数据结构时:当每个人都完成一大块内存时,你希望它自动获得释放而不必坐在那里试图找出谁应该负责在一块内存上调用delete ,以免最终导致内存泄漏,或者指针被错误地释放两次并可能破坏整个堆。

void foo()
{
   std::string bar;
   //
   // more code here
   //
}

No matter what happens, bar is going to be properly deleted once the scope of the foo() function has been left behind. 无论发生什么,一旦foo()函数的范围被遗忘,bar将被正确删除。

Internally std::string implementations often use reference counted pointers. 内部std :: string实现经常使用引用计数指针。 So the internal string only needs to be copied when one of the copies of the strings changed. 因此,只有在其中一个字符串副本发生更改时才需要复制内部字符串。 Therefore a reference counted smart pointer makes it possible to only copy something when necessary. 因此,引用计数智能指针使得可以在必要时仅复制某些内容。

In addition, the internal reference counting makes it possible that the memory will be properly deleted when the copy of the internal string is no longer needed. 此外,内部引用计数使得在不再需要内部字符串的副本时可以正确删除内存。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM