RAII и интеллектуальные указатели на C++

на практике с C++, что такое RAII, что смарт-указатели, как они реализованы в программе и каковы преимущества использования RAII с интеллектуальными указателями?

6 ответов


простой (и, возможно, чрезмерно используемый) пример RAII-это класс файлов. Без RAII код может выглядеть примерно так:

File file("/path/to/file");
// Do stuff with file
file.close();

другими словами, мы должны убедиться, что мы закрываем файл, как только мы закончим с ним. Это имеет два недостатка - во-первых, везде, где мы используем файл, нам придется вызывать File::close () - если мы забудем это сделать, мы держим файл дольше, чем нам нужно. Вторая проблема заключается в том, что, если исключение создается до закрытия файл?

Java решает вторую проблему, используя предложение finally:

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

C++ решает обе проблемы с помощью RAII - то есть закрывает файл в деструкторе файла. Пока объект 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? Мы могли бы написать так:--14-->

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

Итак, что в этом плохого? Ну, возвращаемый тип-std:: string - значит, мы возвращаемся по значению. Это означает, что мы копируем ул. и фактически вернуть копию. Это может быть дорого, и мы могли бы избежать затрат на копирование. Поэтому мы могли бы придумать идею возврата по ссылке или по указателю.

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 уничтожается), файл будет автоматически удален.


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

как вы видите, есть много способов получить pwned. Идея заключается в том, что мы инкапсулируем управление ресурсами в класс. Инициализация его объекта приобретает ресурс ("приобретение ресурса-это инициализация"). В момент выхода из блока (область блока) ресурс снова освобождается.

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, позволяют вам сообщить им так называемый deleter, который используется вместо 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

для совместного владения. Поэтому его можно копировать и перемещать. Несколько экземпляров smart pointer может владеть тем же ресурсом. Как только последний смарт-указатель, владеющий ресурсом, выйдет за пределы области, ресурс будет освобожден. Пример одного из моих проектов в реальном мире:

код:

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 больше не владеет ресурсом.


предпосылки и причины просты, в концепции.

RAII является парадигмой дизайна, чтобы гарантировать, что переменные обрабатывают всю необходимую инициализацию в своих конструкторах и всю необходимую очистку в своих деструкторах. это уменьшает всю инициализацию и очистку до одного шага.

C++ не требует RAII, но все чаще признается, что использование методов RAII приведет к более надежному коду.

причина, по которой RAII полезен в C++ этот C++ внутренне управляет созданием и уничтожением переменных по мере их входа и выхода из области, будь то через обычный поток кода или через размотку стека, вызванную исключением. Это халява на C++.

связывая всю инициализацию и очистку с этими механизмами, вы гарантируете, что C++ позаботится об этой работе и для вас.

разговор о RAII в C++ обычно приводит к обсуждению интеллектуальных указателей, потому что указатели особенно тяжело, когда дело доходит до очистки. При управлении выделенной кучей памяти, полученной из malloc или new, программист обычно несет ответственность за освобождение или удаление этой памяти до уничтожения указателя. Интеллектуальные указатели будут использовать философию RAII для обеспечения уничтожения выделенных объектов кучи при каждом уничтожении переменной указателя.


Smart pointer-это вариация RAII. RAII означает, что приобретение ресурсов является инициализацией. Smart pointer получает ресурс (память) перед использованием, а затем автоматически выбрасывает его в деструктор. Происходят две вещи:--1-->

  1. выделяем прежде чем мы используем его всегда, даже когда мы не чувствуем, как это трудно сделать другим способом с помощью смарт-указатель. Если этого не произошло, вы попытаетесь получить доступ к нулевой памяти, что приведет к сбою (очень болезненный.)
  2. мы даже когда есть ошибки. Нет памяти повисли.

например, Другим примером является сетевой сокет RAII. В этом случае:

  1. открываем сетевой сокет прежде чем мы используем его, всегда, даже когда мы не чувствуем, как ... трудно сделать это по-другому с RAII. Если вы попытаетесь сделать это без RAII, вы можете открыть пустой сокет для, скажем, MSN-соединения. Затем сообщение типа " давай сделаем это сегодня вечером" возможно, вас не переведут, пользователи не будут трахаться, и вы можете рискнуть быть уволены.
  2. закрыть сетевой сокет даже когда есть ошибки. Ни один сокет не остается висящим, поскольку это может помешать ответному сообщению "уверен, что я буду внизу"от удара отправителя.

теперь, как вы можете видеть, RAII-очень полезный инструмент в большинстве случаев, поскольку он помогает людям трахаться.

источники c++ интеллектуальных указателей в миллионах по всей сети, включая ответы выше меня.


Boost имеет ряд из них, включая те, в импульс.Interprocess для общей памяти. Это значительно упрощает управление памятью, особенно в ситуациях, вызывающих головную боль, например, когда у вас есть 5 процессов, разделяющих одну и ту же структуру данных: когда все закончат с куском памяти, вы хотите, чтобы он автоматически освободился и не должен сидеть там, пытаясь выяснить, кто должен отвечать за вызов delete на куске памяти, чтобы вы не закончили утечкой памяти, или указатель, который ошибочно освобождается дважды и может повредить всю кучу.


void foo()
{
   std::string bar;
   //
   // more code here
   //
}

независимо от того, что происходит, bar будет правильно удален, как только область функции foo() будет оставлена позади.

внутренне реализации std::string часто используют указатели подсчета ссылок. Таким образом, внутренняя строка должна быть скопирована только при изменении одной из копий строк. Поэтому интеллектуальный указатель с подсчетом ссылок позволяет копировать что-либо только при необходимости.

кроме того, внутренний подсчет ссылок делает возможно, что память будет правильно удалена, когда копия внутренней строки больше не понадобится.