cpp-oop (823968), страница 21
Текст из файла (страница 21)
Однако работая с указателями программисты, особенно начинающие, допускаютзначительное количество ошибок, приводящих к нарушениям защиты и «утечкам»памяти. Причины этого кроются в особенностях языковых средств, используемых приработе с динамическими объктами. Так:•при использовании указателей ответственность за выделение памяти под объекти ее освобождение целиком лежит на программисте;•на один и тот же объект может ссылаться несколько не связанных между собойуказателей,следовательновозможноналичиеуказателей,ссылающихсянаосвобождённые или перемещённые объекты;•нет никакой возможности проверить, указывает ли ненулевой указатель накорректные данные, либо «в никуда»;•указатель на единичный объект и указатель на массив объектов в С++ никак неотличаются друг от друга.Разработать код, который корректно освобождает выделенную динамическуюпамять, непросто.
При этом надо предусмотреть освобождение памяти не только принормальном вычислительном процессе, но и при аварийном завершении.Так простейшая процедура, в которой создается динамический объект и вызываетсяего метод:void proc(){B *temp = new B;B->func();delete temp;// выделение памяти под объект// вызов метода для объекта// освобождение памяти}ОглавлениеИванова Г.С., Ничушкина Т.Н.
«Объектно-ориентированное программирование на языке C++ в среде Microsoft Visual Studio 2008»163с учетом необходимости освобождения памяти при аварийном завершении существенноудлиняется:void proc(){B *temp = NULL;try{temp = new B; // выделение памяти под объектB->func();// вызов метода для объекта}catch (...)// перехват всех исключений{delete temp;// освобождение памяти при аварийном// завершении процедурыthrow;// возобновление исключения}delete temp;// освобождение памяти при нормальном// завершении процедуры}Еще сложнее обеспечить освобождение памяти при возникновении ошибки вконструкторе.
Язык С++ обеспечивает корректное освобождение памяти, выделенной подобъект, при аварийном завершении программы только, если этот объект полностьюсконструирован. Если же конструирование объекта не завершено, т. е. исключениезафиксировано в конструкторе, то автоматического освобождения памяти,ужевыделенной под часть объекта, выполнено не будет, и, следовательно, произойдет«утечка» памяти. Например, для класса, описанного ниже, при возникновенииисключения в конструкторе часть памяти останется недоступной.class D{private:A * a;B * b;ОглавлениеИванова Г.С., Ничушкина Т.Н.
«Объектно-ориентированное программирование на языке C++ в среде Microsoft Visual Studio 2008»164public:D() : a(new A), b(new B) { }~D() throw(){delete a;delete b; }};Обеспечить корректное освобождение памяти для данного класса существенносложнее.В соответствии с этим естественным является женлание упростить работу сдинамическимиобъектами.Очевидноерешение–вместообычногоуказателяиспользовать объект-указатель, хранящий адрес и освобождающий память в своёмдеструкторе.
Данная технология называется Resource Acquisition Is Initialization (RAII) –«Захват ресурса есть инициализация». Её смысл заключается в том, что захват ресурсасовмещается с инициализацией объекта, а освобождение – с уничтожением объекта.Именно таким поведением обладает шаблон auto_ptr из стандартной библиотеки C++.ОглавлениеИванова Г.С., Ничушкина Т.Н. «Объектно-ориентированное программирование на языке C++ в среде Microsoft Visual Studio 2008»1658.2Шаблон auto_ptrШаблон auto_ptr включен в стандартную библиотеку <memory> языка C++ иопределен в адресном пространстве std.
Это единственный «умный» указатель,включенный в нынешний стандарт C++.Шаблон предусматривает следующие методы:• конструкторы простой и копирующий;• деструктор;• get() – возвращает хранимый адрес;• release() – возвращает хранимый адрес и записывает вместо него NULL вобъект-указатель;• reset() – освобождает память, адрес которой хранится в указателе-объекте, и,если параметр – новый динамический объект указан, то записывает в негоновый адрес, если параметр не указан, то адрес устанавливается в NULL.Шаблон также переопределяет следующие операции:• operator=() – операцию присваивания;• operator*() – операцию доступа к адресуемому объекту по адресу;• operator->() – операцию доступа к адресуемому объекту по адресу;• operatorauto_ptr<Other>–операциюпреобразованияуказателяauto_ptr одного типа к указателю auto_ptr другого типа;• operator auto_ptr_ref<Other> –операцию преобразования указателяauto_ptr одного типа к указателю auto_ptr_ref другого типа.Чтобы построить класс, используя шаблон, необходимо задать тип динамическогообъекта, который будет храниться в объекте указателе.Пример 8.1.
Создание объекта-указателя, его использование и перезапись.#include <memory>#include <iostream>#include <conio.h>using namespace std;class AОглавлениеИванова Г.С., Ничушкина Т.Н. «Объектно-ориентированное программирование на языке C++ в среде Microsoft Visual Studio 2008»166{public:int x;A(int X):x(X){}A(){}~A(){cout<<"destructor"<<endl;}};void main(){auto_ptr<A> temp1; // неинициализированный объект-указательauto_ptr<A> temp(new A(1)); // инициализированный объект-указательA &a= *temp; // ссылка на хранимый объектcout<<"A.x="<<a.x<<endl;A *ptr = temp.get();// указатель на хранимый объектcout<<"ptr="<<ptr<<endl;temp.reset(); // освобождение указателяA *ptr1 = temp.get();// указатель на хранимый объектcout<<"ptr1="<<ptr1<<endl;getch();}Результат выполнения программы:A.x=1ptr=00396520destructorptr1=00000000Следует помнить, что шаблон определяет объект-указатель, реализующий семантику«владения», при которой всегда существует только один объект-указатель, хранящийадрес выделенного фрагмента динамической памяти.
Для этого реализация шаблонаобеспечивает «разрушающее» копирование, при котором копируемый адрес уничтожаетсяпосле переписи в новый объект-указатель. Таким образом становится невозможнымОглавлениеИванова Г.С., Ничушкина Т.Н. «Объектно-ориентированное программирование на языке C++ в среде Microsoft Visual Studio 2008»167наличие двух и более объектов-указателей, адресующих один объект, и исключаютсяошибки, связанные с повторным освобождением памяти объекта.Пример 8.2. Невозможность создания копии объекта-указателя auto_ptr.#include <conio.h>#include <memory>class A{public:void f (){}};int main(){std::auto_ptr<A> temp1(new A); // объявление и инициализация// объекта-указателяtemp1->f();// вызов метода - выполняется нормальноstd::auto_ptr<A> temp2(temp1); // объявление и инициализация// второго объекта-указателяtemp1->f();// вызов метода – не возможен, так как адрес// разрушен при копированииgetch();}Невозможность создания копий адреса не позволяет использовать указатели,построенные по шаблону auto_ptr, для создания списковых структур, поскольку впроцессе обработки список будет разрушаться.
Выходом из этой ситуации являетсяиспользование умного указателя с подсчётом ссылок – shared_ptr.ОглавлениеИванова Г.С., Ничушкина Т.Н. «Объектно-ориентированное программирование на языке C++ в среде Microsoft Visual Studio 2008»1688.3Шаблон shared_ptrШаблон shared_ptr позволяет создавать объекты-указатели, отличающиеся отобъектов-указателей auto_ptrв основном методикой копирования. Хотя данныйшаблон в настоящее время не включен в стандарт, он реализован в библиотеке boost,которая может быть интегрирована в Visual Studio 2008.Шаблон определен в адресном пространстве boost.
Он реализует разделяемое«владение» с подсчётом ссылок, при котором очередное копирование адреса приводит кувеличению специального счетчика на единицу. В свою очередь при уменьшенииколичества указателей-объектов, связанных с динамическим объектом, значение счетчикауменьшается. Все объекты-уазатели при этом равноценно владеют объектом. Согласноиспользуемой методике адресуемый объект освобождает занимаемую им память приотключении последнего адресующего его объекта-указателя.Шаблон также позволяет при инициализации задавать процедуру удаленияdeleter(), что может быть полезно для классов, требующих выполнения нестандартныхдействий при удалении динамического объекта.Кроме того, шаблон предусматривает оператор неявного преобразования объектауказателя в bool, что позволяет выполнять проверку указателя по правилам С++,например:boost::shared_ptr<A> p = ptr;if (p) { // если указатель содержит действующий адрес}При работе с объектами-указателями, построенными по шаблону shared_ptrпрограммисты часто допускают одну и ту же ошибку, создавая несколько разделяемыхуказателей на один объект без их копирования, например:A ptr_obj = new A(); // динамический объект{ // новый блокshared_ptr<A> ptr1(ptr_obj); // разделяемый указатель на// объектshared_ptr<A> ptr2(ptr_obj); // второй разделяемыйОглавлениеИванова Г.С., Ничушкина Т.Н.
«Объектно-ориентированное программирование на языке C++ в среде Microsoft Visual Studio 2008»169// указатель на тот же объект...} // в этом месте программа выдает ошибку обращения к// несуществующему объектуДело в том, что инициализация разделяемых указателей обычным указателем наобъект приводит к созданию двух счетчиков ссылок, независящих друг от друга. Каждыйиз этих счетчиков обнуляется в конце блока, поскольку оба объекта-указателяуничтожаются. Это приводит к двум попыткам удаления одного динамического объекта.Первая срабатывает, вторая – приводит к ошибке (!).Правильным решением было бы создавать оба объекта-указателя копированиемисходного объекта-указателя, тогда будет использован копирующий конструктор шаблонаshared_ptr, и, соответственно, счетчик будет один. Это приведет к единственномуудалению объекта при обнулении счетчика.Пример 8.3.