在实践中使用C ++,什么是RAII,什么是smart pointers,这些在程序中是如何实现的?使用RAII和智能指针有什么好处?
答案 0 :(得分:311)
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对象在正确的时间被销毁(无论如何都应该被销毁),关闭文件将由我们处理。所以,我们的代码现在看起来像:
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被销毁),文件将自动被删除。
答案 1 :(得分:139)
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;
}
通常情况下,智能指针是新的/删除的瘦包装器,当它们拥有的资源超出范围时恰好调用delete
。一些智能指针,比如shared_ptr,允许你告诉他们一个所谓的删除器,它被用来代替delete
。例如,只要告诉shared_ptr关于正确的删除器,就可以管理窗口句柄,正则表达式资源和其他任意内容。
有不同的智能指针用于不同目的:
代码:
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。
代码:
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<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(函数fx)是共享的,但每个都有一个单独的条目,我们在其上设置颜色。有一个weak_ptr类,当代码需要引用智能指针所拥有的资源时使用,但不需要拥有该资源。您应该创建一个weak_ptr,而不是传递原始指针。当它注意到你试图通过weak_ptr访问路径访问资源时会抛出一个异常,即使没有shared_ptr拥有该资源。
答案 2 :(得分:32)
RAII是确保变量处理其构造函数中所有需要的初始化以及所有需要在其析构函数中进行清理的设计范例。这将所有初始化和清理减少到一步。
C ++不需要RAII,但越来越多的人认为使用RAII方法会产生更强大的代码。
RAII在C ++中很有用的原因是C ++在进入和离开作用域时,无论是通过正常的代码流还是通过异常触发的堆栈展开,本质上都会管理变量的创建和销毁。这是C ++中的免费赠品。
通过将所有初始化和清理与这些机制联系起来,您可以确保C ++也会为您处理这项工作。
在C ++中谈论RAII通常会导致对智能指针的讨论,因为指针在清理时特别脆弱。当管理从malloc或new获取的堆分配的内存时,程序员通常负责在销毁指针之前释放或删除该内存。智能指针将使用RAII原理确保在指针变量被销毁时销毁堆分配的对象。
答案 3 :(得分:8)
智能指针是RAII的变体。 RAII意味着资源获取是初始化。智能指针在使用之前获取资源(内存),然后在析构函数中自动将其抛出。发生了两件事:
例如,另一个例子是网络套接字RAII。在这种情况下:
现在,正如您所看到的,RAII在大多数情况下是一个非常有用的工具,因为它可以帮助人们铺设。
智能指针的C ++来源在网络上有数百万,包括我之上的回应。
答案 4 :(得分:2)
Boost有很多这些,包括Boost.Interprocess中的共享内存。它极大地简化了内存管理,特别是在令人头疼的情况下,例如当你有5个进程共享相同的数据结构时:当每个人都完成一大块内存时,你希望它自动获得释放和放大。不必坐在那里试图找出谁应该负责在一块内存上调用delete
,以免你最终导致内存泄漏,或者错误地释放两次并且可能损坏整个堆的指针。
答案 5 :(得分:0)
void foo() { std::string bar; // // more code here // }
无论发生什么事情,一旦foo()函数的范围被遗忘,bar就会被正确删除。
内部std :: string实现经常使用引用计数指针。因此,只有在其中一个字符串副本发生更改时才需要复制内部字符串。因此,引用计数智能指针可以在必要时仅复制某些内容。
此外,内部引用计数可以在不再需要内部字符串的副本时正确删除内存。