Операторы инкремента и декремента
Продолжая развивать реализацию класса ScreenPtr, введенного в предыдущем разделе, рассмотрим еще два оператора, которые поддерживаются для встроенных указателей и которые желательно иметь и для нашего интеллектуального указателя: инкремент (++) и декремент (--). Чтобы использовать класс ScreenPtr для ссылки на элементы массива объектов Screen, туда придется добавить несколько дополнительных членов.
Сначала мы определим новый член size, который содержит либо нуль (это говорит о том, что объект ScreenPtr указывает на единственный объект), либо размер массива, адресуемого объектом ScreenPtr. Нам также понадобится член offset, запоминающий смещение от начала данного массива:
class ScreenPtr {
public:
// ...
private:
int size; // размер массива: 0, если единственный объект
int offset; // смещение ptr от начала массива
Screen *ptr;
| };
Модифицируем конструктор класса ScreenPtr с учетом его новой функциональности и дополнительных членов,. Пользователь нашего класса должен передать конструктору дополнительный аргумент, если создаваемый объект указывает на массив:
class ScreenPtr {
public:
ScreenPtr( Screen &s , int arraySize = 0 )
: ptr( &s ), size ( arraySize ), offset( 0 ) { }
private:
int size;
int offset;
Screen *ptr;
| };
С помощью этого аргумента задается размер массива. Чтобы сохранить прежнюю функциональность, предусмотрим для него значение по умолчанию, равное нулю. Таким образом, если второй аргумент конструктора опущен, то член size окажется равен 0 и, следовательно, такой объект будет указывать на единственный объект Screen. Объекты нового класса ScreenPtr можно определять следующим образом:
Screen myScreen( 4, 4 );
ScreenPtr pobj( myScreen ); // правильно: указывает на один объект
const int arrSize = 10;
Screen *parray = new Screen[ arrSize ];
| ScreenPtr parr( *parray, arrSize ); // правильно: указывает на массив
Теперь мы готовы определить в ScreenPtr перегруженные операторы инкремента и декремента. Однако они бывают двух видов: префиксные и постфиксные. К счастью, можно определить оба варианта. Для префиксного оператора объявление не содержит ничего неожиданного:
class ScreenPtr {
public:
Screen& operator++();
Screen& operator--();
// ...
| };
Такие операторы определяются как унарные операторные функции. Использовать префиксный оператор инкремента можно, к примеру, следующим образом:
const int arrSize = 10;
Screen *parray = new Screen[ arrSize ];
ScreenPtr parr( *parray, arrSize );
for ( int ix = 0;
ix < arrSize;
++ix, ++parr ) // эквивалентно parr.operator++()
}
| printScreen( parr );
Определения этих перегруженных операторов приведены ниже:
Screen& ScreenPtr::operator++()
{
if ( size == 0 ) {
cerr << "не могу инкрементировать указатель для одного объекта\n";
return *ptr;
}
if ( offset >= size - 1 ) {
cerr << "уже в конце массива\n";
return *ptr;
}
++offset;
return *++ptr;
}
Screen& ScreenPtr::operator--()
{
if ( size == 0 ) {
cerr << "не могу декрементировать указатель для одного объекта\n";
return *ptr;
}
if ( offset <= 0 ) {
cerr << "уже в начале массива\n";
return *ptr;
}
--offset;
return *--ptr;
| }
Чтобы отличить префиксные операторы от постфиксных, в объявлениях последних имеется дополнительный параметр типа int. В следующем фрагменте объявлены префиксные и постфиксные варианты операторов инкремента и декремента для класса ScreenPtr:
class ScreenPtr {
public:
Screen& operator++(); // префиксные операторы
Screen& operator--();
Screen& operator++(int); // постфиксные операторы
Screen& operator--(int);
// ...
| };
Ниже приведена возможная реализация постфиксных операторов:
Screen& ScreenPtr::operator++(int)
{
if ( size == 0 ) {
cerr << "не могу инкрементировать указатель для одного объекта\n";
return *ptr;
}
if ( offset == size ) {
cerr << "уже на один элемент дальше конца массива\n";
return *ptr;
}
++offset;
return *ptr++;
}
Screen& ScreenPtr::operator--(int)
{
if ( size == 0 ) {
cerr << "не могу декрементировать указатель для одного объекта\n";
return *ptr;
}
if ( offset == -1 ) {
cerr << "уже на один элемент раньше начала массива\n";
return *ptr;
}
--offset;
return *ptr--;
| }
Обратите внимание, что давать название второму параметру нет необходимости, поскольку внутри определения оператора он не употребляется. Компилятор сам подставляет для него значение по умолчанию, которое можно игнорировать. Вот пример использования постфиксного оператора:
const int arrSize = 10;
Screen *parray = new Screen[ arrSize ];
ScreenPtr parr( *parray, arrSize );
for ( int ix = 0; ix < arrSize; ++ix)
| printScreen( parr++ );
При его явном вызове необходимо все же передать значение второго целого аргумента. В случае нашего класса ScreenPtr это значение игнорируется, поэтому может быть любым:
parr.operator++(1024); // вызов постфиксного operator++
Перегруженные операторы инкремента и декремента разрешается объявлять как дружественные функции. Изменим соответствующим образом определение класса ScreenPtr:
class ScreenPtr {
// объявления не членов
friend Screen& operator++( Screen & ); // префиксные операторы
friend Screen& operator--( Screen & );
friend Screen& operator++( Screen &, int); // постфиксные операторы
friend Screen& operator--( Screen &, int);
public:
// определения членов
| };
Упражнение 15.7
Напишите определения перегруженных операторов инкремента и декремента для класса ScreenPtr, предположив, что они объявлены как друзья класса.
Упражнение 15.8
С помощью ScreenPtr можно представить указатель на массив объектов класса Screen. Модифицируйте перегруженные operator*() и operator‑>() (см. раздел 15.6) так, чтобы указатель ни при каком условии не адресовал элемент перед началом или за концом массива. Совет: в этих операторах следует воспользоваться новыми членами size и offset.
Операторы new и delete
По умолчанию выделение объекта класса из хипа и освобождение занятой им памяти выполняются с помощью глобальных операторов new() и delete(), определенных в стандартной библиотеке C++. (Мы рассматривали эти операторы в разделе 8.4.) Но класс может реализовать и собственную стратегию управления памятью, предоставив одноименные операторы-члены. Если они определены в классе, то вызываются вместо глобальных операторов с целью выделения и освобождения памяти для объектов этого класса.
Определим операторы new() и delete() в нашем классе Screen.
Оператор-член new() должен возвращать значение типа void* и принимать в качестве первого параметра значение типа size_t, где size_t – это typedef, определенный в системном заголовочном файле <cstddef>. Вот его объявление:
class Screen {
public:
void *operator new( size_t );
// ...
| };
Когда для создания объекта типа класса используется new(), компилятор проверяет, определен ли в этом классе такой оператор. Если да, то для выделения памяти под объект вызывается именно он, в противном случае – глобальный оператор new(). Например, следующая инструкция
Screen *ps = new Screen;
создает объект Screen в хипе, а поскольку в этом классе есть оператор new(), то вызывается он. Параметр size_t оператора автоматически инициализируется значением, равным размеру Screen в байтах.
Добавление оператора new() в класс или его удаление оттуда не отражаются на пользовательском коде. Вызов new выглядит одинаково как для глобального оператора, так и для оператора-члена. Если бы в классе Screen не было собственного new(), то обращение осталось бы правильным, только вместо оператора-члена вызывался бы глобальный оператор.
С помощью оператора разрешения глобальной области видимости можно вызвать глобальный new(), даже если в классе Screen определена собственная версия:
Screen *ps = ::new Screen;
Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление для Screen:
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() не вызываются.
15.8.1. Операторы new[ ] и delete [ ]
Оператор new(), определенный в предыдущем подразделе, вызывается только при выделении памяти для единичного объекта. Так, в данной инструкции вызывается new() класса Screen:
// вызывается Screen::operator new()
| Screen *ps = new Screen( 24, 80 );
тогда как ниже вызывается глобальный оператор new[]() для выделения из хипа памяти под массив объектов типа Screen:
// вызывается Screen::operator new[]()
| Screen *psa = new Screen[10];
В классе можно объявить также операторы new[]() и delete[]() для работы с массивами.
Оператор-член new[]() должен возвращать значение типа void* и принимать в качестве первого параметра значение типа size_t. Вот его объявление для Screen:
class Screen {
public:
void *operator new[]( size_t );
// ...
| };
Когда с помощью new создается массив объектов типа класса, компилятор проверяет, определен ли в классе оператор new[](). Если да, то для выделения памяти под массив вызывается именно он, в противном случае – глобальный new[](). В следующей инструкции в хипе создается массив из десяти объектов Screen:
Screen *ps = new Screen[10];
В этом классе есть оператор new[](), поэтому он и вызывается для выделения памяти. Его параметр size_t автоматически инициализируется значением, равным объему памяти в байтах, необходимому для размещения десяти объектов Screen.
Даже если в классе имеется оператор-член new[](), программист может вызвать для создания массива глобальный new[](), воспользовавшись оператором разрешения глобальной области видимости:
Screen *ps = ::new Screen[10];
Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление для Screen:
class Screen {
public:
void operator delete[]( void * );
| };
Чтобы удалить массив объектов класса, delete должен вызываться следующим образом:
delete[] ps;
Когда операндом delete является указатель на объект типа класса, компилятор проверяет, определен ли в этом классе оператор delete[](). Если да, то для освобождения памяти вызывается именно он, в противном случае – его глобальная версия. Параметр типа void* автоматически инициализируется значением адреса начала области памяти, в которой размещен массив.
Даже если в классе имеется оператор-член delete[](), программист может вызвать глобальный delete[](), воспользовавшись оператором разрешения глобальной области видимости:
::delete[] ps;
Добавление операторов new[]() или delete[]() в класс или удаление их оттуда не отражаются на пользовательском коде: вызовы как глобальных операторов, так и операторов-членов выглядят одинаково.
При создании массива сначала вызывается new[]() для выделения необходимой памяти, а затем каждый элемент инициализируется с помощью конструктора по умолчанию. Если у класса есть хотя бы один конструктор, но нет конструктора по умолчанию, то вызов оператора new[]() считается ошибкой. Не существует синтаксической конструкции для задания инициализаторов элементов массива или аргументов конструктора класса при создании массива подобным образом.
При уничтожении массива сначала вызывается деструктор класса для уничтожения элементов, а затем оператор delete[]() – для освобождения всей памяти. При этом важно использовать правильный синтаксис. Если в инструкции
delete ps;
ps указывает на массив объектов класса, то отсутствие квадратных скобок приведет к вызову деструктора лишь для первого элемента, хотя память будет освобождена полностью.
У оператора-члена delete[]() может быть не один, а два параметра, при этом второй должен иметь тип size_t:
class Screen {
public:
// заменяет
// void operator delete[]( void* );
void operator delete[]( void*, size_t );
| };
Если второй параметр присутствует, то компилятор автоматически инициализирует его значением, равным объему отведенной под массив памяти в байтах.
Не нашли, что искали? Воспользуйтесь поиском по сайту:
©2015 - 2024 stydopedia.ru Все материалы защищены законодательством РФ.
|