📚 Hub Books: Онлайн-чтение книгРазная литератураC++17 STL Стандартная библиотека шаблонов - Яцек Галовиц

C++17 STL Стандартная библиотека шаблонов - Яцек Галовиц

Шрифт:

-
+

Интервал:

-
+
1 ... 85 86 87 88 89 90 91 92 93 ... 121
Перейти на страницу:
на который ссылается, и выполняет свою задачу по освобождению его памяти, если та более не используется. Этот класс может навсегда освободить нас от утечек памяти (во всяком случае вместе со своими компаньонами shared_ptr и weak_ptr, но в этом примере мы концентрируемся только на unique_ptr). Самая приятная особенность заключается в том, что он не влияет на производительность и свободное место в сравнении с кодом, содержащим необработанные указатели и предусматривающим ручное управление памятью. (О’кей, он все еще устанавливает значение внутреннего необработанного указателя на nullptr после разрушения объекта, на который он указывает, и это нужно учитывать при оптимизации. Большая часть кода, управляющего динамической памятью и написанного вручную, делает то же самое.)

В этом разделе мы рассмотрим unique_ptr и способы его использования.

Как это делается

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

1. Сначала включим необходимые заголовочные файлы и объявим об использовании пространства имен std:

#include <iostream>

#include <memory>

using namespace std;

2. Мы реализуем небольшой класс для объекта, которым будем управлять с помощью unique_ptr. Его конструктор и деструктор станут выводить сообщения на консоль; это поможет узнать о том, что объект был на самом деле удален.

class Foo

{

public:

  string name;

  Foo(string n)

    : name{move(n)}

  { cout << "CTOR " << name << 'n'; }

  ~Foo() { cout << "DTOR " << name << 'n'; }

};

3. Попробуем реализовать функцию, принимающую в качестве аргументов уникальные указатели, чтобы увидеть, какие ограничения она имеет. Она обрабатывает элемент типа Foo, выводя его название. Обратите внимание: хотя уникальные указатели являются умными, работают без лишних издержек и удобны в использовании, они все еще могут иметь значение null. Это значит, что их все еще нужно проверять перед разыменованием.

void process_item(unique_ptr<Foo> p)

{

  if (!p) { return; }

  cout << "Processing " << p->name << 'n';

}

4. В функции main откроем еще одну область видимости, создадим два объекта типа Foo в куче и будем управлять ими обоими с помощью уникальных указателей. Создадим первый объект в куче явно, используя оператор new, а затем поместим его в конструктор переменной типа unique_ptr<Foo>, p1. Создадим уникальный указатель p2 путем вызова make_unique<Foo> с аргументами, которые в противном случае передали бы конструктору класса Foo. Этот способ более элегантен, поскольку можно воспользоваться автоматическим определением типов и при первом обращении к объекту он уже будет управляться unique_ptr:

int main()

{

  {

    unique_ptr<Foo> p1 {new Foo{"foo"}};

    auto p2 (make_unique<Foo>("bar"));

  }

5. После того как мы покинем область видимости, оба объекта будут разрушены и их память вернется в кучу. Взглянем на функцию process_item и узнаем, как теперь ею пользоваться вкупе с unique_ptr. Если мы создадим новый экземпляр типа Foo, управляемый unique_ptr в вызове функции, то его время жизни будет ограничено областью видимости функции. Когда функция process_item отработает, объект будет уничтожен:

  process_item(make_unique<Foo>("foo1"));

6. Если мы хотим вызвать process_item для объекта, который существовал еще до вызова, нужно передать право владения, поскольку данная функция принимает unique_ptr по значению; это значит, что его вызов создаст копию. Но unique_ptr нельзя скопировать, его можно только переместить. Создадим еще два объекта типа Foo и переместим один из них в process_item. Взглянув на консоль, мы увидим, что foo2 был уничтожен после того, как отработала функция process_item, поскольку мы передали ей право владения. Объект foo3 продолжит существовать до тех пор, пока не отработает функция main.

  auto p1 (make_unique<Foo>("foo2"));

  auto p2 (make_unique<Foo>("foo3"));

  process_item(move(p1));

  cout << "End of main()n";

}

7. Скомпилируем и запустим программу. Сначала мы увидим вызовы конструктора и деструктора для foo и bar. Они разрушаются после того, как программа покидает дополнительную область видимости. Обратите внимание: объекты разрушаются в порядке, обратном тому, в котором были созданы. Следующая строка конструктора принадлежит foo1 — мы создали этот объект во время вызова process_item. Он уничтожается сразу после вызова функции. Затем создали объекты foo2 и foo3. Первый из них уничтожается сразу после вызова process_item, где мы передали право владения. Другой элемент, foo3, разрушается после выполнения последней строки кода функции main.

$ ./unique_ptr

CTOR foo

CTOR bar

DTOR bar

DTOR foo

CTOR foo1

Processing foo1

DTOR foo1

CTOR foo2

CTOR foo3

Processing foo2

DTOR foo2

End of main()

DTOR foo3

Как это работает

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

Если мы присвоим какой-то новый указатель уникальному указателю, то он сначала удалит старый объект, а только затем сохранит новый указатель. Для переменной уникального указателя x можно также вызвать x.reset() только затем, чтобы удалить объект, на который он указывает, не присваивая новый указатель. Еще одна эквивалентная альтернатива повторному присваиванию с помощью x = new_pointer — это x.reset(new_pointer).

 

 Существует единственный способ освободить объект от управления unique_ptr без удаления самого объекта. Это делает функция release, но использовать ее в большинстве ситуаций не рекомендуется.

Поскольку указатели нужно проверять перед разыменованием, они переопределяют некоторые операторы так, чтобы те походили на необработанные указатели. Условия наподобие if (p) {...} и if (p != nullptr) {...} работают так же, как если бы мы проверяли необработанный указатель.

Разыменовать уникальный указатель можно с помощью функции get(), возвращающей необработанный указатель на объект, или непосредственно с применением operator*, что опять же делает их похожими на необработанные указатели.

Одна

1 ... 85 86 87 88 89 90 91 92 93 ... 121
Перейти на страницу:

Комментарии

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

Никто еще не прокомментировал. Хотите быть первым, кто выскажется?