代码之家  ›  专栏  ›  技术社区  ›  Rob Kam Bayard Randel

C++中的RAII和智能指针

  •  189
  • Rob Kam Bayard Randel  · 技术社区  · 17 年前

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

    6 回复  |  直到 6 年前
        1
  •  331
  •   Michael Williamson    7 年前

    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
    }
    

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

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

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

    快速编辑:正如一些评论所指出的,这个例子并不完美,至少有两个原因。首先,由于字符串的实现,复制字符串的成本往往较低。其次,由于所谓的返回值优化,按值返回可能并不昂贵,因为编译器可以做一些聪明的事情来加快速度。

    假设我们想使用一个文件作为日志。这意味着我们希望以仅附加模式打开文件:

    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被销毁),文件将自动删除。

        2
  •  144
  •   Johannes Schaub - litb    17 年前

    拉伊 对于一个简单但令人敬畏的概念来说,这是一个奇怪的名字。名字更好 范围限制资源管理

    仅举一个不使用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正确的删除器。

    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可以放在容器中,因为容器可以保存不可复制(但可移动)的类型,比如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. 
    

    如您所见,打印源(函数fx)是共享的,但每个源都有一个单独的条目,我们在该条目上设置颜色。当代码需要引用智能指针所拥有的资源,但不需要拥有该资源时,可以使用弱_ptr类。然后,您应该创建弱\u ptr,而不是传递原始指针。当它注意到您试图通过弱\u ptr访问路径访问资源时,它将引发异常,即使不再有共享\u ptr拥有该资源。

        3
  •  32
  •   Drew Dormann    17 年前

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

    RAII是确保 这将所有初始化和清理简化为一个步骤。

    C++不需要RAII,但是越来越多地接受使用RAII方法将产生更健壮的代码。

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

        4
  •  8
  •   mannicken    17 年前

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

    1. 在我们使用它之前,总是,即使我们不喜欢它——用智能指针很难用另一种方式。如果没有发生这种情况,您将尝试访问空内存,导致崩溃(非常痛苦)。
    2. 记忆力 即使有错误。没有内存被挂起。

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

    1. 我们开 在我们使用它之前,总是,甚至当我们不想的时候——很难用另一种方式使用RAII。如果您尝试在没有RAII的情况下执行此操作,您可能会打开空套接字,例如MSN连接。然后像“今晚就这么做吧”这样的信息可能不会被传输,用户也不会被解雇,你可能会被炒鱿鱼。
    2. 我们关门 即使有错误。没有任何套接字保持挂起状态,因为这可能会阻止响应消息“sure ill on bottom”(确定将在底部)回击发送方。

    智能指针的C++源在网络上的数百万人,包括我上面的回答。

        5
  •  2
  •   Jason S    17 年前

    Boost有很多这样的功能,包括 Boost.Interprocess 用于共享内存。它极大地简化了内存管理,特别是在令人头痛的情况下,例如当您有5个进程共享相同的数据结构时:当每个人都使用完一块内存时,您希望它自动释放&不必坐在那里试图找出谁应该负责打电话 delete 在内存块上,以防最终出现内存泄漏或指针错误释放两次,从而损坏整个堆。

        6
  •  0
  •   Juan Juan    17 年前
    void foo()
    {
       std::string bar;
       //
       // more code here
       //
    }
    

    不管发生什么情况,一旦foo()函数的作用域被留下,bar将被正确删除。

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

    推荐文章