C++ 中的 RAII 和智能指针

发布于 2024-07-11 01:19:04 字数 244 浏览 8 评论 0原文

在 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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

口干舌燥 2024-07-18 01:19:04

RAII 的一个简单(可能被过度使用)的示例是 File 类。 如果没有 RAII,代码可能看起来像这样:

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

换句话说,我们必须确保在完成文件后立即将其关闭。 这有两个缺点 - 首先,无论我们在哪里使用 File,我们都必须调用 File::close() - 如果我们忘记这样做,我们保存文件的时间就会比我们需要的时间长。 第二个问题是如果在我们关闭文件之前抛出异常怎么办?

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

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

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

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

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

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

这在 Java 中无法完成,因为无法保证对象何时被销毁,因此我们无法保证何时释放文件等资源。

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

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

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

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

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

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

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

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

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

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;

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

快速编辑:正如一些评论所指出的,这个例子并不完美,(至少!)有两个原因。 首先,由于字符串的实现,复制字符串往往很便宜。 其次,由于所谓的命名返回值优化,按值返回可能并不昂贵,因为编译器可以采取一些巧妙的措施来加快速度。

那么,让我们使用 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);
}

不幸的是,这个示例结束得很糟糕 - 一旦此方法结束,文件将被关闭,这意味着 foo和 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);
}

但是谁负责删除文件呢? 如果两者都没有删除文件,那么就会出现内存和资源泄漏。 我们不知道 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 被破坏),文件将自动被删除。

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

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 solves the second problem using a finally clause:

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

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

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

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
}

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

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

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)

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

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.

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

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;

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.

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

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

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

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.

紅太極 2024-07-18 01:19:04

RAII 对于一个简单但很棒的概念来说,这是一个奇怪的名字。 更好的名称是范围限制资源管理 (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);
}

正如你所看到的,我们有很多方法可以被攻击。 我们的想法是将资源管理封装到一个类中。 其对象的初始化获取资源(“资源获取即初始化”)。 当我们退出块(块作用域)时,资源再次被释放。

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

通常,智能指针是 new/delete 的薄包装器,当它们拥有的资源超出范围时,它们恰好会调用 delete 。 一些智能指针,例如shared_ptr,允许您告诉它们一个所谓的删除器,该删除器用于代替delete。 例如,只要您告诉shared_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. It's non-copyable but supports transfer-of-ownership. Some example code (next 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可以放入容器中,因为容器将能够保存不可复制(但可移动)的类型,例如流和unique_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.

代码:

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

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:

代码:

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. 

如您所见,绘图源(函数 fx)是共享的,但每个绘图源都有一个单独的条目,我们可以在其中设置颜色。 当代码需要引用智能指针拥有的资源但不需要拥有该资源时,可以使用weak_ptr 类。 您应该创建一个weak_ptr,而不是传递原始指针。 当它注意到您尝试通过weak_ptr访问路径访问资源时,即使没有shared_ptr再拥有该资源,它也会抛出异常。

RAII This is a strange name for a simple but awesome concept. Better is the name 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:

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. Some smart pointers, like shared_ptr allow you to tell them a so-called deleter, which is used instead of 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.

There are different smart pointers for different purposes:

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. It's non-copyable but supports transfer-of-ownership. Some example code (next C++):

Code:

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

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.

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

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. 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. Instead of passing a raw pointer, you should then create a 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.

生活了然无味 2024-07-18 01:19:04

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

RAII 是一种设计范例,可确保变量在其构造函数中处理所有需要的初始化,并在其析构函数中处理所有需要的清理。这将所有初始化和清理减少到一个步骤。

C++ 不需要 RAII,但越来越多的人认为使用 RAII 方法将生成更健壮的代码。

RAII 在 C++ 中有用的原因是,C++ 在本质上管理变量进入和离开作用域时的创建和销毁,无论是通过正常代码流还是通过异常触发的堆栈展开。 这是 C++ 中的免费赠品。

通过将所有初始化和清理与这些机制联系起来,您可以确保 C++ 也将为您处理这项工作。

在 C++ 中谈论 RAII 通常会引发对智能指针的讨论,因为指针是清理时特别脆弱。 当管理从 malloc 或 new 获取的堆分配内存时,程序员通常有责任在指针被销毁之前释放或删除该内存。 智能指针将使用 RAII 原理来确保在指针变量被销毁时,堆分配的对象也被销毁。

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

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. That's a freebie in 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.

Talking about RAII in C++ usually leads to the discussion of smart pointers, because pointers are particularly fragile when it comes to cleanup. 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. Smart pointers will use the RAII philosophy to ensure that heap allocated objects are destroyed any time the pointer variable is destroyed.

虐人心 2024-07-18 01:19:04

智能指针是 RAII 的变体。 RAII的意思是资源获取即初始化。 智能指针在使用前获取资源(内存),然后在析构函数中自动将其丢弃。 发生了两件事:

  1. 我们总是在使用内存之前分配内存,即使我们不喜欢这样——很难用智能指针做其他的事情。 如果没有发生这种情况,您将尝试访问 NULL 内存,从而导致崩溃(非常痛苦)。
  2. 即使出现错误,我们也会释放内存。 没有任何记忆被悬置。

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

  1. 我们在使用网络套接字之前总是打开它,即使我们不喜欢 - 很难用 RAII 的其他方式来做到这一点。 如果您尝试在没有 RAII 的情况下执行此操作,您可能会打开空套接字,例如 MSN 连接。 那么像“今晚就做吧”之类的消息可能不会被转移,用户也不会被上床,而且你可能会面临被解雇的风险。
  2. 即使出现错误,我们也会关闭网络套接字。 没有套接字挂起,因为这可能会阻止响应消息“肯定会在底部”返回发送者。

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

智能指针的 C++ 源在网络上有数以百万计,包括我上面的回复。

Smart pointer is a variation of RAII. RAII means resource acquisition is initialization. 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).
  2. We free memory even when there's an error. No memory is left hanging.

For instance, another example is network socket 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. If you try doing this without RAII you might open empty socket for, say MSN connection. 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.

C++ sources of smart pointers are in millions around the net including responses above me.

乖不如嘢 2024-07-18 01:19:04

Boost has a number of these including the ones in Boost.Interprocess for shared memory. 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.

岁月打碎记忆 2024-07-18 01:19:04
void foo()
{
   std::string bar;
   //
   // more code here
   //
}

无论发生什么,一旦 foo() 函数的作用域被保留, bar 就会被正确删除。

在内部 std::string 实现通常使用引用计数指针。 因此,只有当字符串的副本之一发生更改时,才需要复制内部字符串。 因此,引用计数智能指针可以仅在必要时复制某些内容。

此外,内部引用计数使得当不再需要内部字符串的副本时可以正确删除内存。

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.

Internally std::string implementations often use reference counted pointers. 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文