在c++的实践中,什么是RAII,什么是智能指针,这些是如何在程序中实现的,以及使用RAII和智能指针的好处是什么?
当前回答
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被销毁),文件将自动删除。
其他回答
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拥有该资源,它也会抛出异常。
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是一种设计范式,用于确保变量在构造函数中处理所有需要的初始化,并在析构函数中处理所有需要的清理。这将所有的初始化和清理减少到一个步骤。
c++不需要RAII,但是越来越多的人认为使用RAII方法可以生成更健壮的代码。
RAII在c++中很有用的原因是,在变量进入和离开作用域时,c++本质上管理变量的创建和销毁,无论是通过正常的代码流还是通过异常触发的堆栈展开。这在c++中是免费的。
通过将所有初始化和清理绑定到这些机制,可以确保c++也会为您处理这些工作。
在c++中讨论RAII通常会引出智能指针的讨论,因为指针在清理时特别脆弱。当管理从malloc或new获得的堆分配内存时,通常由程序员负责在指针被销毁之前释放或删除这些内存。智能指针将使用RAII原理来确保在指针变量被销毁时,堆分配的对象被销毁。
推荐文章
- 如何在Go中使用c++
- 自定义c++分配器的引人注目的例子?
- RAII和c++中的智能指针
- 如何构建和使用谷歌TensorFlow c++ api
- 断言是邪恶的吗?
- 下面这些短语在c++中是什么意思:0 -,default-和value-initialization?
- 在STL地图中,使用map::insert比[]更好吗?
- C++ Linux的想法?
- 如何为Fedora安装g++ ?
- Std::cin输入空格?
- c++标准是否要求iostreams的性能很差,或者我只是在处理一个糟糕的实现?
- gcc在哪里查找C和c++头文件?
- 为什么我们需要require require ?
- 解析c++中的命令行参数?
- 我如何在c++中创建一个随机的字母数字字符串?