Главная страница
Навигация по странице:

  • 8.4.1. Динамическое создание и уничтожение единичных объектов

  • 8.4.3. Динамическое создание и уничтожение массивов

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница36 из 93
    1   ...   32   33   34   35   36   37   38   39   ...   93
    386
    В этом разделе, посвященном динамическим объектам, мы рассмотрим три формы оператора new: для размещения единичного объекта, для размещения массива и третью форму, называемую оператором размещения new (placement new expression). Когда хип исчерпан, этот оператор возбуждает исключение. (Разговор об исключениях будет продолжен в главе 11. В главе 15 мы расскажем об операторах new и delete применительно к классам.)
    8.4.1.
    Динамическое создание и уничтожение единичных
    объектов
    Оператор new состоит их ключевого слова new, за которым следует спецификатор типа.
    Этот спецификатор может относиться к встроенным типам или к типам классов.
    Например: new int; размещает в хипе один объект типа int. Аналогично в результате выполнения инструкции new iStack; там появится один объект класса iStack.
    Сам по себе оператор new не слишком полезен. Как можно реально воспользоваться созданным объектом? Одним из аспектов работы с памятью из хипа является то, что размещаемые в ней объекты не имеют имени. Оператор new возвращает не сам объект, а указатель на него. Все манипуляции с этим объектом производятся косвенно через указатели: int *pi = new int;
    Здесь оператор new создает один объект типа int, на который ссылается указатель pi.
    Выделение памяти из хипа во время выполнения программы называется динамическим
    выделением. Мы говорим, что память, адресуемая указателем pi, выделена динамически.
    Второй аспект, относящийся к использованию хипа, состоит в том, что эта память не инициализируется. Она содержит “мусор”, оставшийся после предыдущей работы.
    Проверка условия: if ( *pi == 0 ) вероятно, даст false, поскольку объект, на который указывает pi, содержит случайную последовательность битов. Следовательно, объекты, создаваемые с помощью оператора new
    , рекомендуется инициализировать. Программист может инициализировать объект типа int из предыдущего примера следующим образом: int *pi = new int( 0 );
    Константа в скобках задает начальное значение для создаваемого объекта; теперь pi ссылается на объект типа int, имеющий значение 0. Выражение в скобках называется
    инициализатором. Это может быть любое выражение (не обязательно константа), возвращающее значение, приводимое к типу int.

    С++ для начинающих
    387
    Оператор new выполняет следующую последовательность действий: выделяет из хипа память для объекта, затем инициализирует его значением, стоящим в скобках. Для выделения памяти вызывается библиотечная функция new(). Предыдущий оператор приблизительно эквивалентен следующей последовательности инструкций: int *pi = &ival; // указатель ссылается на этот объект не считая, конечно, того, что объект, адресуемый pi, создается библиотечной функцией new()
    и размещается в хипе. Аналогично iStack *ps = new iStack( 512 ); создает объект типа iStack на 512 элементов. В случае объекта класса значение или значения в скобках передаются соответствующему конструктору, который вызывается в случае успешного выделения памяти. (Динамическое создание объектов классов более подробно рассматривается в разделе 15.8. Оставшаяся часть данного раздела посвящена созданию объектов встроенных типов.)
    Описанные операторы new могут вызывать одну проблему: хип, к сожалению, является конечным ресурсом, и в некоторой точке выполнения программы мы можем исчерпать его. Если функция new() не может выделить затребованного количества памяти, она возбуждает исключение bad_alloc. (Обработка исключений рассматривается в главе 11.)
    Время жизни объекта, на который указывает pi, заканчивается при освобождении памяти, где этот объект размещен. Это происходит, когда pi передается оператору delete
    . Например, delete pi; освобождает память, на которую ссылается pi, завершая время жизни объекта типа int.
    Программист управляет окончанием жизни объекта, используя оператор delete в нужном месте программы. Этот оператор вызывает библиотечную функцию delete(), которая возвращает выделенную память в хип. Поскольку хип конечен, очень важно возвращать ее своевременно.
    Глядя на предыдущий пример, вы можете спросить: а что случится, если значение pi по какой-либо причине было нулевым? Не следует ли переписать этот код таким образом: delete pi;
    Нет. Язык С++ гарантирует, что оператор delete не будет вызывать функцию delete() в случае нулевого операнда. Следовательно, проверка на 0 необязательна. (Если вы явно добавите такую проверку, в большинстве реализаций она фактически будет выполнена дважды.)
    Важно понимать разницу между временем жизни указателя pi и объекта, который он адресует. Сам объект pi является глобальным и объявлен в глобальной области int ival = 0; // создаем объект типа int и инициализируем его 0
    // необходимо ли это? if ( pi != 0 )

    С++ для начинающих
    388
    видимости. Следовательно, память под него выделяется до выполнения программы и сохраняется за ним до ее завершения. Совсем не так определяется время жизни адресуемого указателем pi объекта, который создается с помощью оператора new во время выполнения. Область памяти, на которую указывает pi, выделена динамически, следовательно, pi является указателем на динамически размещенный объект типа int.
    Когда в программе встретится оператор delete, эта память будет освобождена. Однако память, отведенная самому указателю pi, не освобождается, а ее содержимое не изменяется. После выполнения delete объект pi становится висячим указателем, то есть ссылается на область памяти, не принадлежащую программе. Такой указатель служит источником трудно обнаруживаемых ошибок, поэтому сразу после уничтожения объекта ему полезно присвоить 0, обозначив таким образом, что указатель больше ни на что не ссылается.
    Оператор delete может использоваться только по отношению к указателю, который содержит адрес области памяти, выделенной в результате выполнения оператора new.
    Попытка применить delete к указателю, не ссылающемуся на такую память, приведет к непредсказуемому поведению программы. Однако, как было сказано выше, этот оператор можно применять к нулевому указателю.
    Ниже приведены примеры опасных и безопасных операторов delete:
    }
    Вот три основные ошибки, связанные с динамическим выделением памяти:

    не освободить выделенную память. В таком случае память не возвращается в хип. Эта ошибка получила название утечки памяти;

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

    изменять объект после его удаления. Такое часто случается, поскольку указатель, к которому применяется оператор delete, не обнуляется.
    Эти ошибки при работе с динамически выделяемой памятью гораздо легче допустить, нежели обнаружить и исправить. Для того чтобы помочь программисту, стандартная библиотека С++ представляет класс auto_ptr. Мы рассмотрим его в следующем подразделе. После этого мы покажем, как динамически размещать и уничтожать массивы, используя вторую форму операторов new и delete. void f() { int i; string str = "dwarves"; int *pi = &i; short *ps = 0; double *pd = new doub1e(33); delete str; // плохо: str не является динамическим объектом delete pi; // плохо: pi ссылается на локальный объект delete ps; // безопасно delete pd; // безопасно

    С++ для начинающих
    389
    8.4.2.
    Шаблон auto_ptr А
    В стандартной библиотеке С++ auto_ptr является шаблоном класса, призванным помочь программистам в манипулировании объектами, которые создаются посредством оператора new. (К сожалению, подобного шаблона для манипулирования динамическими массивами нет. Использовать auto_ptr для создания массивов нельзя, это приведет к непредсказуемым результатам.)
    Объект auto_ptr инициализируется адресом динамического объекта, созданного с помощью оператора new. Такой объект автоматически уничтожается, когда заканчивается время жизни auto_ptr. В этом подразделе мы расскажем, как ассоциировать auto_ptr с динамически размещаемыми объектами.
    Для использования шаблона класса auto_ptr необходимо включить заголовочный файл:
    #include
    Определение объекта auto_ptr имеет три формы: auto_ptr< type_pointed_to > identifier;
    Здесь type_pointed_to представляет собой тип нужного объекта. Рассмотрим последовательно каждое из этих определений. Как правило, мы хотим непосредственно инициализировать объект auto_ptr адресом объекта, созданного с помощью оператора new
    . Это можно сделать следующим образом: auto_ptr< int > pi ( new int( 1024 ) );
    В результате значением pi является адрес созданного объекта, инициализированного числом 1024. С объектом, на который указывает auto_ptr, можно работать обычным способом: else *pi *= 2;
    Объект, на который указывает pi, будет автоматически уничтожен по окончании времени жизни pi. Если указатель pi является локальным, то объект, который он адресует, будет уничтожен при выходе из блока, где он определен. Если же pi глобальный, то объект, на который он ссылается, уничтожается при выходе из программы.
    Что будет, если мы инициализируем auto_ptr адресом объекта класса, скажем, стандартного класса string? Например: auto_ptr< type_pointed_to > identifier( ptr_allocated_by_new ); auto_ptr< type_pointed_to > identifier( auto_ptr_of_same_type ); if ( *pi != 1024 )
    // ошибка, что-то не так auto_ptr< string >

    С++ для начинающих
    390
    pstr_auto( new string( "Brontosaurus" ) );
    Предположим, что мы хотим выполнить какую-то операцию со строками. С обычной строкой мы бы поступили таким образом:
    // ошибка, что-то не так
    А как обратиться к операции empty(), используя объект auto_ptr? Точно так же:
    // ошибка, что-то не так
    Создатели шаблона класса auto_ptr не в последнюю очередь стремились сохранить привычный синтаксис, употребляемый с обычными указателями, а также обеспечить дополнительные возможности автоматического удаления объекта, на который ссылается auto_ptr
    . При этом время выполнения не увеличивается. Применение встроенных функций (которые подставляются по месту вызова) позволило сделать использование объекта auto_ptr немногим более дорогим, чем непосредственное употребление указателя.
    Что произойдет, если мы проинициализируем pstr_auto2 значением pstr_auto, который является объектом auto_ptr, указывающим на строку? auto_ptr< string > pstr_auto2( pstr_auto );
    Представим, что мы непосредственно инициализировали один указатель на строку другим: string *pstr_type2( pstr_type );
    Оба указателя теперь содержат адрес одной и той же строки, и мы должны быть внимательными, чтобы не удалить строку дважды.
    В противоположность этому шаблон класса auto_ptr поддерживает понятие владения.
    Когда мы определили pstr_auto, он стал владельцем строки, адресом которой был инициализирован, и принял на себя ответственность за ее уничтожение.
    Вопрос в том, кто станет владельцем строки, когда мы инициализируем pstr_auto2 адресом, указывающим на тот же объект, что и pstr_auto? Нежелательно, чтобы оба объекта владели одной и той же строкой: это вернет нас к проблемам повторного удаления, от которых мы стремились уйти с помощью шаблона класса auto_ptr.
    Когда один объект auto_ptr инициализируется другим или получает его значение в результате присваивания, одновременно он получает и право владения адресуемым объектом. Объект auto_ptr, стоящий справа от оператора присваивания, передает право string *pstr_type = new string( "Brontosaurus" ); if ( pstr_type->empty() ) auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) ); if ( pstr_type->empty() )
    // кто несет ответственность за уничтожение строки?

    С++ для начинающих
    391
    владения и ответственность auto_ptr, стоящему слева. В нашем примере ответственность за уничтожение строки несет pstr_auto2, а не pstr_auto. pstr_auto больше не может употребляться для ссылки на эту строку.
    Аналогично ведет себя и операция присваивания. Пусть у нас есть два объекта auto_ptr: auto_ptr< int > p2( new int( 2048 ) );
    Мы можем скопировать один объекта auto_ptr в другой с помощью этой операции: p1 = p2;
    Перед присваиванием объект, на который ссылался p1, удаляется.
    После присваивания p1 владеет объектом типа int со значением 2048. p2 больше не может использоваться как ссылка на этот объект.
    Третья форма определения объекта auto_ptr создает его, но не инициализирует значением указателя на область памяти из хипа. Например: auto_ptr< int > p_auto_int;
    Поскольку p_auto_int не инициализирован адресом какого-либо объекта, значение хранящегося внутри него указателя равно 0. Разыменование таких указателей приводит к непредсказуемому поведению программы:
    *p_auto_int = 1024;
    Обычный указатель можно проверить на равенство 0: if ( pi ! = 0 ) ...;
    А как проверить, адресует auto_ptr какой-либо объект или нет? Операция get() возвращает внутренний указатель, использующийся в объекте auto_ptr. Значит, мы должны применить следующую проверку: auto_ptr< int > p1( new int( 1024 ) );
    // пока не ссылается ни на какой объект
    // ошибка: разыменование нулевого указателя if ( *p_auto_int != 1024 ) int *pi = 0;
    // проверяем, указывает ли p_auto_int на объект if ( p_auto_int.get() != 0 &&
    *p_auto_int != 1024 )

    С++ для начинающих
    392
    *p_auto_int = 1024;
    Если auto_ptr ни на что не указывает, то как заставить его адресовать что-либо?
    Другими словами, как мы можем присвоить значение внутреннему указателю объекта auto_ptr
    ? Это делается с помощью операции reset(). Например: p_auto_int.reset( new int( 1024 ) );
    Объекту auto_ptr нельзя присвоить адрес объекта, созданного с помощью оператора new
    :
    }
    В этом случае надо использовать функцию reset(), которой можно передать указатель или 0, если мы хотим обнулить объект auto_ptr. Если auto_ptr указывает на объект и является его владельцем, то этот объект уничтожается перед присваиванием нового значения внутреннему указателю auto_ptr. Например: pstr_auto.reset( new string( "Long-neck" ) );
    В последнем случае лучше, используя операцию assign(), присвоить новое значение существующей строке, чем уничтожать одну строку и создавать другую: pstr_auto->assign( "Long-neck" );
    Одна из трудностей программирования состоит в том, что получить правильный результат не всегда достаточно. Иногда накладываются и временные ограничения. Такая мелочь, как удаление и создание заново строкового объекта, вместо использования функции assign() при определенных обстоятельствах может вызвать значительное замедление работы. Подобные детали не должны вас беспокоить при проектировании, но при доводке программы на них следует обращать внимание. else
    // хорошо, присвоим ему значение void example() {
    // инициализируется нулем по умолчанию auto_ptr< int > pi;
    {
    // не поддерживается pi = new int( 5 ) ;
    } auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) );
    // "Brontosaurus" уничтожается перед присваиванием
    // более эффективный способ присвоить новое значение
    // используем операцию assign()

    С++ для начинающих
    393
    Шаблон класса auto_ptr обеспечивает значительные удобства и безопасность использования динамически выделяемой памяти. Однако все равно надо не терять бдительности, чтобы не навлечь на себя неприятности:

    нельзя инициализировать объект auto_ptr указателем, полученным не с помощью оператора new, или присвоить ему такое значение. В противном случае после применения к этому объекту оператора delete поведение программы непредсказуемо;

    два объекта auto_ptr не должны получать во владение один и тот же объект.
    Очевидный способ допустить такую ошибку – присвоить одно значение двум объектам. Менее очевидный – с помощью операции get(). Вот пример: auto_ptr< string > pstr_auto2( pstr_auto.get() );
    Операция release() гарантирует, что несколько указателей не являются владельцами одного и того же объекта. release() не только возвращает адрес объекта, на который ссылается auto_ptr, но и передает владение им.
    Предыдущий фрагмент кода нужно переписать так: pstr_auto2( pstr_auto.release() );
    8.4.3.
    Динамическое создание и уничтожение массивов
    Оператор new может выделить из хипа память для размещения массива. В этом случае после спецификатора типа в квадратных скобках указывается размер массива. Он может быть задан сколь угодно сложным выражением. new возвращает указатель на первый элемент массива. Например: int (*pia2)[ 1024 ] = new int[ 4 ][ 1024 ]; pi содержит адрес единственного элемента типа int, инициализированного значением
    1024
    ; pia – адрес первого элемента массива из 1024 элементов; pia2 – адрес начала auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) );
    // ошибка: теперь оба указывают на один объект
    // и оба являются его владельцами
    // правильно: оба указывают на один объект,
    // но pstr_auto больше не является его владельцем auto_ptr< string >
    // создание единственного объекта типа int
    // с начальным значением 1024 int *pi = new int( 1024 );
    // создание массива из 1024 элементов
    // элементы не инициализируются int *pia = new int[ 1024 ];
    // создание двумерного массива из 4x1024 элементов

    С++ для начинающих
    394
    массива, содержащего четыре массива по 1024 элемента, т.е. pia2 адресует 4096 элементов.
    В общем случае массив, размещаемый в хипе, не может быть инициализирован. (В разделе 15.8 мы покажем, как с помощью конструктора по умолчанию присвоить начальное значение динамическому массиву объектов типа класса.) Задавать инициализатор при выделении оператором new памяти под массив не разрешается.
    Массиву элементов встроенного типа, размещенному в хипе, начальные значения присваиваются с помощью цикла for: pia[ index ] = 0;
    Основное преимущество динамического массива состоит в том, что количество элементов в его первом измерении не обязано быть константой, т.е. может не быть известным во время компиляции. Для массивов, определяемых в локальной или глобальной области видимости, это не так: здесь размер задавать необходимо.
    Например, если указатель в ходе выполнения программы ссылается на разные C-строки, то область памяти под текущую строку обычно выделяется динамически и ее размер определяется в зависимости от длины строки. Как правило, это более эффективно, чем создавать массив фиксированного размера, способный вместить самую длинную строку: ведь все остальные строки могут быть значительно короче. Более того, программа может аварийно завершиться, если длина хотя бы одной из строк превысит отведенный лимит.
    Оператор new допустимо использовать для задания первого измерения массива с помощью значения, вычисляемого во время выполнения. Предположим, у нас есть следующие C-строки:
    "specify a function return type!";
    Размер создаваемого с помощью оператора new массива может быть задан значением, вычисляемым во время выполнения: strcpy( strl, errorTxt ); for (int index = 0; index < 1024; ++index ) const char *noerr = "success";
    // ... const char *err189 = "Error: a function declaration must "
    #include const char *errorTxt; if (errorFound) errorTxt = errl89; else errorTxt = noerr; int dimension = strlen( errorTxt ) + 1; char *strl = new char[ dimension ];
    // копируем текст ошибки в strl

    С++ для начинающих
    1   ...   32   33   34   35   36   37   38   39   ...   93


    написать администратору сайта