Главная страница

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


Скачать 5.41 Mb.
НазваниеС для начинающих
Дата24.08.2022
Размер5.41 Mb.
Формат файлаpdf
Имя файлаЯзык программирования C++. Вводный курс.pdf
ТипДокументы
#652350
страница69 из 93
1   ...   65   66   67   68   69   70   71   72   ...   93
722
String& operator=( const char * );
Отдельный оператор присваивания может существовать для каждого типа, который разрешено присваивать объекту String. Однако все такие операторы должны быть определены как функции-члены класса.
15.4.
Оператор взятия индекса
Оператор взятия индекса operator[]() можно определять для классов, представляющих абстракцию контейнера, из которого извлекаются отдельные элементы. Примерами таких контейнеров могут служить наш класс String, класс IntArray, представленный в главе
2, или шаблон класса vector, определенный в стандартной библиотеке C++. Оператор взятия индекса обязан быть функцией-членом класса.
У пользователей String должна иметься возможность чтения и записи отдельных символов члена _string. Мы хотим поддержать следующий способ применения объектов данного класса: mycopy[ ix ] = entry[ ix ];
Оператор взятия индекса может появляться как слева, так и справа от оператора присваивания. Чтобы быть в левой части, он должен возвращать l-значение индексируемого элемента. Для этого мы возвращаем ссылку:
}
В следующем фрагменте нулевому элементу массива color присваивается символ 'V': color[ 0 ] = 'V';
Обратите внимание, что в определении оператора проверяется выход индекса за границы массива. Для этого используется библиотечная C-функция assert(). Можно также возбудить исключение, показывающее, что значение elem меньше 0 или больше длины C-
// набор перегруженных операторов присваивания
String& operator=( const String & );
String entry( "extravagant" );
String mycopy; for ( int ix = 0; ix < entry.size(); ++ix )
#include inine char&
String::operator[]( int elem ) const
{ assert( elem >= 0 && elem < _size ); return _string[ elem ];
String color( "violet" );

С++ для начинающих
723
строки, на которую ссылается _string. (Возбуждение и обработка исключений обсуждались в главе 11.)
15.5.
Оператор вызова функции
Оператор вызова функции может быть перегружен для объектов типа класса. (Мы уже видели, как он используется, при рассмотрении объектов-функций в разделе 12.3.) Если определен класс, представляющий некоторую операцию, то для ее вызова перегружается соответствующий оператор. Например, для взятия абсолютного значения числа типа int можно определить класс absInt:
};
Перегруженный оператор operator() должен быть объявлен как функция-член с произвольным числом параметров. Параметры и возвращаемое значение могут иметь любые типы, допустимые для функций (см. разделы 7.2, 7.3 и 7.4). operator() вызывается путем применения списка аргументов к объекту того класса, в котором он определен. Мы рассмотрим, как он используется в одном из обобщенных алгоритмов, описанных в главе 12. В следующем примере обобщенный алгоритм transform() вызывается для применения определенной в absInt операции к каждому элементу вектора ivec, т.е. для замены элемента его абсолютным значением.
}
Первый и второй аргументы transform() ограничивают диапазон элементов, к которым применяется операция absInt. Третий указывает на начало вектора, где будет сохранен результат применения операции.
Четвертый аргумент – это временный объект класса absInt, создаваемый с помощью конструктора по умолчанию. Конкретизация обобщенного алгоритма transform(), вызываемого из main(), могла бы выглядеть так: class absInt { public: int operator()( int val ) { int result = val < 0 ? -val : val; return result;
}
#include
#include 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() );
// ...

С++ для начинающих
724
} func
– это объект класса, который предоставляет операцию absInt, заменяющую число типа int его абсолютным значением. Он используется для вызова перегруженного оператора operator() класса absInt. Этому оператору передается аргумент *iter, указывающий на тот элемент вектора, для которого мы хотим получить абсолютное значение.
15.6.
Оператор “стрелка”
Оператор “стрелка”, разрешающий доступ к членам, может перегружаться для объектов класса. Он должен быть определен как функция-член и обеспечивать семантику указателя. Чаще всего этот оператор используется в классах, которые предоставляют
“интеллектуальный указатель” (smart pointer), ведущий себя аналогично встроенным, но поддерживают и некоторую дополнительную функциональность.
Допустим, мы хотим определить тип класса для представления указателя на объект
Screen
(см. главу 13):
};
Определение ScreenPtr должно быть таким, чтобы объект этого класса гарантировано указывал на объект Screen: в отличие от встроенного указателя, он не может быть нулевым. Тогда приложение сможет пользоваться объектами типа ScreenPtr, не проверяя, указывают ли они на какой-нибудь объект Screen. Для этого нужно определить класс ScreenPtr с конструктором, но без конструктора по умолчанию
(детально конструкторы рассматривались в разделе 14.2):
}; 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; class ScreenPtr {
// ... private:
Screen *ptr; class ScreenPtr { public:
ScreenPtr( const Screen &s ) : ptr( &s ) { }
// ...

С++ для начинающих
725
В любом определении объекта класса ScreenPtr должен присутствовать инициализатор – объект класса Screen, на который будет ссылаться объект ScreenPtr:
ScreenPtr ps( myScreen ); // правильно
Чтобы класс ScreenPtr вел себя как встроенный указатель, необходимо определить некоторые перегруженные операторы – разыменования (*) и “стрелку” для доступа к членам:
};
Оператор доступа к членам унарный, поэтому параметры ему не передаются. При использовании в составе выражения его результат зависит только от типа левого операнда. Например, в инструкции point->action(); исследуется тип point. Если это указатель на некоторый тип класса, то применяется семантика встроенного оператора доступа к члену. Если же это объект или ссылка на объект, то проверяется, есть ли в этом классе перегруженный оператор доступа. Когда перегруженный оператор “стрелка” определен, он вызывается для объекта point, иначе инструкция неверна, поскольку для обращения к членам самого объекта (в том числе по ссылке) следует использовать оператор “точка”.
Перегруженный оператор “стрелка” должен возвращать либо указатель на тип класса, либо объект класса, в котором он определен. Если возвращается указатель, то к нему применяется семантика встроенного оператора “стрелка”. В противном случае процесс продолжается рекурсивно, пока не будет получен указатель или определена ошибка.
Например, так можно воспользоваться объектом ps класса ScreenPtr для доступа к членам Screen: ps->move( 2, 3 );
Поскольку слева от оператора “стрелка” находится объект типа ScreenPtr, то употребляется перегруженный оператор этого класса, который возвращает указатель на объект Screen. Затем к полученному значению применяется встроенный оператор
“стрелка” для вызова функции-члена move().
Ниже приводится небольшая программа для тестирования класса ScreenPtr. Объект типа ScreenPtr используется точно так же, как любой объект типа Screen*:
ScreenPtr p1; // ошибка: у класса ScreenPtr нет конструктора по умолчанию
Screen myScreen( 4, 4 );
// перегруженные операторы для поддержки поведения указателя class ScreenPtr { public:
Screen& operator*() { return *ptr; }
Screen* operator->() { return ptr; }
// ...

С++ для начинающих
726
}
Разумеется, подобные манипуляции с указателями на объекты классов не так эффективны, как работа со встроенными указателями. Поэтому интеллектуальный указатель должен предоставлять дополнительную функциональность, важную для приложения, чтобы оправдать сложность своего использования.
15.7.
Операторы инкремента и декремента
Продолжая развивать реализацию класса ScreenPtr, введенного в предыдущем разделе, рассмотрим еще два оператора, которые поддерживаются для встроенных указателей и которые желательно иметь и для нашего интеллектуального указателя: инкремент (++) и декремент (--). Чтобы использовать класс ScreenPtr для ссылки на элементы массива объектов Screen, туда придется добавить несколько дополнительных членов.
Сначала мы определим новый член size, который содержит либо нуль (это говорит о том, что объект ScreenPtr указывает на единственный объект), либо размер массива, адресуемого объектом ScreenPtr. Нам также понадобится член offset, запоминающий смещение от начала данного массива:
#include
#include
#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;

С++ для начинающих
727
};
Модифицируем конструктор класса ScreenPtr с учетом его новой функциональности и дополнительных членов,. Пользователь нашего класса должен передать конструктору дополнительный аргумент, если создаваемый объект указывает на массив:
};
С помощью этого аргумента задается размер массива. Чтобы сохранить прежнюю функциональность, предусмотрим для него значение по умолчанию, равное нулю. Таким образом, если второй аргумент конструктора опущен, то член size окажется равен 0 и, следовательно, такой объект будет указывать на единственный объект Screen. Объекты нового класса ScreenPtr можно определять следующим образом:
ScreenPtr parr( *parray, arrSize ); // правильно: указывает на массив
Теперь мы готовы определить в ScreenPtr перегруженные операторы инкремента и декремента. Однако они бывают двух видов: префиксные и постфиксные. К счастью, можно определить оба варианта. Для префиксного оператора объявление не содержит ничего неожиданного:
};
Такие операторы определяются как унарные операторные функции. Использовать префиксный оператор инкремента можно, к примеру, следующим образом: class ScreenPtr { public:
// ... private: int size; // размер массива: 0, если единственный объект int offset; // смещение ptr от начала массива
Screen *ptr; class ScreenPtr { public:
ScreenPtr( Screen &s , int arraySize = 0 )
: ptr( &s ), size ( arraySize ), offset( 0 ) { } private: int size; int offset;
Screen *ptr;
Screen myScreen( 4, 4 );
ScreenPtr pobj( myScreen ); // правильно: указывает на один объект const int arrSize = 10;
Screen *parray = new Screen[ arrSize ]; class ScreenPtr { public:
Screen& operator++();
Screen& operator--();
// ...

С++ для начинающих
728
printScreen( parr );
Определения этих перегруженных операторов приведены ниже:
}
Чтобы отличить префиксные операторы от постфиксных, в объявлениях последних имеется дополнительный параметр типа int. В следующем фрагменте объявлены префиксные и постфиксные варианты операторов инкремента и декремента для класса
ScreenPtr
:
}; const int arrSize = 10;
Screen *parray = new Screen[ arrSize ];
ScreenPtr parr( *parray, arrSize ); for ( int ix = 0; ix < arrSize;
++ix, ++parr ) // эквивалентно parr.operator++()
}
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; class ScreenPtr { public:
Screen& operator++(); // префиксные операторы
Screen& operator--();
Screen& operator++(int); // постфиксные операторы
Screen& operator--(int);
// ...

С++ для начинающих
729
Ниже приведена возможная реализация постфиксных операторов:
}
Обратите внимание, что давать название второму параметру нет необходимости, поскольку внутри определения оператора он не употребляется. Компилятор сам подставляет для него значение по умолчанию, которое можно игнорировать. Вот пример использования постфиксного оператора: printScreen( parr++ );
При его явном вызове необходимо все же передать значение второго целого аргумента. В случае нашего класса ScreenPtr это значение игнорируется, поэтому может быть любым: parr.operator++(1024); // вызов постфиксного operator++
Перегруженные операторы инкремента и декремента разрешается объявлять как дружественные функции. Изменим соответствующим образом определение класса
ScreenPtr
:
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)

С++ для начинающих
730
};
Упражнение 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, определенный в системном заголовочном файле . Вот его объявление:
};
Когда для создания объекта типа класса используется new(), компилятор проверяет, определен ли в этом классе такой оператор. Если да, то для выделения памяти под объект вызывается именно он, в противном случае – глобальный оператор new(). Например, следующая инструкция
Screen *ps = new Screen; class ScreenPtr {
// объявления не членов friend Screen& operator++( Screen & ); // префиксные операторы friend Screen& operator--( Screen & ); friend Screen& operator++( Screen &, int); // постфиксные операторы friend Screen& operator--( Screen &, int); public:
// определения членов class Screen { public: void *operator new( size_t );
// ...

С++ для начинающих
731
создает объект Screen в хипе, а поскольку в этом классе есть оператор new(), то вызывается он. Параметр size_t оператора автоматически инициализируется значением, равным размеру Screen в байтах.
Добавление оператора new() в класс или его удаление оттуда не отражаются на пользовательском коде. Вызов new выглядит одинаково как для глобального оператора, так и для оператора-члена. Если бы в классе Screen не было собственного new(), то обращение осталось бы правильным, только вместо оператора-члена вызывался бы глобальный оператор.
С помощью оператора разрешения глобальной области видимости можно вызвать глобальный new(), даже если в классе Screen определена собственная версия:
Screen *ps = ::new Screen;
Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать 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(). class Screen { public: void operator delete( void * );

С++ для начинающих
1   ...   65   66   67   68   69   70   71   72   ...   93


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