Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
235
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

722

// набор перегруженных операторов присваивания

String& operator=( const String & );

String& operator=( const char * );

Отдельный оператор присваивания может существовать для каждого типа, который разрешено присваивать объекту String. Однако все такие операторы должны быть определены как функции-члены класса.

15.4. Оператор взятия индекса

Оператор взятия индекса operator[]() можно определять для классов, представляющих абстракцию контейнера, из которого извлекаются отдельные элементы. Примерами таких контейнеров могут служить наш класс String, класс IntArray, представленный в главе 2, или шаблон класса vector, определенный в стандартной библиотеке C++. Оператор взятия индекса обязан быть функцией-членом класса.

У пользователей String должна иметься возможность чтения и записи отдельных символов члена _string. Мы хотим поддержать следующий способ применения объектов

String entry( "extravagant" );

String mycopy;

for ( int ix = 0; ix < entry.size(); ++ix )

данного класса:

mycopy[ ix ] = entry[ ix ];

Оператор взятия индекса может появляться как слева, так и справа от оператора присваивания. Чтобы быть в левой части, он должен возвращать l-значение

#include <cassert>

inine char&

String::operator[]( int elem ) const

{

assert( elem >= 0 && elem < _size ); return _string[ elem ];

индексируемого элемента. Для этого мы возвращаем ссылку:

}

String color( "violet" );

Вследующем фрагменте нулевому элементу массива color присваивается символ 'V': color[ 0 ] = 'V';

Обратите внимание, что в определении оператора проверяется выход индекса за границы массива. Для этого используется библиотечная C-функция assert(). Можно также возбудить исключение, показывающее, что значение elem меньше 0 или больше длины C-

С++ для начинающих

723

строки, на которую ссылается _string. (Возбуждение и обработка исключений обсуждались в главе 11.)

15.5. Оператор вызова функции

Оператор вызова функции может быть перегружен для объектов типа класса. (Мы уже видели, как он используется, при рассмотрении объектов-функций в разделе 12.3.) Если определен класс, представляющий некоторую операцию, то для ее вызова перегружается соответствующий оператор. Например, для взятия абсолютного значения числа типа int

class absInt { public:

int operator()( int val ) {

int result = val < 0 ? -val : val; return result;

}

можно определить класс absInt:

};

Перегруженный оператор operator() должен быть объявлен как функция-член с произвольным числом параметров. Параметры и возвращаемое значение могут иметь любые типы, допустимые для функций (см. разделы 7.2, 7.3 и 7.4). operator() вызывается путем применения списка аргументов к объекту того класса, в котором он определен. Мы рассмотрим, как он используется в одном из обобщенных алгоритмов, описанных в главе 12. В следующем примере обобщенный алгоритм transform()

вызывается для применения определенной в absInt операции к каждому элементу

#include <vector> #include <algoritm>

int main() {

int ia[] = { -0, 1, -1, -2, 3, 5, -5, 8 }; vector< int > ivec( ia, ia+8 );

//заменить каждый элемент его абсолютным значением transform( ivec.begin(), ivec.end(), ivec.begin(), absInt() );

//...

вектора ivec, т.е. для замены элемента его абсолютным значением.

}

Первый и второй аргументы transform() ограничивают диапазон элементов, к которым применяется операция absInt. Третий указывает на начало вектора, где будет сохранен результат применения операции.

Четвертый аргумент это временный объект класса absInt, создаваемый с помощью конструктора по умолчанию. Конкретизация обобщенного алгоритма transform(), вызываемого из main(), могла бы выглядеть так:

С++ для начинающих

724

typedef vector< int >::iterator iter_type;

//конкретизация transform()

//операция absInt применяется к элементу вектора int

iter_type transform( iter_type iter, iter_type last, iter_type result, absInt func )

{

while ( iter != last )

*result++ = func( *iter++ ); // вызывается absInt::operator()

return iter;

}

func это объект класса, который предоставляет операцию absInt, заменяющую число типа int его абсолютным значением. Он используется для вызова перегруженного оператора operator() класса absInt. Этому оператору передается аргумент *iter, указывающий на тот элемент вектора, для которого мы хотим получить абсолютное значение.

15.6. Оператор стрелка

Оператор стрелка”, разрешающий доступ к членам, может перегружаться для объектов класса. Он должен быть определен как функция-член и обеспечивать семантику указателя. Чаще всего этот оператор используется в классах, которые предоставляют интеллектуальный указатель” (smart pointer), ведущий себя аналогично встроенным, но поддерживают и некоторую дополнительную функциональность.

Допустим, мы хотим определить тип класса для представления указателя на объект

class ScreenPtr { // ...

private: Screen *ptr;

Screen (см. главу 13):

};

Определение ScreenPtr должно быть таким, чтобы объект этого класса гарантировано указывал на объект Screen: в отличие от встроенного указателя, он не может быть нулевым. Тогда приложение сможет пользоваться объектами типа ScreenPtr, не проверяя, указывают ли они на какой-нибудь объект Screen. Для этого нужно определить класс ScreenPtr с конструктором, но без конструктора по умолчанию

class ScreenPtr { public:

ScreenPtr( const Screen &s ) : ptr( &s ) { } // ...

(детально конструкторы рассматривались в разделе 14.2):

};

С++ для начинающих

725

В любом определении объекта класса

ScreenPtr должен присутствовать

 

ScreenPtr p1; // ошибка: у класса ScreenPtr нет конструктора по

 

 

умолчанию

 

 

Screen myScreen( 4, 4 );

 

инициализатор объект класса Screen, на который будет ссылаться объект ScreenPtr:

ScreenPtr ps( myScreen ); // правильно

Чтобы класс ScreenPtr вел себя как встроенный указатель, необходимо определить некоторые перегруженные операторы разыменования (*) и стрелкудля доступа к

// перегруженные операторы для поддержки поведения указателя class ScreenPtr {

public:

Screen& operator*() { return *ptr; } Screen* operator->() { return ptr; } // ...

членам:

};

Оператор доступа к членам унарный, поэтому параметры ему не передаются. При

использовании в составе выражения его результат зависит только от типа левого операнда. Например, в инструкции

point->action();

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

Перегруженный оператор стрелкадолжен возвращать либо указатель на тип класса, либо объект класса, в котором он определен. Если возвращается указатель, то к нему применяется семантика встроенного оператора стрелка”. В противном случае процесс продолжается рекурсивно, пока не будет получен указатель или определена ошибка. Например, так можно воспользоваться объектом ps класса ScreenPtr для доступа к членам Screen:

ps->move( 2, 3 );

Поскольку слева от оператора стрелканаходится объект типа ScreenPtr, то употребляется перегруженный оператор этого класса, который возвращает указатель на объект Screen. Затем к полученному значению применяется встроенный оператор стрелкадля вызова функции-члена move().

Ниже приводится небольшая программа для тестирования класса ScreenPtr. Объект типа ScreenPtr используется точно так же, как любой объект типа Screen*:

С++ для начинающих

726

#include <iostream> #include <string> #include "Screen.h"

void printScreen( const ScreenPtr &ps )

{

cout << "Screen Object ( "

<<ps->height() << ", "

<<ps->width() << " )\n\n";

for ( int ix = 1; ix <= ps->height(); ++ix )

{

for ( int iy = 1; iy <= ps->width(); ++iy ) cout << ps->get( ix, iy );

cout << "\n";

}

}

int main() {

Screen sobj( 2, 5 );

string init( "HelloWorld" ); ScreenPtr ps( sobj );

// Установить содержимое экрана string::size_type initpos = 0;

for ( int ix = 1; ix <= ps->height(); ++ix ) for ( int iy = 1; iy <= ps->width(); ++iy )

{

ps->move( ix, iy );

ps->set( init[ initpos++ ] );

}

// Вывести содержимое экрана printScreen( ps );

return 0;

}

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

15.7. Операторы инкремента и декремента

Продолжая развивать реализацию класса ScreenPtr, введенного в предыдущем разделе, рассмотрим еще два оператора, которые поддерживаются для встроенных указателей и которые желательно иметь и для нашего интеллектуального указателя: инкремент (++) и

декремент (--). Чтобы использовать класс ScreenPtr для ссылки на элементы массива объектов Screen, туда придется добавить несколько дополнительных членов.

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

С++ для начинающих

727

 

 

class ScreenPtr {

 

 

 

 

 

 

public:

 

 

 

// ...

 

 

 

private:

// размер массива: 0, если единственный объект

 

 

int size;

 

 

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. Объекты

Screen myScreen( 4, 4 );

ScreenPtr pobj( myScreen ); // правильно: указывает на один объект const int arrSize = 10;

Screen *parray = new Screen[ arrSize ];

нового класса ScreenPtr можно определять следующим образом:

ScreenPtr parr( *parray, arrSize ); // правильно: указывает на массив

Теперь мы готовы определить в ScreenPtr перегруженные операторы инкремента и декремента. Однако они бывают двух видов: префиксные и постфиксные. К счастью, можно определить оба варианта. Для префиксного оператора объявление не содержит

class ScreenPtr { public:

Screen& operator++(); Screen& operator--(); // ...

ничего неожиданного:

};

Такие операторы определяются как унарные операторные функции. Использовать префиксный оператор инкремента можно, к примеру, следующим образом:

С++ для начинающих

728

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. В следующем фрагменте объявлены

префиксные и постфиксные варианты операторов инкремента и декремента для класса

class ScreenPtr {

 

public:

// префиксные операторы

Screen& operator++();

Screen& operator--();

 

Screen& operator++(int); // постфиксные операторы

Screen& operator--(int);

 

// ...

 

ScreenPtr:

};

С++ для начинающих

729

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:

С++ для начинающих

730

 

 

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.

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

По умолчанию выделение объекта класса из хипа и освобождение занятой им памяти выполняются с помощью глобальных операторов new() и delete(), определенных в стандартной библиотеке C++. (Мы рассматривали эти операторы в разделе 8.4.) Но класс может реализовать и собственную стратегию управления памятью, предоставив одноименные операторы-члены. Если они определены в классе, то вызываются вместо

глобальных операторов с целью выделения и освобождения памяти для объектов этого класса.

Определим операторы new() и delete() в нашем классе Screen.

Оператор-член new() должен возвращать значение типа void* и принимать в качестве первого параметра значение типа size_t, где size_t это typedef, определенный в

class Screen { public:

void *operator new( size_t ); // ...

системном заголовочном файле <cstddef>. Вот его объявление:

};

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

следующая инструкция

Screen *ps = new Screen;

С++ для начинающих

731

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

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

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

Screen *ps = ::new Screen;

Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление

class Screen { public:

void operator delete( void * );

для Screen:

};

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

инструкция

delete ps;

освобождает память, занятую объектом класса Screen, на который указывает ps. Поскольку в Screen есть оператор-член delete(), то применяется именно он. Параметр оператора типа void* автоматически инициализируется значением ps.

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

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

::delete ps;

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