Язык программирования C++. Вводный курс

Операторы new и delete


Каждая программа во время работы получает определенное количество памяти, которую можно использовать. Такое выделение памяти под объекты во время выполнения называется динамическим, а сама память выделяется из хипа (heap). (Мы уже касались вопроса о динамическом выделении памяти в главе 1.) Напомним, что выделение памяти объекту производится с помощью оператора new,  возвращающего указатель на вновь созданный объект того типа, который был ему задан. Например:

int *pi =

new int;

размещает объект типа int в памяти и инициализирует указатель pi адресом этого объекта. Сам объект в таком случае не инициализируется, но это легко изменить:

int *pi = new int( 1024 );

Можно динамически выделить память под массив:

int *pia = new int[ 10 ];

Такая инструкция размещает в памяти массив встроенного типа из десяти элементов типа int. Для подобного массива нельзя задать список начальных значений его элементов при динамическом размещении. (Однако если размещается массив объектов типа класса, то для каждого из элементов вызывается конструктор по умолчанию.) Например:

string *ps = new string;

размещает в памяти один объект типа string, инициализирует ps его адресом и вызывает конструктор по умолчанию для вновь созданного объекта типа string. Аналогично

string *psa = new string[10];

размещает в памяти массив из десяти элементов типа string, инициализирует psa его адресом и вызывает конструктор по умолчанию для каждого элемента массива.

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



После использования объекта, созданного таким образом, мы должны явно освободить память, применив оператор delete к указателю на этот объект. (Попытка применить оператор delete к указателю, не содержащему адрес объекта, полученного описанным способом, вызовет ошибку времени выполнения.) Например:

delete pi;

освобождает память, на которую указывает объект типа int, на который указывает pi. Аналогично

delete ps;

освобождает память, на которую указывает объект класса string, адрес которого содержится в ps. Перед уничтожением этого объекта вызывается деструктор. Выражение

delete [] pia;

освобождает память, отведенную под массив pia. При выполнении такой операции необходимо придерживаться указанного синтаксиса.

(Об операциях new и delete мы еще поговорим в главе 8.)

Упражнение 4.11

Какие из следующих выражений ошибочны?

(a) vector<string> svec( 10 );

(b) vector<string> *pvecl = new vector<string>(10);

(c) vector<string> **pvec2 = new vector<string>[10];

(d) vector<string> *pvl = &svec;

(e) vector<string> *pv2 = pvecl;

(f) delete svec;

(g) delete pvecl;

(h) delete [] pvec2;

(i) delete pvl;

(j) delete pv2;




class Screen {

public:

   void operator delete( void * );
};

Когда операндом delete

служит указатель на объект типа класса, компилятор проверяет, определен ли в этом классе оператор delete(). Если да, то для освобождения памяти вызывается именно он, в противном случае – глобальная версия оператора. Следующая инструкция

delete ps;

освобождает память, занятую объектом класса Screen, на который указывает ps. Поскольку в Screen

есть оператор-член delete(), то применяется именно он. Параметр оператора типа void*

автоматически инициализируется значением ps.

Добавление delete() в класс или его удаление оттуда никак не сказываются на пользовательском коде. Вызов delete

выглядит одинаково как для глобального оператора, так и для оператора-члена. Если бы в классе Screen не было собственного оператора delete(), то обращение осталось бы правильным, только вместо оператора-члена вызывался бы глобальный оператор.

С помощью оператора разрешения глобальной области видимости можно вызвать глобальный delete(), даже если в Screen

определена собственная версия:

::delete ps;

В общем случае используемый оператор delete()

должен соответствовать тому оператору new(), с помощью которого была выделена память. Например, если ps указывает на область памяти, выделенную глобальным new(), то для ее освобождения следует использовать глобальный же delete().

Оператор delete(), определенный для типа класса, может содержать два параметра вместо одного. Первый параметр по-прежнему должен иметь тип void*, а второй – предопределенный тип size_t (не забудьте включить заголовочный файл <cstddef>):



class Screen {

public:

   // заменяет

   // void operator delete( void * );

   void operator delete( void *, size_t );
};

Если второй параметр есть, компилятор автоматически инициализирует его значением, равным размеру адресованного первым параметром объекта в байтах. (Этот параметр важен в иерархии классов, когда оператор delete() может наследоваться производным классом. Подробнее наследование обсуждается в главе 17.)



Рассмотрим реализацию операторов new()

и delete() в классе Screen

более детально. В основе нашей стратегии распределения памяти будет лежать связанный список объектов Screen, на начало которого указывает член freeStore. При каждом обращении к оператору-члену new() возвращается следующий объект из списка. При вызове delete()

объект возвращается в список. Если при создании нового объекта список, адресованный freeStore, пуст, то вызывается глобальный оператор new(), чтобы получить блок памяти, достаточный для хранения screenChunk объектов класса Screen.

Как screenChunk, так и freeStore

представляют интерес только для Screen, поэтому мы сделаем их закрытыми членами. Кроме того, для всех создаваемых объектов нашего класса значения этих членов должны быть одинаковыми, а следовательно, нужно объявить их статическими. Чтобы поддержать структуру связанного списка объектов Screen, нам понадобится третий член next:



class Screen {

public:

   void *operator new( size_t );

   void operator delete( void *, size_t );

   // ...

private:

   Screen *next;

   static Screen *freeStore;

   static const int screenChunk;
};

Вот одна из возможных реализаций оператора new() для класса Screen:



#include "Screen.h"

#include <cstddef>

// статические члены инициализируются

// в исходных файлах программы, а не в заголовочных файлах

Screen *Screen::freeStore = 0;

const int Screen::screenChunk = 24;

void *Screen::operator new( size_t size )

{

   Screen *p;

   if ( !freeStore ) {

      // связанный список пуст: получить новый блок

      // вызывается глобальный оператор new

      size_t chunk = screenChunk * size;

      freeStore = p =

         reinterpret_cast< Screen* >( new char[ chunk ] );

      // включить полученный блок в список

      for ( ;

            p != &freeStore[ screenChunk - 1 ];

            ++p )

          p->next = p+1;

      p->next = 0;

    }

    p = freeStore;

    freeStore = freeStore->next;

    return p;
<


}

А вот реализация оператора delete():



void Screen::operator delete( void *p, size_t )

{

   // вставить "удаленный" объект назад,

   // в список свободных

   ( static_cast< Screen* >( p ) )->next = freeStore;

   freeStore = static_cast< Screen* >( p );
}

Оператор new()

можно объявить в классе и без соответствующего delete(). В таком случае объекты освобождаются с помощью одноименного глобального оператора. Разрешается также объявить и оператор delete() без new(): объекты будут создаваться с помощью одноименного глобального оператора. Однако обычно эти операторы реализуются одновременно, как в примере выше, поскольку разработчику класса, как правило, нужны оба.

Они являются статическими членами класса, даже если программист явно не объявит их таковыми, и подчиняются обычным ограничениями для подобных функций-членов: им не передается указатель this, а следовательно, напрямую они могут получить доступ только к статическим членам. (См. обсуждение статических функций-членов в разделе 13.5.) Причина, по которой эти операторы делаются статическими, заключается в том, что они вызываются либо перед конструированием объекта класса (new()), либо после его уничтожения (delete()).

Выделение памяти с помощью оператора

new(), например:

Screen *ptr = new Screen( 10, 20 );

эквивалентно последовательному выполнению таких инструкций:



// Псевдокод на C++

ptr = Screen::operator new( sizeof( Screen ) );
Screen::Screen( ptr, 10, 20 );

Иными словами, сначала вызывается определенный в классе оператор new(), чтобы выделить память для объекта, а затем этот объект инициализируется конструктором. Если new()

неудачно завершает работу, то возбуждается исключение типа bad_alloc и конструктор не вызывается.

Освобождение памяти с помощью оператора delete(), например:

delete ptr;

эквивалентно последовательному выполнению таких инструкций:



// Псевдокод на C++

Screen::~Screen( ptr );
Screen::operator delete( ptr, sizeof( *ptr ) );

Таким образом, при уничтожении объекта сначала вызывается деструктор класса, а затем определенный в классе оператор delete() для освобождения памяти. Если значение ptr равно 0, то ни деструктор, ни delete() не вызываются.


Содержание раздела