Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
Оператор delete(), определенный для типа класса, может содержать два параметра вместо одного. Первый параметр по-прежнему должен иметь тип void*, а второй – предопределенный тип size_t (не забудьте включить заголовочный файл }; Если второй параметр есть, компилятор автоматически инициализирует его значением, равным размеру адресованного первым параметром объекта в байтах. (Этот параметр важен в иерархии классов, когда оператор delete() может наследоваться производным классом. Подробнее наследование обсуждается в главе 17.) Рассмотрим реализацию операторов new() и delete() в классе Screen более детально. В основе нашей стратегии распределения памяти будет лежать связанный список объектов Screen , на начало которого указывает член freeStore. При каждом обращении к оператору-члену new() возвращается следующий объект из списка. При вызове delete() объект возвращается в список. Если при создании нового объекта список, адресованный freeStore , пуст, то вызывается глобальный оператор new(), чтобы получить блок памяти, достаточный для хранения screenChunk объектов класса Screen. Как screenChunk, так и freeStore представляют интерес только для Screen, поэтому мы сделаем их закрытыми членами. Кроме того, для всех создаваемых объектов нашего класса значения этих членов должны быть одинаковыми, а следовательно, нужно объявить их статическими. Чтобы поддержать структуру связанного списка объектов Screen , нам понадобится третий член next: }; Вот одна из возможных реализаций оператора new() для класса Screen: class Screen { public: // заменяет // void operator delete( void * ); void operator delete( void *, size_t ); class Screen { public: void *operator new( size_t ); void operator delete( void *, size_t ); // ... private: Screen *next; static Screen *freeStore; static const int screenChunk; С++ для начинающих 733 } А вот реализация оператора delete(): } Оператор new() можно объявить в классе и без соответствующего delete(). В таком случае объекты освобождаются с помощью одноименного глобального оператора. Разрешается также объявить и оператор delete() без new(): объекты будут создаваться с помощью одноименного глобального оператора. Однако обычно эти операторы реализуются одновременно, как в примере выше, поскольку разработчику класса, как правило, нужны оба. Они являются статическими членами класса, даже если программист явно не объявит их таковыми, и подчиняются обычным ограничениями для подобных функций-членов: им не передается указатель this, а следовательно, напрямую они могут получить доступ только к статическим членам. (См. обсуждение статических функций-членов в разделе 13.5.) Причина, по которой эти операторы делаются статическими, заключается в том, #include "Screen.h" #include // статические члены инициализируются // в исходных файлах программы, а не в заголовочных файлах 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; void Screen::operator delete( void *p, size_t ) { // вставить "удаленный" объект назад, // в список свободных ( static_cast< Screen* >( p ) )->next = freeStore; freeStore = static_cast< Screen* >( p ); С++ для начинающих 734 что они вызываются либо перед конструированием объекта класса (new()), либо после его уничтожения (delete()). Выделение памяти с помощью оператора new(), например: Screen *ptr = new Screen( 10, 20 ); эквивалентно последовательному выполнению таких инструкций: Screen::Screen( ptr, 10, 20 ); Иными словами, сначала вызывается определенный в классе оператор new(), чтобы выделить память для объекта, а затем этот объект инициализируется конструктором. Если new() неудачно завершает работу, то возбуждается исключение типа bad_alloc и конструктор не вызывается. Освобождение памяти с помощью оператора delete(), например: delete ptr; эквивалентно последовательному выполнению таких инструкций: Screen::operator delete( ptr, sizeof( *ptr ) ); Таким образом, при уничтожении объекта сначала вызывается деструктор класса, а затем определенный в классе оператор delete() для освобождения памяти. Если значение ptr равно 0, то ни деструктор, ни delete() не вызываются. 15.8.1. Операторы new[ ] и delete [ ] Оператор new(), определенный в предыдущем подразделе, вызывается только при выделении памяти для единичного объекта. Так, в данной инструкции вызывается new() класса Screen: Screen *ps = new Screen( 24, 80 ); тогда как ниже вызывается глобальный оператор new[]() для выделения из хипа памяти под массив объектов типа Screen: // Псевдокод на C++ ptr = Screen::operator new( sizeof( Screen ) ); // Псевдокод на C++ Screen::Screen( ptr ); // вызывается Screen::operator new() // вызывается Screen::operator new[]() С++ для начинающих 735 Screen *psa = new Screen[10]; В классе можно объявить также операторы new[]() и delete[]() для работы с массивами. Оператор-член new[]() должен возвращать значение типа void* и принимать в качестве первого параметра значение типа size_t. Вот его объявление для Screen: }; Когда с помощью new создается массив объектов типа класса, компилятор проверяет, определен ли в классе оператор new[](). Если да, то для выделения памяти под массив вызывается именно он, в противном случае – глобальный new[](). В следующей инструкции в хипе создается массив из десяти объектов Screen: Screen *ps = new Screen[10]; В этом классе есть оператор new[](), поэтому он и вызывается для выделения памяти. Его параметр size_t автоматически инициализируется значением, равным объему памяти в байтах, необходимому для размещения десяти объектов Screen. Даже если в классе имеется оператор-член new[](), программист может вызвать для создания массива глобальный new[](), воспользовавшись оператором разрешения глобальной области видимости: Screen *ps = ::new Screen[10]; Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление для Screen: }; Чтобы удалить массив объектов класса, delete должен вызываться следующим образом: delete[] ps; Когда операндом delete является указатель на объект типа класса, компилятор проверяет, определен ли в этом классе оператор delete[](). Если да, то для освобождения памяти вызывается именно он, в противном случае – его глобальная версия. Параметр типа void* автоматически инициализируется значением адреса начала области памяти, в которой размещен массив. class Screen { public: void *operator new[]( size_t ); // ... class Screen { public: void operator delete[]( void * ); С++ для начинающих 736 Даже если в классе имеется оператор-член delete[](), программист может вызвать глобальный delete[](), воспользовавшись оператором разрешения глобальной области видимости: ::delete[] ps; Добавление операторов new[]() или delete[]() в класс или удаление их оттуда не отражаются на пользовательском коде: вызовы как глобальных операторов, так и операторов-членов выглядят одинаково. При создании массива сначала вызывается new[]() для выделения необходимой памяти, а затем каждый элемент инициализируется с помощью конструктора по умолчанию. Если у класса есть хотя бы один конструктор, но нет конструктора по умолчанию, то вызов оператора new[]() считается ошибкой. Не существует синтаксической конструкции для задания инициализаторов элементов массива или аргументов конструктора класса при создании массива подобным образом. При уничтожении массива сначала вызывается деструктор класса для уничтожения элементов, а затем оператор delete[]() – для освобождения всей памяти. При этом важно использовать правильный синтаксис. Если в инструкции delete ps; ps указывает на массив объектов класса, то отсутствие квадратных скобок приведет к вызову деструктора лишь для первого элемента, хотя память будет освобождена полностью. У оператора-члена delete[]() может быть не один, а два параметра, при этом второй должен иметь тип size_t: }; Если второй параметр присутствует, то компилятор автоматически инициализирует его значением, равным объему отведенной под массив памяти в байтах. 15.8.2. Оператор размещения new() и оператор delete() Оператор-член new() может быть перегружен при условии, что все объявления имеют разные списки параметров. Первый параметр должен иметь тип size_t: class Screen { public: // заменяет // void operator delete[]( void* ); void operator delete[]( void*, size_t ); class Screen { public: void *operator new( size_t ); void *operator new( size_t, Screen * ); // ... С++ для начинающих 737 }; Остальные параметры инициализируются аргументами размещения, заданными при вызове new: } Та часть выражения, которая находится после ключевого слова new и заключена в круглые скобки, представляет аргументы размещения. В примере выше вызывается оператор new(), принимающий два параметра. Первый автоматически инициализируется значением, равным размеру класса Screen в байтах, а второй – значением аргумента размещения start. Можно также перегружать и оператор-член delete(). Однако такой оператор никогда не вызывается из выражения delete. Перегруженный delete() неявно вызывается компилятором, если конструктор, вызванный при выполнении оператора new (это не опечатка, мы действительно имеем в виду new), возбуждает исключение. Рассмотрим использование delete() более внимательно. Последовательность действий при вычислении выражения Screen *ps = new ( start ) Screen; такова: 1. Вызывается определенный в классе оператор new(size_t, Screen*). 2. Вызывается конструктор по умолчанию класса Screen для инициализации созданного объекта. Переменная ps инициализируется адресом нового объекта Screen. Предположим, что оператор класса new(size_t, Screen*) выделяет память с помощью глобального new(). Как разработчик может гарантировать, что память будет освобождена, если вызванный на шаге 2 конструктор возбуждает исключение? Чтобы защитить пользовательский код от утечки памяти, следует предоставить перегруженный оператор delete(), который вызывается только в подобной ситуации. Если в классе имеется перегруженный оператор с параметрами, типы которых соответствуют типам параметров new(), то компилятор автоматически вызывает его для освобождения памяти. Предположим, есть следующее выражение с оператором размещения new: Screen *ps = new (start) Screen; Если конструктор по умолчанию класса Screen возбуждает исключение, то компилятор ищет delete() в области видимости Screen. Чтобы такой оператор был найден, типы его параметров должны соответствовать типам параметров вызванного new(). Поскольку первый параметр new() всегда имеет тип size_t, а оператора delete() – void*, то первые параметры при сравнении не учитываются. Компилятор ищет в классе Screen оператор delete() следующего вида: void func( Screen *start ) { Screen *ps = new (start) Screen; // ... С++ для начинающих 738 void operator delete( void*, Screen* ); Если такой оператор будет найден, то он вызывается для освобождения памяти в случае, когда new() возбуждает исключение. (Иначе – не вызывается.) Разработчик класса принимает решение, предоставлять ли delete(), соответствующий некоторому new(), в зависимости от того, выделяет ли этот оператор new() память самостоятельно или пользуется уже выделенной. В первом случае delete() необходимо включить для освобождения памяти, если конструктор возбудит исключение; иначе в нем нет необходимости. Можно также перегрузить оператор размещения new[]() и оператор delete[]() для массивов: }; Оператор new[]() используется в случае, когда в выражении, содержащем new для распределения массива, заданы соответствующие аргументы размещения: } Если при работе оператора new конструктор возбуждает исключение, то автоматически вызывается соответствующий delete[](). Упражнение 15.9 Объясните, какие из приведенных инициализаций ошибочны: }; class Screen { public: void *operator new[]( size_t ); void *operator new[]( size_t, Screen* ); void operator delete[]( void*, size_t ); void operator delete[]( void*, Screen* ); // ... void func( Screen *start ) { // вызывается Screen::operator new[]( size_t, Screen* ) Screen *ps = new (start) Screen[10]; // ... class iStack { public: iStack( int capacity ) : _stack( capacity ), _top( 0 ) {} // ... private: int _top; vatcor< int > _stack; С++ для начинающих 739 (c) iStack *ps3 = new iStack[ 100 ]; Упражнение 15.10 Что происходит в следующих выражениях, содержащих new и delete? delete[] ps; Измените эти выражения так, чтобы вызывались глобальные операторы new() и delete() Упражнение 15.11 Объясните, зачем разработчик класса должен предоставлять оператор delete(). 15.9. Определенные пользователем преобразования Мы уже видели, как преобразования типов применяются к операндам встроенных типов: в разделе 4.14 этот вопрос рассматривался на примере операндов встроенных операторов, а в разделе 9.3 – на примере фактических аргументов вызванной функции для приведения их к типам формальных параметров. Рассмотрим с этой точки зрения следующие шесть операций сложения: ival + sh; sh + ival; Операнды ch и sh расширяются до типа int. При выполнении операции складываются два значения типа int. Расширение типа неявно выполняется компилятором и для пользователя прозрачно. В этом разделе мы рассмотрим, как разработчик может определить собственные преобразования для объектов типа класса. Такие определенные пользователем преобразования также автоматически вызываются компилятором по мере необходимости. Чтобы показать, зачем они нужны, обратимся снова к классу SmallInt, введенному в разделе 10.9. (a) iStack *ps = new iStack(20); (b) iStack *ps2 = new const iStack(15); class Exercise { public: Exercise(); Exercise(); }; Exercise *pe = new Exercise[20]; char ch; short sh;, int ival; /* в каждой операции один операнд * требует преобразования типа */ ch + ival; ival + ch; ch + sh; ch + ch; С++ для начинающих 740 Напомним, что SmallInt позволяет определять объекты, способные хранить значения из того же диапазона, что unsigned char, т.е. от 0 до 255, и перехватывает ошибки выхода за его границы. Во всех остальных отношениях этот класс ведет себя точно так же, как unsigned char Чтобы иметь возможность складывать объекты SmallInt с другими объектами того же класса или со значениями встроенных типов, а также вычитать их, реализуем шесть операторных функций: }; Операторы-члены дают возможность складывать и вычитать два объекта SmallInt. Глобальные же операторы-друзья позволяют производить эти операции над объектами данного класса и объектами встроенных арифметических типов. Необходимо только шесть операторов, поскольку любой встроенный арифметический тип может быть приведен к типу int. Например, выражение si + 3.14159 разрешается в два шага: 1. Константа 3.14159 типа double преобразуется в целое число 3. 2. Вызывается operator+(const SmallInt &,int), который возвращает значение 6. Если мы хотим поддержать битовые и логические операции, а также операции сравнения и составные операторы присваивания, то сколько же необходимо перегрузить операторов? Сразу и не сосчитаешь. Значительно удобнее автоматически преобразовать объект класса SmallInt в объект типа int. В языке C++ имеется механизм, позволяющий в любом классе задать набор преобразований, применимых к его объектам. Для SmallInt мы определим приведение объекта к типу int. Вот его реализация: class SmallInt { friend operator+( const SmallInt &, int ); friend operator-( const SmallInt &, int ); friend operator-( int, const SmallInt & ); friend operator+( int, const SmallInt & ); public: SmallInt( int ival ) : value( ival ) { } operator+( const SmallInt & ); operator-( const SmallInt & ); // ... private: int value; SmallInt si( 3 ); С++ для начинающих 741 }; Оператор int() – это конвертер, реализующий определенное пользователем преобразование, в данном случае приведение типа класса к заданному типу int. Определение конвертера описывает, что означает преобразование и какие действия компилятор должен выполнить для его применения. Для объекта SmallInt смысл преобразования в int заключается в том, чтобы вернуть число типа int, хранящееся в члене value. Теперь объект класса SmallInt можно использовать всюду, где допустимо использование int. Если предположить, что перегруженных операторов больше нет и в SmallInt определен конвертер в int, операция сложения si + 3.14159 разрешается двумя шагами: 1. Вызывается конвертер класса SmallInt, который возвращает целое число 3. 2. Целое число 3 расширяется до 3.0 и складывается с константой двойной точности 3.14159, что дает 6.14159. Такое поведение больше соответствует поведению операндов встроенных типов по сравнению с определенными ранее перегруженными операторами. Когда значение типа int складывается со значением типа double, то выполняется сложение двух чисел типа double (поскольку тип int расширяется до double) и результатом будет число того же типа. В этой программе иллюстрируется применение класса SmallInt: class SmallInt { public: SmallInt( int ival ) : value( ival ) { } // конвертер // SmallInt ==> int operator int() { return value; } // перегруженные операторы не нужны private: int value; SmallInt si( 3 ); |