IT Notes

Умные указатели в C++

Умные указатели - это классы-обертки для обычных указателей C++. Они позволяют забыть о ручном освобождении памяти с помощью delete.

Классы умных указателей основаны на принципе RAII - получение ресурса есть инициализация. В случае указателей этот принцип сводится к вызову delete в деструкторе класса-обертки. Сам класс-обертка является локальной переменной, поэтому при выходе из области видимости ресурс (память в куче) автоматически освобождается.

Вернемся к примеру из статьи про области видимости переменных в C++:

class MyClass { /* … */ };
MyClass* func() {
    return new MyClass;
}

void anotherFunc() {
    MyClass* c = func();
    // …
    delete c;
}

В этом коде имеется существенный недостаток. Если в функции anotherFunc() между вызовом func() и delete будет выброшено исключение, то память мы так и не освободим. Просто не дойдем до delete.

void anotherFunc() {
    MyClass* c = func();
    // …
    throw std::exception();
    // До сюда мы уже не дойдем!
    delete c;
}

Класс std::unique_ptr в C++

Первый вариант исправления ситуации:

#include <memory> // Не забудьте подключить для работы с умными указателями

void anotherFunc() {
    std::unique_ptr< MyClass > c( func() );
    // …
    throw std::exception();
    // До сюда мы уже не дойдем!
    // Но нам и не надо! :)
}

Теперь нам не нужно самим думать о вызове delete. Все сделает std::unique_ptr. При этом использование такого указателя практически ничем не отличается от применения обычного.

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

Особенность std::unique_ptr заключается в том, что он никому так просто не отдаст указатель, которым управляет:

void anotherFunc() {
    std::unique_ptr< MyClass > c( func() );
    // std::unique_ptr< MyClass > c2 = c; - Не скомпилируется
}

Но мы можем явно указать, что хотим перенести управление указателем из одного unique_ptr в другой:

void anotherFunc() {
    std::unique_ptr< MyClass > c( func() );
    std::unique_ptr< MyClass > c2 = std::move( c ); // А вот так можно
}

После этого переменная с становится пустой, а монопольное управление указателем получает c2.

Класс std::shared_ptr в C++

Но если нам нужно иметь несколько указателей на один и тот же объект? Для этого воспользуемся std::shared_ptr:

void anotherFunc() {
    std::shared_ptr< MyClass > c( func() );
    std::shared_ptr< MyClass > c2 = c; // Допустимо простое копирование
}

В этом случае оба умных указателя в равной мере управляют обычным указателем. Освобождение памяти произойдет в момент, когда последний shared_ptr, обладающий общим ресурсом, покинет область видимости.

Умный указатель в качестве возвращаемого значения

Еще лучше, если функция func() явно будет возвращать умный указатель вместо обычного:

std::unique_ptr< MyClass > func() {
    return std::unique_ptr< MyClass >( new MyClass );
}

void anotherFunc() {
    std::unique_ptr< MyClass > c = func();
    std::shared_ptr< MyClass > c2 = func(); // Также допустимо со всеми последствиями
    // std::shared_ptr< MyClass > c3 = c; - Но это не сработает, если только с std::move()
}

Это обяжет использовать безопасную конструкцию всех пользователей вашей функции.

В зависимости от ситуации может потребоваться вернуть shared_ptr, а не unique_ptr:

std::shared_ptr< MyClass > func() {
    return std::make_shared< MyClass >();
}

void anotherFunc() {
    std::shared_ptr< MyClass > c = func();
    // std::unique_ptr< MyClass > c2 = func(); - А так нельзя
}

Обратите внимание, что для создания shared_ptr мы использовали шаблонную функцию std::make_shared(). В качестве параметра шаблона она принимает имя класса, а ее аргументы будут использованы при вызове конструктора создаваемого объекта.

Умные указатели для массивов

Оба класса умных указателей можно использовать для управления массивами:

std::shared_ptr< int > a1( new int[ 10 ], std::default_delete< int[] >() );
std::unique_ptr< int > a2( new int[ 10 ] );

Заметим, что при создании shared_ptr потребовался дополнительный аргумент std::default_delete< int[] >(). Он необходим для корректного освобождения ресурсов, обеспечивая вызов delete[].

С другой стороны, вряд ли найдется веское основание, чтобы использовать подобные конструкции. Лучше применять std::array или std::vector.

Когда использовать std::shared_ptr, а когда std::unique_ptr

На самом деле, все следует из названия этих классов. Если объект нужен только в одном месте, то используйте std::unique_ptr (чтобы защититься от непреднамеренного копирования). Если объект понадобился в нескольких местах, то - std::shared_ptr.

Если же проводить более глубокий анализ, то выясняется, что std::unique_ptr по своей эффективности очень близок к обычным указателям. Чего нельзя сказать о std::shared_ptr. Он предоставляет больше возможностей, но за все приходится платить. Увеличивается и расход памяти, и время доступа. Однако накладные расходы не столь существенны, поэтому в большинстве приложений разница окажется незаметной.

Похожие публикации

Комментарии

нужная

Спасибо за статью )

Зачем нужны функции std::make shared, std::make_unique? Почему просто не воспользоваться вызовом конструктора?

Используя эти функции вы гарантируете, что один и тот же "сырой" указатель не будет обернут в "умный" указатель более одного раза (как это может произойти при вызове оператора new). Конечно, это вопрос дисциплины, но если существует способ обезопасить себя от ненужных проблем, то имеет смысл им пользоваться.