Простой (и, возможно, часто используемый) пример 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 (). Другими словами, к тому моменту, когда вызывающий объект получает указатель, он становится бесполезным (и, возможно, хуже, чем бесполезным, поскольку его использование может привести к всевозможным прикольным ошибкам)
Так, каково решение? Мы можем создать str в куче, используя new - таким образом, когда 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), файл будет автоматически удален.