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). Конечно, это вопрос дисциплины, но если существует способ обезопасить себя от ненужных проблем, то имеет смысл им пользоваться.

RSS RSS-рассылка

Популярное

Дешевый хостинг