当前位置:  开发笔记 > 编程语言 > 正文

RAII和C++中的智能指针

如何解决《RAII和C++中的智能指针》经验,为你挑选了4个好方法。

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



1> Michael Will..:

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

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

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

在Java中无法做到这一点的原因是我们无法保证何时销毁对象,因此无法保证何时释放诸如文件之类的资源.

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

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

这很好 - 但是如果我们想要返回str呢?我们可以这样写:

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

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

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

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

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

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

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

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

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

假设我们想要将文件用作日志.这意味着我们想要以仅附加模式打开我们的文件:

shared_ptr str = foo();
shared_ptr str2 = 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

不幸的是,这个例子结尾可怕 - 文件将在此方法结束时立即关闭,这意味着foo和bar现在具有无效的日志文件.我们可以在堆上构造文件,并将指向文件的指针传递给foo和bar:

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完成之前删除了该文件,则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被销毁),文件将自动被删除.


返回一个字符串并不是真正使用智能指针的好理由.返回值优化可以轻松优化返回,而c ++ 1x移动语义将完全消除副本(正确使用时).显示一些真实世界的例子(例如,当我们共享相同的资源时):)
应该注意,许多字符串实现是根据引用计数指针实现的.这些写时复制语义使得按值返回字符串非常便宜.
即使对于那些没有的,许多编译器也会实现NRV优化,这将解决开销问题.一般来说,我发现shared_ptr很少有用 - 只需坚持使用RAII并避免共享所有权.
@Nemanja Trifunovic:在这种情况下,RAII是指在堆栈中返回副本/创建对象?如果您返回/接受可以进行子类化的类型的对象,那么这不起作用.然后你必须使用一个指针来避免切片对象,我认为在这些情况下,智能指针通常比原始指针好.

2> Johannes Sch..:

RAII这是一个简单但令人敬畏的概念的奇怪名称.更好的是Scope Bound Resource Management(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 create_entry(Parameters p) {
    shared_ptr e(Entry::createEntry(p), &Entry::freeEntry);
    return e;
}

通常,智能指针是new/delete周围的瘦包装器,delete当它们拥有的资源超出范围时恰好调用它.一些智能指针,比如shared_ptr,允许你告诉他们一个所谓的删除器,而不是使用它delete.例如,只要告诉shared_ptr关于正确的删除器,就可以管理窗口句柄,正则表达式资源和其他任意内容.

有不同的智能指针用于不同的目的:

的unique_ptr

是一个智能指针,专门拥有一个对象.它不是在提升,但它可能会出现在下一个C++标准中.它是不可复制的,但支持所有权转让.一些示例代码(下一个C++):

码:

unique_ptr p(new plot_src); // now, p owns
unique_ptr u(move(p)); // now, u owns, p owns nothing.
unique_ptr v(u); // error, trying to copy u

vector> 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 sp(new pipe);
    // do something here...
} // when going out of scope, sp will delete the pointer automatically. 

shared_ptr的

是为了共享所有权.因此,它既可复制又可移动.多个智能指针实例可以拥有相同的资源.一旦拥有资源的最后一个智能指针超出范围,资源就会被释放.我的一个项目的一些现实世界的例子:

码:

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



3> Drew Dormann..:

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

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

C++不需要RAII,但越来越多的人认为使用RAII方法会产生更强大的代码.

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

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

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



4> mannicken..:

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

    我们在使用它之前总是分配内存,即使我们不喜欢它 - 用智能指针做另一种方式很难.如果没有发生这种情况,您将尝试访问NULL内存,从而导致崩溃(非常痛苦).

    即使出现错误,我们也会释放内存.没有记忆悬空.

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

    我们在使用之前打开网络套接字,总是,即使我们不喜欢这样 - 用RAII做另一种方式很难.如果您尝试在没有RAII的情况下执行此操作,则可能会打开空插槽,例如MSN连接.那么像今晚"让我们这样做"的消息可能不会被转移,用户也不会被放下,你可能会被解雇.

    即使出现错误,我们也会关闭网络套接字.没有任何套接字被挂起,因为这可能会阻止响应消息"肯定会在底部"发送回来.

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

智能指针的C++来源在网络上有数百万,包括我之上的响应.

推荐阅读
和谐啄木鸟
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有