Язык программирования C Пятое издание
![]()
|
Упражнение 12.1. Сколько элементов будут иметь объекты b1 и b2 в конце этого кода? StrBlob b1; { StrBlob b2 = {"a", "an", "the"}; b1 = b2; b2.push_back("about"); } Упражнение 12.2. Напишите собственную версию класса StrBlob, включающего константные версии функций front() и back(). Упражнение 12.3. Нуждается ли этот класс в константных версиях функций push_back() и Page 571/1103 pop_back()? Если они нужны, добавьте их. В противном случае объясните, почему они не нужны? Упражнение 12.4. В функции check() нет проверки того, что параметр i больше нуля. Почему эта проверка не нужна? Упражнение 12.5. Конструктор, получающий тип initializer_list, не был объявлен как explicit (см. раздел 7.5.4). Обсудите преимущества и недостатки этого выбора. 12.1.2. Непосредственное управление памятью Язык определяет два оператора, позволяющие резервировать и освобождать области в динамической памяти. Оператор new резервирует память, а оператор delete освобождает память, зарезервированную оператором new. По причинам, которые станут ясны позже, использование этих операторов для управления памятью существенно более подвержено ошибкам, чем использование интеллектуальных указателей. Кроме того, классы, самостоятельно управляющие памятью (в отличие от таковых, использующих интеллектуальные указатели), не могут полагаться на стандартные определения тех их членов, которые копируют, присваивают и удаляют объекты класса (см. раздел 7.1.4). В результате программы, использующие интеллектуальные указатели, вероятно, будет проще написать и отлаживать. Пока не пройдена глава 13, будем использовать классы, резервирующие динамическую память, только если для управления ею используются интеллектуальные указатели.Использование оператора new для динамического резервирования и инициализации объектов Созданные в динамической памяти объекты не имеют имен, поэтому оператор new не предполагает никаких способов именования резервируемых объектов. Вместо этого оператор new возвращает указатель на зарезервированный объект: int *pi = new int; // pi указывает на динамически созданный, // безымянный, // неинициализированный объект типа int Это выражение new создает в динамической памяти объект типа int и возвращает указатель на него. По умолчанию создаваемые в динамической памяти объекты инициализируются значением по умолчанию (см. раздел 2.2.1). Это значит, что у объектов встроенного или составного типа будет неопределенное значение, а объекты типа класса инициализируются их стандартным конструктором: Page 572/1103 string *ps = new string; // инициализируется пустой строкой int *pi = new int; // pi указывает на неинициализированный int Динамически созданный объект можно инициализировать, используя прямую инициализацию (см. раздел 3.2.1). Можно применить традиционный конструктор (используя круглые скобки), а по новому стандарту можно также использовать списочную инициализацию (с фигурными скобками): int *pi = new int(1024); // pi указывает на объект со значением 1024 string *ps = new string(10, '9'); // *ps = "9999999999" // вектор на десять элементов со значениями от 0 до 9 vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9}; Динамически созданный объект можно также инициализировать значением по умолчанию (см. раздел 3.3.1), сопроводив имя типа парой пустых круглых скобок: string *ps1 = new string; // инициализация по умолчанию пустой строкой string *ps = new string(); // инициализация значением по умолчанию // (пустой строкой) int *pi1 = new int; // инициализация по умолчанию; // значение *pi1 не определено int *pi2 = new int(); // инициализация значением по умолчанию 0; // *pi2 = 0 Для типов классов (таких как string), определяющих собственные конструкторы (см. раздел Page 573/1103 7.1.4), запрос инициализации значением по умолчанию не имеет последствий; независимо от формы, объект инициализируется стандартным конструктором. Различие существенно в случае встроенных типов: инициализация объекта встроенного типа значением по умолчанию присваивает ему вполне конкретное значение, а инициализация по умолчанию — нет. Точно так же полагающиеся на синтезируемый стандартный конструктор члены класса встроенного типа также не будут не инициализированы, если эти члены не будут инициализированы в теле класса (см. раздел 7.1.4). По тем же причинам, по которым обычно инициализируют переменные, имеет смысл инициализировать и динамически созданные объекты. Когда предоставляется инициализатор в круглых скобках, для вывода типа объекта, который предстоит зарезервировать для этого инициализатора, можно использовать ключевое слово auto (см. раздел 2.5.2). Но, поскольку компилятор использует тип инициализатора для вывода резервируемого типа, ключевое слово auto можно использовать только с одиночным инициализатором в круглых скобках: auto p1 = new auto(obj); // p указывает на объект типа obj // этот объект инициализируется значением obj auto p2 = new auto{a,b,c}; // ошибка: для инициализатора нужно // использовать круглые скобки Тип p1 — это указатель на автоматически выведенный тип obj. Если obj имеет тип int, то тип p1 — int*; если obj имеет тип string, то тип p1 — string* и т.д. Вновь созданный объект инициализируется значением объекта obj. Динамически созданные константные объекты Для резервирования константных объектов вполне допустимо использовать оператор new: // зарезервировать и инициализировать const int const int *pci = new const int(1024); // зарезервировать и инициализировать значением по умолчанию const string const string *pcs = new const string; Подобно любым другим константным объектам, динамически созданный константный объект следует инициализировать. Динамический константный объект типа класса, определяющего стандартный конструктор (см. раздел 7.1.4), можно инициализировать неявно. Объекты других типов следует инициализировать явно. Поскольку динамически зарезервированный объект является константой, возвращенный оператором new указатель является указателем на константу (см. раздел 2.4.2). Исчерпание памяти Хотя современные машины имеют огромный объем памяти, всегда существует вероятность исчерпания динамической памяти. Как только программа использует всю доступную ей память, выражения с оператором new будут терпеть неудачу. По умолчанию, если оператор Page 574/1103 new неспособен зарезервировать требуемый объем памяти, он передает исключение типа bad_alloc (см. раздел 5.6). Используя иную форму оператора new, можно воспрепятствовать передаче исключения: // при неудаче оператор new возвращает нулевой указатель int *p1 = new int; // при неудаче оператор new передает // исключение std::bad_alloc int *p2 = new (nothrow) int; // при неудаче оператор new возвращает // нулевой указатель По причинам, рассматриваемым в разделе 19.1.2, эта форма оператора new упоминается как размещающий оператор new (placement new). Выражение размещающего оператора new позволяет передать дополнительные аргументы. В данном случае передается определенный библиотекой объект nothrow. Передача объекта nothrow оператору new указывает, что он не должен передавать исключения. Если эта форма оператора new окажется неспособна зарезервировать требуемый объем памяти, она возвратит нулевой указатель. Типы bad_alloc и nothrow определены в заголовке new.Освобождение динамической памяти Чтобы предотвратить исчерпание памяти, по завершении использования ее следует возвратить операционной системе. Для этого используется оператор delete, получающий указатель на освобождаемый объект: delete p; // p должен быть указателем на динамически созданный объект // или нулевым указателем Подобно оператору new, оператор delete выполняет два действия: удаляет объект, на который указывает переданный ему указатель, и освобождает соответствующую область памяти. Значения указателя и оператор delete Передаваемый оператору delete указатель должен либо указывать на динамически созданный объект, либо быть нулевым указателем (см. раздел 2.3.2). Результат удаления указателя на область памяти, зарезервированную не оператором new, или повторного удаления значения того же указателя непредсказуем: int i, *pi1 = &i, *pi2 = nullptr; double *pd = new double(33), *pd2 = pd; Page 575/1103 delete i; // ошибка: i - не указатель delete pi1; // непредсказуемо: pi1 - локальный delete pd; // ok delete pd2; // непредсказуемо: память, на которую указывает pd2, // уже освобождена delete pi2; // ok: освобождение нулевого указателя всегда допустимо Компилятор сообщает об ошибке оператора delete i, поскольку знает, что i — не указатель. Ошибки, связанные с выполнением оператора delete для указателей pi1 и pd2, коварней: обычно компиляторы неспособны выяснить, указывает ли указатель на объект, созданный статически или динамически. Точно так же компилятор не может установить, была ли уже освобождена память, на которую указывает указатель. Большинство компиляторов примет такие выражения delete, несмотря на их ошибочность. Хотя значение константного объекта не может быть изменено, сам объект вполне может быть удален. Подобно любым динамическим объектам, константный динамический объект освобождается выполнением оператора delete для указателя, указывающего на этот объект: const int *pci = new const int(1024); delete pci; // ok: удаляет константный объект Динамически созданные объекты существуют до тех пор, пока не будут освобождены Как упоминалось в разделе 12.1.1, управляемая указателем shared_ptr память автоматически освобождается при удалении последнего указателя shared_ptr. Динамический объект, управляемый указателем встроенного типа, существует до тех пор, пока к областям памяти, управляемой при помощи указателей встроенных типов, не будет удален явно. Функции, возвращающие обычные (а не интеллектуальные) указатели на области динамической памяти, возлагают ответственность за их удаление на вызывающую сторону: // возвращает указатель на динамически созданный объект Foo* factory(Т arg) { // Page 576/1103 обработать аргумент соответственно return new Foo(arg); // за освобождение этой памяти отвечает // вызывающая сторона } Подобно прежней версии функции factory() (см. раздел 12.1.1), эта версия резервирует объект, но не удаляет его. Ответственность за освобождение памяти динамического объекта, когда он станет больше не нужен, несет вызывающая сторона функции factory(). К сожалению, вызывающая сторона слишком часто забывает сделать это: void use_factory(Т arg) { Foo *p = factory(arg); // использовать p, но не удалить его } // p выходит из области видимости, но память, // на которую он указывает, не освобождается! Здесь функция use_factory() вызывает функцию factory() резервирующую новый объект типа Foo. Когда функция use_factory() завершает работу, локальная переменная p удаляется. Эта переменная — встроенный указатель, а не интеллектуальный. В отличие от классов, при удалении объектов встроенного типа не происходит ничего. В частности, когда указатель выходит из области видимости, с объектом, на который он указывает, ничего не происходит. Если этот указатель указывает на динамическую память, она не освобождается автоматически. Динамическая память, управляемая при помощи встроенных (а не интеллектуальных) указателей, продолжает существование, пока не будет освобождена явно. В этом примере указатель p был единственным указателем на область памяти, зарезервированную функцией factory(). По завершении функции use_factory() у программы больше нет никакого способа освободить эту память. Согласно общей логике программирования, следует исправить эту ошибку и напомнить о необходимости освобождения памяти в функции use_factory(): void use_factory(Т arg) { Foo *p = factory(arg); // использование p Page 577/1103 delete p; // не забыть освободить память сейчас, когда // она больше не нужна } Если созданный функцией use_factory() объект должен использовать другой код, то эту функцию следует изменить так, чтобы она возвращала указатель на зарезервированную ею память: Foo* use_factory(Т arg) { Foo *p = factory(arg); // использование p return p; // освободить память должна вызывающая сторона } Внимание! Управление динамической памятью подвержено ошибкам Есть три общеизвестных проблемы, связанных с использованием операторов new и delete при управлении динамической памятью: 1. Память забыли освободить. Когда динамическая память не освобождается, это называется "утечка памяти", поскольку она уже не возвращается в пул динамической памяти. Проверка утечек памяти очень трудна, поскольку она обычно не проявляется, пока приложение, проработав достаточно долго, фактически не исчерпает память. 2. Объект использован после удаления. Иногда эта ошибка обнаруживается при создании нулевого указателя после удаления. 3. Повторное освобождение той же памяти. Эта ошибка может произойти в случае, когда два указателя указывают на тот же динамически созданный объект. Если оператор delete применен к одному из указателей, то память объекта возвращается в пул динамической памяти. Если впоследствии применить оператор delete ко второму указателю, то динамическая память может быть нарушена. Допустить эти ошибки значительно проще, чем потом найти и исправить. Избежать всех этих проблем при использовании исключительно интеллектуальных указателей не получится. Интеллектуальный указатель способен позаботиться об удалении памяти только тогда, когда не останется других интеллектуальных указателей на эту область памяти.Переустановка значения указателя после удаления… Когда указатель удаляется, он становится недопустимым. Но, даже став недопустимым, на многих машинах он продолжает содержать адрес уже освобожденной области динамической памяти. После освобождения области памяти указатель на нее становится Page 578/1103 потерянным указателем (dangling pointer). Потерянный указатель указывает на ту область памяти, которая когда-то содержала объект, но больше не содержит. Потерянным указателям присущи все проблемы неинициализированных указателей (см. раздел 2.3.2). Проблем с потерянными указателями можно избежать, освободив связанную с ними память непосредственно перед выходом из области видимости самого указателя. Так не появится шанса использовать указатель уже после того, как связанная с ним память будет освобождена. Если указатель необходимо сохранить, то после применения оператора delete ему можно присвоить значение nullptr. Это непосредственно свидетельствует о том, что указатель не указывает на объект. …обеспечивает лишь частичную защиту Фундаментальная проблема с динамической памятью в том, что может быть несколько указателей на ту же область памяти. Переустановка значения указателя при освобождении памяти позволяет проверять допустимость данного конкретного указателя, но никак не влияет на все остальные указатели, все еще указывающие на уже освобожденную область памяти. Рассмотрим пример: int *p(new int(42)); // p указывает на динамическую память auto q = p; // p и q указывают на ту же область памяти delete p; // делает недопустимыми p и q p = nullptr; // указывает, что указатель p больше не связан с объектом Здесь указатели p и q указывают на тот же динамически созданный объект. Удалим этот объект и присвоим указателю p значение nullptr, засвидетельствовав, что он больше не указывает на объект. Однако переустановка значения указателя p никак не влияет на указатель q, который стал недопустимым после освобождения памяти, на которую указывал указатель p (и указатель q!). В реальных системах поиск всех указателей на ту же область памяти зачастую на удивление труден. Упражнения раздела 12.1.2 Упражнение 12.6. Напишите функцию, которая возвращает динамически созданный вектор целых чисел. Передайте этот вектор другой функции, которая читает значения его элементов со стандартного устройства ввода. Передайте вектор другой функции, выводящей прочитанные ранее значения. Не забудьте удалить вектор в подходящий момент. Упражнение 12.7. Переделайте предыдущее упражнение, используя на сей раз указатель shared_ptr. Упражнение 12.8. Объясните, все ли правильно в следующей функции: bool b() { int* p = new int; // ... return p; Page 579/1103 } Упражнение 12.9. Объясните, что происходит в следующем коде: int *q = new int(42), *r = new int(100); r = q; auto q2 = make_shared<int>(42), r2 = make_shared<int>(100); r2 = q2; 12.1.3. Использование указателя shared_ptr с оператором new Как уже упоминалось, если не инициализировать интеллектуальный указатель, он инициализируется как нулевой. Как свидетельствует табл. 12.3, интеллектуальный указатель можно также инициализировать указателем, возвращенным оператором new: shared_ptr<double> p1; // shared_ptr может указывать на double shared_ptr<int> p2(new int(42)); // p2 указывает на int со значением 42 Конструкторы интеллектуального указателя, получающие указатели, являются явными (см. раздел 7.5.4). Следовательно, нельзя неявно преобразовать встроенный указатель в интеллектуальный; для инициализации интеллектуального указателя придется использовать прямую форму инициализации (см. раздел 3.2.1): shared_ptr<int> p1 = new int(1024); // ошибка: нужна // прямая инициализация shared_ptr<int> p2(new int(1024)); // ok: использует // прямую инициализацию Таблица 12.3. Другие способы определения и изменения указателя shared_ptr shared_ptr<T> p(q) Указатель p управляет объектом, на который указывает указатель встроенного типа q; указатель q должен указывать на область памяти, зарезервированную оператором new, а его тип должен быть преобразуем в тип Т* shared_ptr<T> p(u) Указатель p учитывает собственность указателя u типа unique_ptr; указатель u становится нулевым shared_ptr<T> p(q, d) Указатель p учитывает собственность объекта, на который Page 580/1103 указывает встроенный указатель q. Тип указателя q должен быть преобразуем в тип Т* (см. раздел 4.11.2). Для освобождения q указатель p будет использовать вызываемый объект d (см. раздел 10.3.2) вместо оператора delete shared_ptr<T> p(p2, d) Указатель p — это копия указателя p2 типа shared_ptr, как описано в табл. 12.2, за исключением того, что указатель p использует вызываемый объект d вместо оператора delete p.reset() p.reset(q) p.reset(q, d) Если p единственный указатель shared_ptr на объект, функция reset() освободит существующий объект p. Если передан необязательный встроенный указатель q, то p будет указывать на q, в противном случае p станет нулевым. Если предоставлен вызываемый объект d, то он будет вызван для освобождения указателя q, в противном случае используется оператор delete Инициализация указателя p1 неявно требует, чтобы компилятор создал указатель типа shared_ptr из указателя int*, возвращенного оператором new. Поскольку нельзя неявно преобразовать обычный указатель в интеллектуальный, такая инициализация ошибочна. По той же причине функция, возвращающая указатель shared_ptr, не может неявно преобразовать простой указатель в своем операторе return: shared_ptr<int> clone(int p) { return new int(p); // ошибка: неявное преобразование // в shared_ptr<int> } Следует явно связать указатель shared_ptr с указателем, который предстоит возвратить: shared_ptr<int> clone (int p) { // ok: явное создание shared_ptr<int> из int* return shared_ptr<int>(new int(p)); } По умолчанию указатель, используемый для инициализации интеллектуального указателя, должен указывать на область динамической памяти, поскольку по умолчанию интеллектуальные указатели используют оператор delete для освобождения связанного с ним объекта. Интеллектуальные указатели можно связать с указателями на другие виды ресурсов. Но для этого необходимо предоставить собственную функцию, используемую вместо оператора delete. Предоставление собственного кода удаления рассматривается в разделе 12.1.4. Не смешивайте обычные указатели с интеллектуальными Указатель shared_ptr может координировать удаление только с другими указателями shared_ptr, которые являются его копиями. Действительно, этот факт — одна из причин, по которой рекомендуется использовать функцию make_shared(), а не оператор new. Это связывает указатель shared_ptr с объектом одновременно с его резервированием. При этом нет никакого способа по неосторожности связать ту же область памяти с несколькими независимо созданными указателями shared_ptr. Рассмотрим следующую функцию, работающую с указателем shared_ptr: // Page 581/1103 ptr создается и инициализируется при вызове process() void process(shared_ptr<int> ptr) { // использование ptr } // ptr выходит из области видимости и удаляется Параметр функции process() передается по значению, поэтому аргумент копируется в параметр ptr. Копирование указателя shared_ptr осуществляет инкремент его счетчика ссылок. Таким образом, в функции process() значение счетчика не меньше 2. По завершении функции process() осуществляется декремент счетчика ссылок указателя ptr, но он не может достигнуть нуля. Поэтому, когда локальная переменная ptr удаляется, память, на которую она указывает, не освобождается. Правильный способ использования этой функции подразумевает передачу ей указателя shared_ptr: shared_ptr<int> p(new int (42)); // счетчик ссылок = 1 process(p); // копирование p увеличивает счетчик; // в функции process() счетчик = 2 int i = *p; // ok: счетчик ссылок = 1 Хотя функции process() нельзя передать встроенный указатель, ей можно передать временный указатель shared_ptr, явно созданный из встроенного указателя. Но это, вероятно, будет ошибкой: int *x(new int(1024)); // опасно: x - обычный указатель, a // не интеллектуальный process(x); // ошибка: нельзя преобразовать int* в shared_ptr<int> process(shared_ptr<int>(x)); // допустимо, но память будет освобождена! int j = *x; // Page 582/1103 непредсказуемо: x - потерянный указатель! В этом вызове функции process() передан временный указатель shared_ptr. Этот временный указатель удаляется, когда завершается выражение, в котором присутствует вызов. Удаление временного объекта приводит к декременту счетчика ссылок, доводя его до нуля. Память, на которую указывает временный указатель, освобождается при удалении временного указателя. Но указатель x продолжает указывать на эту (освобожденную) область памяти; теперь x — потерянный указатель. Результат попытки использования значения, на которое указывает указатель x, непредсказуем. При связывании указателя shared_ptr с простым указателем ответственность за эту память передается указателю shared_ptr. Как только ответственность за область памяти встроенного указателя передается указателю shared_ptr, больше нельзя использовать встроенный указатель для доступа к памяти, на которую теперь указывает указатель shared_ptr. Опасно использовать встроенный указатель для доступа к объекту, принадлежащему интеллектуальному указателю, поскольку нельзя быть уверенным в том, что этот объект еще не удален. Другие операции с указателем shared_ptr Класс shared_ptr предоставляет также несколько других операций, перечисленных в табл. 12.2 и табл. 12.3. Чтобы присвоить новый указатель указателю shared_ptr, можно использовать функцию reset(): p = new int(1024); // нельзя присвоить обычный указатель // указателю shared_ptr p.reset(new int(1024)); // ok: p указывает на новый объект Подобно оператору присвоения, функция reset() модифицирует счетчики ссылок, а если нужно, удаляет объект, на который указывает указатель p. Функцию-член reset() зачастую используют вместе с функцией unique() для контроля совместного использования объекта несколькими указателями shared_ptr. Прежде чем изменять базовый объект, проверяем, является ли владелец единственным. В противном случае перед изменением создается новая копия: if (!p.unique()) p.reset(new string(*p)); // владелец не один; резервируем новую копию *p += newVal; // теперь, когда известно, что указатель единственный, // можно изменить объект Упражнения раздела 12.1.3 Page 583/1103 Упражнение 12.10. Укажите, правилен ли следующий вызов функции process(), определенной в текущем разделе. В противном случае укажите, как его исправить? shared_ptr<int> p(new int(42)); process(shared_ptr<int>(p)); Упражнение 12.11. Что будет, если вызвать функцию process() следующим образом? process(shared_ptr<int>(p.get())); Упражнение 12.12. Используя объявления указателей p и sp, объясните каждый из следующих вызовов функции process(). Если вызов корректен, объясните, что он делает. Если вызов некорректен, объясните почему: auto p = new int(); auto sp = make_shared<int>(); (a) process(sp); (b) process(new int()); (c) process(p); (d) process(shared_ptr<int>(p)); Упражнение 12.13. Что будет при выполнении следующего кода? auto sp = make_shared<int>(); auto p = sp.get(); delete p; 12.1.4. Интеллектуальные указатели и исключения В разделе 5.6.2 упоминалось, что программы, использующие обработку исключений для продолжения работы после того, как произошло исключение, нуждаются в способе правильного освобождения ресурсов в случае исключения. Самый простой из них подразумевает использование интеллектуальных указателей. При использовании интеллектуального указателя его класс гарантирует освобождение памяти, когда в ней больше нет необходимости, даже при преждевременном выходе из блока: void f() { shared_ptr<int> sp(new int(42)); // зарезервировать новый объект // код, передающий исключение, не обрабатываемое в функции f() Page 584/1103 } // shared_ptr освобождает память автоматически по завершении функции При выходе из функции, обычном или в связи с исключением, удаляются все ее локальные объекты. В данном случае указатель sp имеет тип shared_ptr, поэтому при удалении проверяется его счетчик ссылок. В данном случае sp — единственный указатель на контролируемую им область памяти, поэтому она освобождается в ходе удаления указателя sp. Память, контролируемая непосредственно, напротив, не освобождается автоматически, когда происходит исключение. Если для управления памятью используются встроенные указатели и исключение происходит после оператора new, но перед оператором delete, то контролируемая память не будет освобождена: void f() { int *ip = new int(42); // динамически зарезервировать новый объект // код, передающий исключение, не обрабатываемое в функции f() delete ip; // освобождает память перед выходом } Если исключение происходит между операторами new и delete и не обрабатывается в функции f(), то освободить эту память никак не получится. Вне функции f() нет указателя на эту память, поэтому нет никакого способа освободить ее. Интеллектуальные указатели и классы без деструкторов Большинство классов языка С++, включая все библиотечные классы, определяют деструкторы (см. раздел 12.1.1), заботящиеся об удалении используемых объектом ресурсов. Но не все классы таковы. В частности, классы, разработанные для использования и в языке С, и в языке С++, обычно требуют от пользователя явного освобождения всех используемых ресурсов. Классы, которые резервируют ресурсы, но не определяют деструкторы для их освобождения, подвержены тем же ошибкам, которые возникают при самостоятельном использовании динамической памяти. Довольно просто забыть освободить ресурс. Аналогично, если произойдет исключение после резервирования ресурса, но до его освобождения, программа потеряет его. Для управления классами без деструкторов зачастую можно использовать те же подходы, что и для управления динамической памятью. Предположим, например, что используется сетевая библиотека, применимая как в языке С, так и в С++. Использующая эту библиотеку программа могла бы содержать такой код: struct destination; // представляет то, с чем установлено соединение Page 585/1103 struct connection; // информация для использования соединения connection connect(destination*); // открывает соединение void disconnect(connection); // закрывает данное соединение void f(destination &d /* другие параметры */) { // получить соединение; не забыть закрывать по завершении connection с = connect(&d); // использовать соединение // если забыть вызывать функцию disconnect() перед выходом из // функции f(), то уже не будет никакого способа закрыть соединение } Если бы у структуры connection был деструктор, то по завершении функции f() он закрыл бы соединение автоматически. Однако у нее нет деструктора. Эта проблема почти идентична проблеме предыдущей программы, использовавшей указатель shared_ptr, чтобы избежать утечек памяти. Здесь также можно использовать указатель shared_ptr для гарантии правильности закрытия соединения. Использование собственного кода удаления По умолчанию указатели shared_ptr подразумевали, что они указывают на динамическую память. Следовательно, когда указатель shared_ptr удаляется, он по умолчанию выполняет оператор delete для содержащегося в нем указателя. Чтобы использовать указатель shared_ptr для управления соединением connection, следует сначала определить функцию, используемую вместо оператора delete. Должна быть возможность вызова этой функции удаления (deleter) с указателем, хранимым в указателе shared_ptr. В данном случае функция удаления должна получать один аргумент типа connection*: void end_connection(connection *p) { disconnect(*p); } При создании указателя shared_ptr можно передать необязательный аргумент, указывающий на функцию удаления (см. раздел 6.7): void f(destination &d /* другие параметры */) { connection с = connect(&d); Page 586/1103 shared_ptr<connection> p(&c, end_connection); // использовать соединение // при выходе из функции f(), даже в случае исключения, соединение // будет закрыто правильно } При удалении указателя p для хранимого в нем указателя вместо оператора delete будет вызвана функция end_connection(). Функция end_connection(), в свою очередь, вызовет функцию disconnect(), гарантируя таким образом закрытие соединения. При нормальном выходе из функции f() указатель p будет удален в ходе процедуры выхода. Кроме того, указатель p будет также удален, а соединение закрыто, если произойдет исключение. Внимание! Проблемы интеллектуального указателя Интеллектуальные указатели могут обеспечить безопасность и удобство работы с динамически созданной памятью только при правильном использовании. Для этого следует придерживаться ряда соглашений. • Не используйте значение того же встроенного указателя для инициализации (переустановки) нескольких интеллектуальных указателей. • Не используйте оператор delete для указателя, возвращенного функцией get(). • Не используйте функцию get() для инициализации или переустановки другого интеллектуального указателя. • Используя указатель, возвращенный функцией get(), помните, что указатель станет недопустимым после удаления последнего соответствующего интеллектуального указателя. • Если интеллектуальный указатель используется для управления ресурсом, отличным от области динамической памяти, зарезервированной оператором new, не забывайте использовать функцию удаления (раздел 12.1.4 и раздел 12.1.5). Упражнения раздела 12.1.4 Упражнение 12.14. Напишите собственную версию функции, использующую указатель shared_ptr для управления соединением. Упражнение 12.15. Перепишите первое упражнение так, чтобы использовать лямбда-выражение (см. раздел 10.3.2) вместо функции end_connection(). 12.1.5. Класс unique_ptr Указатель unique_ptr "владеет" объектом, на который он указывает. В отличие от указателя shared_ptr, только один указатель unique_ptr может одновременно указывать на данный объект. Объект, на который указывает указатель unique_ptr, удаляется при удалении указателя. Список функций, специфических для указателя unique_ptr, приведен в табл. 12.4. Page 587/1103 Функции, общие для обоих указателей, приведены в табл. 12.1. В отличие от указателя shared_ptr, нет никакой библиотечной функции, подобной функции make_shared(), которая возвращала бы указатель unique_ptr. Вместо этого определяемый указатель unique_ptr связывается с указателем, возвращенным оператором new. Подобно указателю shared_ptr, можно использовать прямую форму инициализации: unique_ptr<double> p1; // указатель unique_ptr на тип double unique_ptr<int> p2(new int(42)); // p2 указывает на int со значением 42 Таблица 12.4. Функции указателя unique_ptr (см. также табл. 12.1) unique_ptr<T> u1 unique_ptr<T, D> u2 Обнуляет указатель unique_ptr, способный указывать на объект типа Т. Указатель u1 использует для освобождения своего указателя оператор delete; а указатель u2 — вызываемый объект типа D unique_ptr<T, D> u(d) Обнуляет указатель unique_ptr, указывающий на объекты типа Т. Использует вызываемый объект d типа D вместо оператора delete u = nullptr Удаляет объект, на который указывает указатель u; обнуляет указатель u u.release() Прекращает контроль содержимого указателя u; возвращает содержимое указателя u и обнуляет его u.reset() u.reset(q) u.reset(nullptr) Удаляет объект, на который указывает указатель u. Если предоставляется встроенный указатель q, то u будет указывать на его объект. В противном случае указатель u обнуляется Поскольку указатель unique_ptr владеет объектом, на который указывает, он не поддерживает обычного копирования и присвоения: unique_ptr<string> p1(new string("Stegosaurus")); unique_ptr<string> p2(p1); // ошибка: невозможно копирование unique_ptr unique_ptr<string> p3; p3 = p2; // ошибка: невозможно присвоение unique_ptr Хотя указатель unique_ptr нельзя ни присвоить, ни скопировать, можно передать собственность от одного (неконстантного) указателя unique_ptr другому, вызвав функцию release() или reset(): // передает собственность от p1 (указывающего на // строку "Stegosaurus") к p2 unique_ptr<string> p2(p1.release()); // release() обнуляет p1 Page 588/1103 unique_ptr<string> p3(new string("Trex")); // передает собственность от p3 к p2 р2.reset(p3.release()); // reset() освобождает память, на которую // указывал указатель p2 Функция-член release() возвращает указатель, хранимый в настоящее время в указателе unique_ptr, и обнуляет указатель unique_ptr. Таким образом, указатель p2 инициализируется указателем, хранимым в указателе p1, а сам указатель p1 становится нулевым. Функция-член reset() получает необязательный указатель и переустанавливает указатель unique_ptr на заданный указатель. Если указатель unique_ptr не нулевой, то объект, на который он указывает, удаляется. Поэтому вызов функции reset() указателя p2 освобождает память, используемую строкой со значением "Stegosaurus", передает содержимое указателя p3 указателю p2 и обнуляет указатель p3. Вызов функции release() нарушает связь между указателем unique_ptr и объектом, который он контролирует. Зачастую указатель, возвращенный функцией release(), используется для инициализации или присвоения другому интеллектуальному указателю. В этом случае ответственность за управление памятью просто передается от одного интеллектуального указателя другому. Но если другой интеллектуальный указатель не используется для хранения указателя, возвращенного функцией release(), то ответственность за освобождения этого ресурса берет на себя программа: p2.release(); // ОШИБКА: p2 не освободит память, и указатель // будет потерян auto p = p2.release(); // ok, но следует не забыть delete(p) Передача и возвращение указателя unique_ptr Из правила, запрещающего копирование указателя unique_ptr, есть одно исключение: можно копировать и присваивать те указатели unique_ptr, которые предстоит удалить. Наиболее распространенный пример — возвращение указателя unique_ptr из функции: unique_ptr<int> clone(int p) { // ok: явное создание unique_ptr<int> для int* return unique_ptr<int>(new int(p)); } Page 589/1103 В качестве альтернативы можно также возвратить копию локального объекта: unique_ptr<int> clone(int p) { unique_ptr<int> ret(new int(p)); // ... return ret; } В обоих случаях компилятор знает, что возвращаемый объект будет сейчас удален. В таких случаях компилятор осуществляет специальный вид "копирования", обсуждаемый в разделе 13.6.2. Совместимость с прежней версией: класс auto_ptr Прежние версии библиотеки включали класс auto_ptr, обладавший некоторыми, но не всеми, свойствами указателя unique_ptr. В частности, невозможно было хранить указатели auto_ptr в контейнере и возвращать их из функции. Хотя указатель auto_ptr все еще присутствует в стандартной библиотеке, вместо него следует использовать указатель unique_ptr. Передача функции удаления указателю unique_ptr Подобно указателю shared_ptr, для освобождения объекта, на который указывает указатель unique_ptr, по умолчанию используется оператор delete. Подобно указателю shared_ptr, функцию удаления указателя unique_ptr (см. раздел 12.1.4) можно переопределить. Но по причинам, описанным в разделе 16.1.6, способ применения функции удаления указателем unique_ptr отличается от такового у shared_ptr. Переопределение функции удаления указателя unique_ptr влияет на тип и способ создания (или переустановки) объектов этого типа. Подобно переопределению оператора сравнения ассоциативного контейнера (см. раздел 11.2.2), тип функции удаления можно предоставить в угловых скобках наряду с типом, на который может указывать указатель unique_ptr. При создании или переустановке объекта этого типа предоставляется вызываемый объект определенного типа: // p указывает на объект типа objT и использует объект типа delT // для его освобождения // он вызовет объект по имени fcn типа delT unique_ptr<objТ, delT> p(new objT, fcn); В качестве несколько более конкретного примера перепишем программу соединения так, чтобы использовать указатель unique_ptr вместо указателя shared_ptr следующим образом: void f(destination &d /* другие необходимые параметры */) { connection c = connect(&d); // Page 590/1103 открыть соединение // когда p будет удален, соединение будет закрыто unique_ptr<connection, decltype(end_connection)*> p(&с, end_connection); // использовать соединение // по завершении f(), даже при исключении, соединение будет // закрыто правильно } Для определения типа указателя на функцию используется ключевое слово decltype (см. раздел 2.5.3). Поскольку выражение decltype(end_connection) возвращает тип функции, следует добавить символ *, указывающий, что используется указатель на этот тип (см. раздел 6.7). Упражнения раздела 12.1.5 Упражнение 12.16. Компиляторы не всегда предоставляют понятные сообщения об ошибках, если осуществляется попытка скопировать или присвоить указатель unique_ptr. Напишите программу, которая содержит эти ошибки, и посмотрите, как компилятор диагностирует их. Упражнение 12.17. Какие из следующих объявлений указателей unique_ptr недопустимы или вероятнее всего приведут к ошибке впоследствии? Объясните проблему каждого из них. int ix = 1024, *pi = &ix, *pi2 = new int(2048); typedef unique_ptr<int> IntP; (a) IntP p0(ix); (b) IntP p1(pi); (c) IntP p2(pi2); (d) IntP p3(&ix); (e) IntP p4(new int(2048)); (f) IntP p5(p2.get()); |