C++ 中的 RAII 和智能指针 [英] RAII and smart pointers in C++

查看:26
本文介绍了C++ 中的 RAII 和智能指针的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

In practice with C++, what is RAII, what are smart pointers, how are these implemented in a program and what are the benefits of using RAII with smart pointers?

推荐答案

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

A simple (and perhaps overused) example of RAII is a File class. Without RAII, the code might look something like this:

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

换句话说,我们必须确保在完成文件后将其关闭.这有两个缺点 - 首先,无论我们在哪里使用 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. The second problem is what if an exception is thrown before we close the file?

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

Java solves the second problem using a finally clause:

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

或者从 Java 7 开始,try-with-resource 语句:

or since Java 7, a try-with-resource statement:

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

C++ 使用 RAII 解决了这两个问题——即在 File 的析构函数中关闭文件.只要 File 对象在正确的时间被销毁(无论如何它应该是),关闭文件就会为我们处理.所以,我们的代码现在看起来像:

C++ solves both problems using RAII - that is, closing the file in the destructor of 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. 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

Java 无法做到这一点,因为无法保证对象何时会被销毁,因此我们无法保证文件等资源何时会被释放.

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.

关于智能指针——很多时候,我们只是在堆栈上创建对象.例如(并从另一个答案中窃取示例):

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
}

这很好用——但是如果我们想返回 str 怎么办?我们可以这样写:

This works fine - but what if we want to return str? We could write this:

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

所以,这有什么问题?好吧,返回类型是 std::string - 所以这意味着我们是按值返回的.这意味着我们复制 str 并实际返回副本.这可能很昂贵,我们可能希望避免复制它的成本.因此,我们可能会想出按引用或按指针返回的想法.

So, what's wrong with that? Well, the return type is std::string - so it means we're returning by value. This means that we copy str and actually return the copy. 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;
}

不幸的是,此代码不起作用.我们正在返回一个指向 str 的指针 - 但是 str 是在堆栈上创建的,因此一旦我们退出 foo(),我们就会被删除.换句话说,当调用者获得指针时,它已经没用了(可以说比没用更糟糕,因为使用它可能会导致各种奇怪的错误)

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(). 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)

那么,解决方案是什么?我们可以使用 new 在堆上创建 str - 这样,当 foo() 完成时, str 不会被销毁.

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.

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

当然,这个解决方案也不是完美的.原因是我们创建了str,但我们从来没有删除它.这在非常小的程序中可能不是问题,但一般来说,我们希望确保将其删除.我们可以说调用者一旦完成它就必须删除它.缺点是调用者必须管理内存,这会增加额外的复杂性,并且可能会出错,导致内存泄漏,即即使不再需要也不删除对象.

Of course, this solution isn't perfect either. The reason is that we've created str, but we never delete it. 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 i.e. not deleting object even though it is no longer required.

这就是智能指针的用武之地.以下示例使用 shared_ptr - 我建议您查看不同类型的智能指针以了解您实际想要使用的内容.

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<std::string> foo() {
    shared_ptr<std::string> str = new std::string();
    // Do cool things to or using str
    return str;
}

现在,shared_ptr 将计算对 str 的引用次数.例如

Now, shared_ptr will count the number of references to str. For instance

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

现在有两个对同一个字符串的引用.一旦没有剩余对 str 的引用,它将被删除.因此,您不再需要担心自己删除它.

Now there are two references to the same string. Once there are no remaining references to str, it will be deleted. 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.

那么,让我们使用 File 类尝试不同的示例.

So, let's try a different example using our File class.

假设我们想使用一个文件作为日志.这意味着我们想以仅追加模式打开我们的文件:

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

不幸的是,这个例子结束得很糟糕——这个方法一结束文件就会被关闭,这意味着 foo 和 bar 现在有一个无效的日志文件.我们可以在堆上构造文件,并将指向文件的指针传递给 foo 和 bar:

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. We could construct file on the heap, and pass a pointer to file to both foo and 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 现在有一个无效的指针.

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. For instance, if foo deletes the file before bar has finished with it, bar now has an invalid pointer.

所以,正如您可能已经猜到的,我们可以使用智能指针来帮助我们.

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

现在,没有人需要担心删除文件 - 一旦 foo 和 bar 都完成并且不再有任何对文件的引用(可能是由于 foo 和 bar 被销毁),文件将被自动删除.

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.

这篇关于C++ 中的 RAII 和智能指针的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆