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


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

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

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

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


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现在有一个无效的日志文件。我们可以在堆上构造file,并将指向file的指针传递给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的变体。RAII的意思是资源获取是初始化。智能指针在使用前获取资源(内存),然后在析构函数中自动丢弃它。会发生两件事:

我们总是在使用内存之前分配内存,甚至当我们不喜欢它的时候——这很难用智能指针做另一种方式。如果这没有发生,你将尝试访问NULL内存,导致崩溃(非常痛苦)。 即使出现错误,我们也会释放内存。没有遗留的记忆。

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

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

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

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


前提和理由在概念上很简单。

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

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

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

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

在c++中讨论RAII通常会引出智能指针的讨论,因为指针在清理时特别脆弱。当管理从malloc或new获得的堆分配内存时,通常由程序员负责在指针被销毁之前释放或删除这些内存。智能指针将使用RAII原理来确保在指针变量被销毁时,堆分配的对象被销毁。


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.


对于一个简单但很棒的概念来说,这是个奇怪的名字。更好的名称是范围绑定资源管理(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是一个智能指针,它独占一个对象。它还没有在boost中,但它可能会出现在下一个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可以放入容器中,因为容器将能够保存不可复制(但可移动)的类型,如流和unique_ptr。

Scoped_ptr是一个boost智能指针,既不可复制也不可移动。当你想要确保指针在超出作用域时被删除时,它是一个完美的东西。

代码:

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 (function fx)是共享的,但每个都有一个单独的条目,我们在其上设置颜色。当代码需要引用智能指针所拥有的资源,但不需要拥有该资源时,使用weak_ptr类。你应该创建一个weak_ptr,而不是传递一个原始指针。当它注意到您试图通过weak_ptr访问路径访问资源时,即使不再有shared_ptr拥有该资源,它也会抛出异常。