Объекты классов
Определение класса, например Screen, не приводит к выделению памяти. Память выделяется только тогда, когда определяется объект типа класса. Так, если имеется следующая реализация Screen:
class Screen {
public:
// функции-члены
private:
string _screen;
string:size_type _cursor;
short _height;
short _width;
};
то определение
Screen myScreen;
выделяет область памяти, достаточную для хранения четырех членов Screen. Имя myScreen относится к этой области. У каждого объекта класса есть собственная копия данных-членов. Изменение членов myScreen не отражается на значениях членов любого другого объекта типа Screen.
Область видимости объекта класса зависит от его положения в тексте программы. Он определяется в иной области, нежели сам тип класса:
class Screen {
// список членов
};
int main()
{
Screen mainScreen;
}
Тип Screen объявлен в глобальной области видимости, тогда как объект mainScreen – в локальной области функции main().
Объект класса также имеет время жизни. В зависимости от того, где (в области видимости пространства имен или в локальной области) и как (статическим или нестатическим) он объявлен, он может существовать в течение всего времени выполнения программы или только во время вызова некоторой функции. Область видимости объекта класса и его время жизни ведут себя очень похоже. (Понятия области видимости и времени жизни введены в главе 8.)
Объекты одного и того же класса можно инициализировать и присваивать друг другу. По умолчанию копирование объекта класса эквивалентно копированию всех его членов. Например:
Screen bufScreen = myScreen;
// bufScreen._height = myScreen._height;
// bufScreen._width = myScreen._width;
// bufScreen._cursor = myScreen._cursor;
// bufScreen._screen = myScreen._screen;
Указатели и ссылки на объекты класса также можно объявлять. Указатель на тип класса разрешается инициализировать адресом объекта того же класса или присвоить ему такой адрес. Аналогично ссылка инициализируется l-значением объекта того же класса. (В объектно-ориентированном программировании указатель или ссылка на объект базового класса могут относиться и к объекту производного от него класса.)
int main()
{
Screen myScreen, bufScreen[10];
Screen *ptr = new Screen;
myScreen = *ptr;
delete ptr;
ptr = bufScreen;
Screen &ref = *ptr;
Screen &ref2 = bufScreen[6];
}
По умолчанию объект класса передается по значению, если он выступает в роли аргумента функции или ее возвращаемого значения. Можно объявить формальный параметр функции или возвращаемое ею значение как указатель или ссылку на тип класса. (В разделе 7.3 были представлены параметры, являющиеся указателями или ссылками на типы классов, и объяснялось, когда их следует использовать. В разделе 7.4 с этой точки зрения рассматривались типы возвращаемых значений.)
Для доступа к данным или функциям-членам объекта класса следует пользоваться соответствующими операторами. Оператор “точка” (.) применяется, когда операндом является сам объект или ссылка на него; а “стрелка” (->) – когда операндом служит указатель на объект:
#include "Screen.h"
bool isEqual( Screen& s1, Screen *s2 )
{ // возвращает false, если объекты не равны, и true - если равны
if (s1.height() != s2->height() ||
s2.width() != s2->width() )
return false;
for ( int ix = 0; ix < s1.height(); ++ix )
for ( int jy = 0; jy < s2->width(); ++jy )
if ( s1.get( ix, jy ) != s2->get( ix, jy ) )
return false;
return true; // попали сюда? значит, объекты равны
}
isEqual() – это не являющаяся членом функция, которая сравнивает два объекта Screen. У нее нет права доступа к закрытым членам Screen, поэтому напрямую обращаться к ним она не может. Сравнение проводится с помощью открытых функций-членов данного класса.
Для получения высоты и ширины экрана isEqual() должна пользоваться функциями-членами height() и width() для чтения закрытых членов класса. Их реализация тривиальна:
class Screen {
public:
int height() { return _height; }
int width() { return _width; }
// ...
private:
short _heigh, _width;
// ...
};
Применение оператора доступа к указателю на объект класса эквивалентно последовательному выполнению двух операций: применению оператора разыменования (*) к указателю, чтобы получить адресуемый объект, и последующему применению оператора “точка” для доступа к нужному члену класса. Например, выражение
s2->height()
можно переписать так:
(*s2).height()
Результат будет одним и тем же.
Объявление и определение класса
О классе говорят, что он определен, как только встретилась скобка, закрывающая его тело. После этого становятся известными все члены класса, а следовательно, и его размер.
Можно объявить класс, не определяя его. Например:
class Screen; // объявление класса Screen
Это объявление вводит в программу имя Screen и указывает, что оно относится к типу класса.
Тип объявленного, но еще не определенного класса допустимо использовать весьма ограниченно. Нельзя определять объект типа класса, если сам класс еще не определен, поскольку размер класса в этом момент неизвестен и компилятор не знает, сколько памяти отвести под объект.
Однако указатель или ссылку на объект такого класса объявлять можно, так как они имеют фиксированный размер, не зависящий от типа. Но, поскольку размеры класса и его членов неизвестны, применять оператор разыменования (*) к такому указателю, а также использовать указатель или ссылку для обращения к члену не разрешается, пока класс не будет полностью определен.
Член некоторого класса можно объявить принадлежащим к типу какого-либо класса только тогда, когда компилятор уже видел определение этого класса. До этого объявляются лишь члены, являющиеся указателями или ссылками на такой тип. Ниже приведено определение StackScreen, один из членов которого служит указателем на Screen, который объявлен, но еще не определен:
class Screen; // объявление
class StackScreen {
int topStack;
// правильно: указатель на объект Screen
Screen *stack;
void (*handler)();
};
Поскольку класс не считается определенным, пока не закончилось его тело, то в нем не может быть данных-членов его собственного типа. Однако класс считается объявленным, как только распознан его заголовок, поэтому в нем допустимы члены, являющиеся ссылками или указателями на его тип. Например:
class LinkScreen {
Screen window;
LinkScreen *next;
LinkScreen *prev;
};
Упражнение 13.1
Пусть дан класс Person со следующими двумя членами:
string _name;
string _address;
и такие функции-члены:
Person( const string &n, const string &s )
: _name( n ), _address( a ) { }
string name() { return _name; }
string address() { return _address; }
Какие члены вы объявили бы в секции public, а какие– в секции private? Поясните свой выбор.
Упражнение 13.2
Объясните разницу между объявлением и определением класса. Когда вы стали бы использовать объявление класса? А определение?
Объявление mutable
При объявлении объекта класса Screen константным возникают некоторые проблемы. Предполагается, что после инициализации объекта Screen, его содержимое уже нельзя изменять. Но это не должно мешать нам читать содержимое экрана. Рассмотрим следующий константный объект класса Screen:
const Screen cs ( 5, 5 );
Если мы хотим прочитать символ, находящийся в позиции (3,4), то попробуем сделать так:
// прочитать содержимое экрана в позиции (3,4)
// Увы! Это не работает
cs.move( 3, 4 );
char ch = cs.get();
Но такая конструкция не работает: move()– это не константная функция-член, и сделать ее таковой непросто. Определение move() выглядит следующим образом:
inline void Screen::move( int r, int c )
{
if ( checkRange( r, c ) )
{
int row = (r-1) * _width;
_cursor = row + c - 1; // модифицирует _cursor
}
}
Обратите внимание, что move()изменяет член класса _cursor, следовательно, не может быть объявлена константной.
Но почему нельзя модифицировать _cursor для константного объекта класса Screen? Ведь _cursor – это просто индекс. Изменяя его, мы не модифицируем содержимое экрана, а лишь пытаемся установить позицию внутри него. Модификация _cursor должна быть разрешена несмотря на то, что у класса Screen есть спецификатор const.
Чтобы разрешить модификацию члена класса, принадлежащего константному объекту, объявим его изменчивым (mutable). Член с таким спецификатором не бывает константным, даже если он член константного объекта. Его можно обновлять, в том числе функцией-членом со спецификатором const. Объявлению изменчивого члена класса должно предшествовать ключевое слово mutable:
class Screen {
public:
// функции-члены
private:
string _screen;
mutable string::size_type _cursor; // изменчивый член
short _height;
short _width;
};
Теперь любая константная функция способна модифицировать _cursor, и move() может быть объявлена константной. Хотя move() изменяет данный член, компилятор не считает это ошибкой.
// move() - константная функция-член
inline void Screen::move( int r, int c ) const
{
// ...
// правильно: константная функция-член может модифицировать члены
// со спецификатором mutable
_cursor = row + c - 1;
// ...
}
Показанные в начале этого подраздела операции позиционирования внутри экрана теперь можно выполнить без сообщения об ошибке.
Отметим, что изменчивым объявлен только член _cursor, тогда как _screen, _height и _width не имеют спецификатора mutable, поскольку их значения в константном объекте класса Screen изменять нельзя.
Упражнение 13.3
Объясните, как будет вести себя copy() при следующих вызовах:
Screen myScreen;
myScreen.copy( myScreen );
Упражнение 13.4
К дополнительным перемещениям курсора можно отнести его передвижение вперед и назад на один символ. Из правого нижнего угла экрана курсор должен попасть в левый верхний угол. Реализуйте функции forward() и backward().
Упражнение 13.5
Еще одной полезной возможностью является перемещение курсора вниз и вверх на одну строку. По достижении верхней или нижней строки экрана курсор не перепрыгивает на противоположный край; вместо этого подается звуковой сигнал, и курсор остается на месте. Реализуйте функции up() и down(). Для подачи сигнала следует вывести на стандартный вывод cout символ с кодом '007'.
Упражнение 13.6
Пересмотрите описанные функции-члены класса Screen и объявите те, которые сочтете нужными, константными. Объясните свое решение.
Объявление виртуального базового класса
Для указания виртуального наследования в объявление базового класса вставляется модификатор virtual. Так, в данном примере ZooAnimal становится виртуальным базовым для Bear и Raccoon:
// взаимное расположение ключевых слов public и virtual
// несущественно
class Bear : public virtual ZooAnimal { ... };
class Raccoon : virtual public ZooAnimal { ... };
Виртуальное наследование не является явной характеристикой самого базового класса, а лишь описывает его отношение к производному. Как мы уже отмечали, виртуальное наследование – это разновидность композиции по ссылке. Иначе говоря, доступ к подобъекту и его нестатическим членам косвенный, что обеспечивает гибкость, необходимую для объединения нескольких виртуально унаследованных подобъектов базовых классов в один разделяемый экземпляр внутри производного. В то же время объектом производного класса можно манипулировать через указатель или ссылку на тип базового, хотя последний является виртуальным. Например, все показанные ниже преобразования базовых классов Panda выполняются корректно, хотя Panda использует виртуальное наследование:
extern void dance( const Bear* );
extern void rummage( const Raccoon* );
extern ostream&
operator<<( ostream&, const ZooAnimal& );
int main()
{
Panda yin_yang;
dance( &yin_yang ); // правильно
rummage( &yin_yang ); // правильно
cout << yin_yang; // правильно
// ...
}
Любой класс, который можно задать в качестве базового, разрешается сделать виртуальным, причем он способен содержать все те же элементы, что обычные базовые классы. Так выглядит объявление ZooAnimal:
#include <iostream>
#include <string>
class ZooAnimal;
extern ostream&
operator<<( ostream&, const ZooAnimal& );
class ZooAnimal {
public:
ZooAnimal( string name,
bool onExhibit, string fam_name )
: _name( name ),
_onExhibit( onExhibit ), _fam_name( fam_name )
{}
virtual ~ZooAnimal();
virtual ostream& print( ostream& ) const;
string name() const { return _name; }
string family_name() const { return _fam_name; }
// ...
protected:
bool _onExhibit;
string _name;
string _fam_name;
// ...
};
К объявлению и реализации непосредственного базового класса при использовании виртуального наследования добавляется ключевое слово virtual. Вот, например, объявление нашего класса Bear:
class Bear : public virtual ZooAnimal {
public:
enum DanceType {
two_left_feet, macarena, fandango, waltz };
Bear( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Bear" ),
_dance( two_left_feet )
{}
virtual ostream& print( ostream& ) const;
void dance( DanceType );
// ...
protected:
DanceType _dance;
// ...
};
А вот объявление класса Raccoon:
class Raccoon : public virtual ZooAnimal {
public:
Raccoon( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Raccoon" ),
_pettable( false )
{}
virtual ostream& print( ostream& ) const;
bool pettable() const { return _pettable; }
void pettable( bool petval ) { _pettable = petval; }
// ...
protected:
bool _pettable;
// ...
};
Объявления друзей в шаблонах классов
обычный (не шаблонный) дружественный класс или дружественная функция. В следующем примере функция foo(), функция-член bar() и класс foobar объявлены друзьями всех конкретизаций шаблона QueueItem:
class Foo {
void bar();
};
template <class T>
class QueueItem {
friend class foobar;
friend void foo();
friend void Foo::bar();
// ...
};
Ни класс foobar, ни функцию foo() не обязательно объявлять или определять в глобальной области видимости перед объявлением их друзьями шаблона QueueItem.
Однако перед тем как объявить другом какой-либо из членов класса Foo, необходимо определить его. Напомним, что член класса может быть введен в область видимости только через определение объемлющего класса. QueueItem не может ссылаться на Foo::bar(), пока не будет найдено определение Foo;
связанный дружественный шаблон класса или функции. В следующем примере определено взаимно однозначное соответствие между классами, конкретизированными по шаблону QueueItem, и их друзьями – также конкретизациями шаблонов. Для каждого класса, конкретизированного по шаблону QueueItem, ассоциированные конкретизации foobar, foo() и Queue::bar() являются друзьями.
template <class Type>
class foobar { ... };
template <class Type>
void foo( QueueItem<Type> );
template <class Type>
class Queue {
void bar();
// ...
};
template <class Type>
class QueueItem {
friend class foobar<Type>;
friend void foo<Type>( QueueItem<Type> );
friend void Queue<Type>::bar();
// ...
};
Прежде чем шаблон класса можно будет использовать в объявлениях друзей, он сам должен быть объявлен или определен. В нашем примере шаблоны классов foobar и Queue, а также шаблон функции foo() следует объявить до того, как они объявлены друзьями в QueueItem.
Синтаксис, использованный для объявления foo() другом, может показаться странным:
friend void foo<Type>( QueueItem<Type> );
За именем функции следует список явных аргументов шаблона: foo<Type>. Такой синтаксис показывает, что в качестве друга объявляется конкретизированный шаблон функции foo(). Если бы список явных аргументов был опущен:
friend void foo( QueueItem<Type> );
то компилятор интерпретировал бы объявление как относящееся к обычной функции (а не к шаблону), у которой тип параметра – это экземпляр шаблона QueueItem. Как отмечалось в разделе 10.6, шаблон функции и одноименная обычная функция могут сосуществовать, и присутствие объявления такого шаблона перед определением класса QueueItem не вынуждает компилятор соотнести объявление друга именно с ним. Для того, чтобы соотнесение было верным, в конкретизированном шаблоне функции необходимо указать список явных аргументов;
несвязанный дружественный шаблон класса или функции. В следующем примере имеется отображение один-ко-многим между конкретизациями шаблона класса QueueItem и его друзьями. Для каждой конкретизации типа QueueItem все конкретизации foobar, foo() и Queue<T>::bar() являются друзьями:
template <class Type>
class QueueItem {
template <class T>
friend class foobar;
template <class T>
friend void foo( QueueItem<T> );
template <class T>
friend class Queue<T>::bar();
// ...
};
Следует отметить, что этот вид объявлений друзей в шаблоне класса не поддерживается компиляторами, написанными до принятия стандарта C++.
Объявления друзей в шаблонах Queue и QueueItem
Поскольку QueueItem не предназначен для непосредственного использования в вызывающей программе, то объявление конструктора этого класса помещено в закрытую секцию шаблона. Теперь класс Queue необходимо объявить другом QueueItem, чтобы можно было создавать и манипулировать объектами последнего.
Существует два способа объявить шаблон класса другом. Первый заключается в том, чтобы объявить любой экземпляр Queue другом любого экземпляра QueueItem:
template <class Type>
class QueueItem {
// любой экземпляр Queue является другом
// любого экземпляра QueueItem
template <class T> friend class Queue;
};
Однако нет смысла объявлять, например, класс Queue, конкретизированный типом string, другом QueueItem, конкретизированного типом complex<double>. Queue<string> должен быть другом только для класса QueueItem<string>. Таким образом, нам нужно взаимно однозначное соответствие между экземплярами Queue и QueueItem, конкретизированными одинаковыми типами. Чтобы добиться этого, применим второй метод объявления друзей:
template <class Type>
class QueueItem {
// для любого экземпляра QueueItem другом является
// только конкретизированный тем же типом экземпляр Queue
friend class Queue<Type>;
// ...
};
Данное объявление говорит о том, что для любой конкретизации QueueItem некоторым типом экземпляр Queue, конкретизированный тем же типом, является другом. Так, экземпляр Queue, конкретизированный типом int, будет другом экземпляра QueueItem, тоже конкретизированного типом int. Но для экземпляров QueueItem, конкретизированных типами complex<double> или string, этот экземпляр Queue другом не будет.
В любой точке программы у пользователю может понадобиться распечатать содержимое объекта Queue. Такая возможность предоставляется с помощью перегруженного оператора вывода. Этот оператор должен быть объявлен другом шаблона Queue, так как ему необходим доступ к закрытым членам класса. Какой же будет его сигнатура?
// как задать аргумент типа Queue?
ostream& operator<<( ostream &, ??? );
Поскольку Queue– это шаблон класса, то в имени конкретизированного экземпляра должен быть задан полный список аргументов:
ostream& operator<<( ostream &, const Queue<int> & );
Так мы определили оператор вывода для класса, конкретизированного из шаблона Queue типом int. Но что, если Queue – это очередь элементов типа string?
ostream& operator<<( ostream &, const Queue<string> & );
Вместо того чтобы явно определять нужный оператор вывода по мере необходимости, желательно сразу определить общий оператор, который будет работать для любой конкретизации Queue. Например:
ostream& operator<<( ostream &, const Queue<Type> & );
Однако из этого перегруженного оператора вывода придется сделать шаблон функции:
template <class Type> ostream&
operator<<( ostream &, const Queue<Type> & );
Теперь всякий раз, когда оператору ostream передается конкретизированный экземпляр Queue, конкретизируется и вызывается шаблон функции. Вот одна из возможных реализаций оператора вывода в виде такого шаблона:
template <class Type>
ostream& operator<<( ostream &os, const Queue<Type> &q )
{
os << "< ";
QueueItem<Type> *p;
for ( p = q.front; p; p = p->next )
os << *p << " ";
os << " >";
return os;
}
Если очередь объектов типа int содержит значения 3, 5, 8, 13, то распечатка ее содержимого с помощью такого оператора дает
< 3 5 8 13 >
Обратите внимание, что оператор вывода обращается к закрытому члену front класса Queue. Поэтому оператор необходимо объявить другом Queue:
template <class Type>
class Queue {
friend ostream&
operator<<( ostream &, const Queue<Type> & );
// ...
};
Здесь, как и при объявлении друга в шаблоне класса Queue, создается взаимно однозначное соответствие между конкретизациями Queue и оператора operator<<().
Распечатка элементов Queue производится оператором вывода operator<<() класса QueueItem:
os << *p;
Этот оператор также должен быть реализован в виде шаблона функции; тогда можно быть уверенным, что в нужный момент будет конкретизирован подходящий экземпляр:
template <class Type>
ostream& operator<<( ostream &os, const QueueItem<Type> &qi )
{
os << qi.item;
return os;
}
Поскольку здесь имеется обращение к закрытому члену item класса QueueItem, оператор следует объявить другом шаблона QueueItem. Это делается следующим образом:
template <class Type>
class QueueItem {
friend class Queue<Type>;
friend ostream&
operator<<( ostream &, const QueueItem<Type> & );
// ...
};
Оператор вывода класса QueueItem полагается на то, что item умеет распечатывать себя:
os << qi.item;
Это порождает тонкую зависимость типов при конкретизации Queue. Любой определенный пользователем и связанный с Queue класс, содержимое которого нужно распечатывать, должен предоставлять оператор вывода. В языке нет механизма, с помощью которого можно было бы задать такую зависимость в определении самого шаблона Queue. Но если оператор вывода не определен для типа, с которым конкретизируется данный шаблон, и делается попытка вывести содержимое конкретизированного экземпляра, то в том месте, где используется отсутствующий оператор вывода, компилятор выдает сообщение об ошибке. Шаблон Queue можно конкретизировать типом, не имеющим оператора вывода, – при условии, что не будет попытки распечатать содержимое очереди.
Следующая программа демонстрирует конкретизацию и использование функций-друзей шаблонов классов Queue и QueueItem:
#include <iostream>
#include "Queue.h"
int main() {
Queue<int> qi;
// конкретизируются оба экземпляра
// ostream& operator<<(ostream &os, const Queue<int> &)
// ostream& operator<<(ostream &os, const QueueItem<int> &)
cout << qi << endl;
int ival;
for ( ival = 0; ival < 10; ++ival )
qi.add( ival );
cout << qi << endl;
int err_cnt = 0;
for ( ival = 0; ival < 10; ++ival ) {
int qval = qi.remove();
if ( ival != qval ) err_cnt++;
}
cout << qi << endl;
if ( !err_cnt )
cout << "!! queue executed ok\n";
else cout << "?? queue errors: " << err_cnt << endl;
return 0;
}
После компиляции и запуска программа выдает результат:
< >
< 0 1 2 3 4 5 6 7 8 9 >
< >
!! queue executed ok
Упражнение 16.6
Пользуясь шаблоном класса Screen, определенным в упражнении 16.5, реализуйте операторы ввода и вывода (см. упражнение 15.6 из раздела 15.2) в виде шаблонов. Объясните, почему вы выбрали тот, а не иной способ объявления друзей класса Screen, добавленных в его шаблон.
Объявления и определения
Как было сказано в главе 7, объявление функции устанавливает ее имя, а также тип возвращаемого значения и список параметров. Определение функции, помимо этой информации, задает еще и тело– набор инструкций, заключенных в фигурные скобки. Функция должна быть объявлена перед вызовом. Например:
// объявление функции calc()
// определение находится в другом файле
void calc(int);
int main()
{
int loc1 = get(); // ошибка: get() не объявлена
calc(loc1); // правильно: calc() объявлена
// ...
}
Определение объекта имеет две формы:
type_specifier object_name;
type_specifier object_name = initializer;
Вот, например, определение obj1. Здесь obj1 инициализируется значением 97:
int obj1 = 97;
Следующая инструкция задает obj2, хотя начальное значение не задано:
int obj2;
Объект, определенный в глобальной области видимости без явной инициализации, гарантированно получит нулевое значение. Таким образом, в следующих двух примерах и var1, и var2 будут равны нулю:
int var1 = 0;
int var2;
Глобальный объект можно определить в программе только один раз. Поскольку он должен быть объявлен в исходном файле перед использованием, то для программы, состоящей из нескольких файлов, необходима возможность объявить объект, не определяя его. Как это сделать?
С помощью ключевого слова extern, аналогичного объявлению функции: оно указывает, что объект определен в другом месте – в этом же исходном файле или в другом. Например:
extern int i;
Эта инструкция “обещает”, что в программе имеется определение, подобное
int i;
extern-объявление не выделяет места под объект. Оно может встретиться несколько раз в одном и том же исходном файле или в разных файлах одной программы. Однако обычно находится в общедоступном заголовочном файле, который включается в те модули, где необходимо использовать глобальный объект:
// заголовочный файл
extern int obj1;
extern int obj2;
// исходный файл
int obj1 = 97;
int obj2;
Объявление глобального объекта с указанием ключевого слова extern и с явной инициализацией считается определением. Под этот объект выделяется память, и другие определения не допускаются:
extern const double pi = 3.1416; // определение
const double pi; // ошибка: повторное определение pi
Ключевое слово extern может быть указано и при объявлении функции – для явного обозначения его подразумеваемого смысла: “определено в другом месте”. Например:
extern void putValues( int*, int );
Объявления перегруженных функций
Теперь, научившись объявлять, определять и использовать функции в программах, познакомимся с перегрузкой – еще одним аспектом в C++. Перегрузка позволяет иметь несколько одноименных функций, выполняющих схожие операции над аргументами разных типов.
Вы уже воспользовались предопределенной перегруженной функцией. Например, для вычисления выражения
1 + 3
вызывается операция целочисленного сложения, тогда как вычисление выражения
1.0 + 3.0
осуществляет сложение с плавающей точкой. Выбор той или иной операции производится незаметно для пользователя. Операция сложения перегружена, чтобы обеспечить работу с операндами разных типов. Ответственность за распознавание контекста и применение операции, соответствующей типам операндов, возлагается на компилятор, а не на программиста.
В этой главе мы покажем, как определять собственные перегруженные функции.
Объявления перегруженных функций-членов
Функции-члены класса можно перегружать:
class myClass {
public:
void f( double );
char f( char, char ); // перегружает myClass::f( double )
// ...
};
Как и в случае функций, объявленных в пространстве имен, функции-члены могут иметь одинаковые имена при условии, что списки их параметров различны либо по числу параметров, либо по их типам. Если же объявления двух функций-членов отличаются только типом возвращаемого значения, то второе объявление считается ошибкой компиляции:
class myClass {
public:
void mf();
double mf(); // ошибка: так перегружать нельзя
// ...
};
В отличие от функций в пространствах имен, функции-члены должны быть объявлены только один раз. Если даже тип возвращаемого значения и списки параметров двух функций-членов совпадают, то второе объявление компилятор трактует как неверное повторное объявление:
class myClass {
public:
void mf();
void mf(); // ошибка: повторное объявление
// ...
};
Все функции из множества перегруженных должны быть объявлены в одной и той же области видимости. Поэтому функции-члены никогда не перегружают функций, объявленных в пространстве имен. Кроме того, поскольку у каждого класса своя область видимости, функции, являющиеся членами разных классов, не перегружают друг друга.
Множество перегруженных функций-членов может содержать как статические, так и нестатические функции:
class myClass {
public:
void mcf( double );
static void mcf( int* ); // перегружает myClass::mcf( double )
// ...
};
Какая из функций-членов будет вызвана– статическая или нестатическая – зависит от результатов разрешения перегрузки. Процесс разрешения в ситуации, когда устояли как статические, так и нестатические члены, мы подробно рассмотрим в следующем разделе.
Область видимости
Каждое имя в С++ программе должно относиться к уникальной сущности (объекту, функции, типу или шаблону). Это не значит, что оно встречается только один раз во всей программе: его можно повторно использовать для обозначения другой сущности, если только есть некоторый контекст, помогающий различить разные значения одного и того же имени. Контекстом, служащим для такого различения, служит область видимости. В С++ поддерживается три их типа: локальная
область видимости, область видимости пространства имен и область видимости класса.
Локальная область– это часть исходного текста программы, содержащаяся в определении функции (или в блоке). Любая функция имеет собственную такую часть, и каждая составная инструкция (или блок) внутри функции также представляет собой отдельную локальную область.
Область видимости пространства имен – часть исходного текста программы, не содержащаяся внутри объявления или определения функции или определения класса. Самая внешняя часть называется глобальной областью видимости или глобальной областью видимости пространства имен.
Объекты, функции, типы и шаблоны могут быть определены в глобальной области видимости. Программисту разрешено задать пользовательские пространства имен, заключенные внутри глобальной области с помощью определения пространства имен. Каждое такое пространство является отдельной областью видимости. Пользовательское пространство, как и глобальное, может содержать объявления и определения объектов, функций, типов и шаблонов, а также вложенные пользовательские пространства имен. (Они рассматриваются в разделах 8.5 и 8.6.)
Каждое определение класса представляет собой отдельную область видимости класса. (О таких областях мы расскажем в главе 13.)
Имя может обозначать различные сущности в зависимости от области видимости. В следующем фрагменте программы имя s1 относится к четырем разным сущностям:
#include <iostream>
#include <string>
// сравниваем s1 и s2 лексикографически
int lexicoCompare( const string &sl, const string &s2 ) { ... }
// сравниваем длины s1 и s2
int sizeCompare( const string &sl, const string &s2 ) { ... }
typedef int ( PFI)( const string &, const string & );
// сортируем массив строк
void sort( string *s1, string *s2, PFI compare =lexicoCompare )
{ ... }
string sl[10] = { "a", "light", "drizzle", "was", "falling",
"when", "they", "left", "the", "school" };
int main()
{
// вызов sort() со значением по умолчанию параметра compare
// s1 - глобальный массив
sort( s1, s1 + sizeof(s1)/sizeof(s1[0]) - 1 );
// выводим результат сортировки
for ( int i = 0; i < sizeof(s1) / sizeof(s1[0]); ++i )
cout << s1[ i ].c_str() << "\n\t";
}
Поскольку определения функций lexicoCompare(), sizeCompare() и sort() представляют собой различные области видимости и все они отличны от глобальной, в каждой из этих областей можно завести переменную с именем s1.
Имя, введенное с помощью объявления, можно использовать от точки объявления до конца области видимости (включая вложенные области). Так, имя s1 параметра функции lexicoCompare() разрешается употреблять до конца ее области видимости, то есть до конца ее определения.
Имя глобального массива s1 видимо с точки его объявления до конца исходного файла, включая вложенные области, такие, как определение функции main().
В общем случае имя должно обозначать одну сущность внутри одной области видимости. Если в предыдущем примере после объявления массива s1 добавить следующую строку, компилятор выдаст сообщение об ошибке:
void s1(); // ошибка: повторное объявление s1
Перегруженные функции являются исключением из правила: можно завести несколько одноименных функций в одной области видимости, если они отличаются списком параметров. (Перегруженные функции рассматриваются в главе 9.)
В С++ имя должно быть объявлено до момента его первого использования в выражении. В противном случае компилятор выдаст сообщение об ошибке. Процесс сопоставления имени, используемого в выражении, с его объявлением называется разрешением. С помощью этого процесса имя получает конкретный смысл. Разрешение имени зависит от способа его употребления и от его области видимости. Мы рассмотрим этот процесс в различных контекстах. (В следующем подразделе описывается разрешение имен в локальной области видимости; в разделе 10.9 – разрешение в шаблонах функций; в конце главы 13 – в области видимости классов, а в разделе 16.12 – в шаблонах классов.)
Области видимости и разрешение имен – понятия времени компиляции. Они применимы к отдельным частям текста программы. Компилятор интерпретирует текст программы согласно правилам областей видимости и правилам разрешения имен.
Область видимости и время жизни
В этой главе обсуждаются два важных вопроса, касающиеся объявлений в С++. Где употребляется объявленное имя? Когда можно безопасно использовать объект или вызывать функцию, т.е. каково время жизни сущности в программе? Для ответа на первый вопрос мы введем понятие областей видимости и покажем, как они ограничивают применение имен в исходном файле программы. Мы рассмотрим разные типы таких областей: глобальную и локальную, а также более сложное понятие областей видимости пространств имен, которое появится в конце главы. Отвечая на второй вопрос, мы опишем, как объявления вводят глобальные объекты и функции (сущности, “живущие” в течение всего времени работы программы), локальные (“живущие” на определенном отрезке выполнения) и динамически размещаемые объекты (временем жизни которых управляет программист). Мы также исследуем свойства времени выполнения, характерные для этих объектов и функций.
Область видимости класса *
Тело класса определяет область видимости. Объявления членов класса внутри тела вводят их имена в область видимости класса.
Для обращения к ним применяются операторы доступа (точка и стрелка) и оператор разрешения области видимости (::). Когда употребляется оператор доступа, то предшествующее ему имя обозначает объект или указатель на объект типа класса, а следующее за ним имя должно находиться в области видимости этого класса. Аналогично при использовании оператора разрешения области видимости поиск имени, следующего за ним, идет в области видимости класса, имя которого стоит перед оператором. (В главах 17 и 18 мы увидим, что производный класс может обращаться к членам своих базовых.)
Однако применение операторов доступа или оператора разрешения области видимости нужно не всегда. Некоторые части программы сами по себе находятся в области видимости класса, и в них к членам класса можно обращаться напрямую. Одной из таких частей является само определение класса. Имя его члена можно использовать в теле после объявления:
class String {
public:
typedef int index_type;
// тип параметра - это на самом деле String::index_type
char& operator[]( index_type )
};
Порядок объявления членов класса в его теле важен: нельзя ссылаться на члены, которые будут объявлены позже. Например, если объявление оператора operator[]() находится раньше объявления typedef index_type, то приведенное ниже объявление operator[]() оказывается ошибочным, поскольку в нем используется еще неизвестное имя index_type:
class String {
public:
// ошибка: имя index_type не объявлено
char &operator[]( index_type );
typedef int index_type;
};
Однако из этого правила есть два исключения. Первое касается имен, использованных в определениях встроенных функций-членов, второе – имен, применяемых как аргументы по умолчанию. Рассмотрим обе ситуации.
Разрешение имен в определениях встроенных функций-членов происходит в два этапа. Сначала объявление функции (т.е. тип возвращаемого значения и список параметров) обрабатывается в том месте, где оно встретилось в определении класса. Затем тело функции обрабатывается во всей области видимости, сразу после того, как были просмотрены объявления всех членов. Посмотрим на наш пример, в котором оператор operator[]() определен как встроенный внутри тела класса:
class String {
public:
typedef int index_type;
char &operator[]( index_type elem )
{ return _string[ elem ]; }
private:
char *_string;
};
На первом этапе просматриваются имена, использованные в объявлении operator[](), чтобы найти имя типа параметра index_type. Поскольку первый шаг выполняется тогда, когда в теле класса встретилось определение функции-члена, то имя index_type должно быть объявлено до определения operator[]().
Обратите внимание, что член _string объявлен в теле класса после определения operator[](). Это правильно, и _string не является в теле operator[]() необъявленным именем. Имена в телах функций-членов просматриваются на втором шаге разрешения имен в определениях встроенных функций-членов. Этот этап выполняется во всей области видимости класса, как если бы тела функций-членов обрабатывались последними, прямо перед закрытием тела класса, когда все его члены уже объявлены.
Аргументы по умолчанию также разрешаются на втором шаге. Например, в объявлении функции-члена clear() используется имя статического члена bkground, который определен позже:
class Screen {
public:
// bkground относится к статическому члену,
// объявленному позже в определении класса
Screen& clear( char = bkground );
private:
static const char bkground = '#';
};
Хотя такие аргументы в объявлениях функций-членов разрешаются во всей области видимости класса, программа будет считаться ошибочной, если он ссылается на нестатический член. Нестатический член должен быть привязан к объекту своего класса или к указателю на такой объект, иначе использовать его нельзя. Употребление подобных членов в качестве аргументов по умолчанию нарушает это ограничение. Если переписать предыдущий пример так:
class Screen {
public:
// ...
// ошибка: bkground - нестатический член
Screen& clear( char = bkground );
private:
const char bkground = '#';
};
то имя аргумента по умолчанию разрешается нестатическим членом bkground, а это считается ошибкой.
Определения членов класса, появляющиеся вне его тела, – это еще один пример части программы, которая находится в области видимости класса. В ней имена членов распознаются несмотря на то, что оператор доступа или оператор разрешения области видимости при обращении к ним не применяется. Как же разрешаются имена в определениях членов?
Как правило, если такое определение появляется вне тела, то часть программы, следующая за именем определяемого члена, считается находящейся в области видимости класса вплоть до конца определения члена. Вынесем определение оператора operator[]() из класса String:
class String {
public:
typedef int index_type;
char& operator[]( index_type );
private:
char *_string;
};
// в operator[]() есть обращения к index_type и _string
inline char& operator[]( index_type elem )
{
return _string[ elem ];
}
Обратите внимание, что в списке параметров встречается typedef index_type без квалифицирующего имени класса String::.Текст, следующий за именем члена String::operator[] и до конца определения функции, находится в области видимости класса. Объявленные в этой области типы рассматриваются при разрешении имен типов, использованных в списке параметров функции-члена.
Определения статических данных-членов также появляются вне определения класса. В них часть программы, следующая за именем статического члена вплоть до конца определения, считается находящейся в области видимости класса. Например, инициализатор статического члена может непосредственно, без соответствующих операторов, ссылаться на члены класса:
class Account:
// ...
private:
static double _interestRate;
static double initInterestRate();
};
// ссылается на Account::initInterest()
double Account::_interestRate = initInterest();
Инициализатор _interestRate вызывает статическую функцию-член Account::initInterest() несмотря на то, что ее имя не квалифицировано именем класса.
Не только инициализатор, но и все, что следует за именем статического члена _interestRate до завершающей точки с запятой, находится в области видимости класса Account. Поэтому в определении статического члена name может быть обращение к члену класса nameSize:
class Account:
// ...
private:
static const int nameSize = 16;
static const char name[nameSize];
// nameSize не квалифицировано именем класса Account
const char Account::name[nameSize] = "Savins Account";
Хотя член nameSize не квалифицирован именем класса Account, определение name не является ошибкой, так как оно находится в области видимости своего класса и может ссылаться на его члены после того, как компилятор прочитал Account::name.
В определении члена, которое появляется вне тела, часть программы перед определяемым именем не находится в области видимости класса. При обращении к члену в этой части следует пользоваться оператором разрешения области видимости. Например, если типом статического члена является typedef Money, определенный в классе Account, то имя Money должно быть квалифицировано, когда статический член данных определяется вне тела класса:
class Account {
typedef double Money;
//...
private:
static Money _interestRate;
static Money initInterest();
};
// Money должно быть квалифицировано именем класса Account::
Account::Money Account::_interestRate = initInterest();
С каждым классом ассоциируется отдельная область видимости, причем у разных классов эти области различны. К членам одного класса нельзя напрямую обращаться в определениях членов другого класса, если только один из них не является для второго базовым. (Наследование и базовые классы рассматриваются в главах 17 и 18.)
Область видимости класса и наследование
У каждого класса есть собственная область видимости, в которой определены имена членов и вложенные типы (см. разделы 13.9 и 13.10). При наследовании область видимости производного класса вкладывается в область видимости непосредственного базового. Если имя не удается разрешить в области видимости производного класса, то поиск определения продолжается в области видимости базового.
Именно эта иерархическая вложенность областей видимости классов при наследовании и делает возможным обращение к именам членов базового класса так, как если бы они были членами производного. Рассмотрим сначала несколько примеров одиночного наследования, а затем перейдем к множественному. Предположим, есть упрощенное определение класса ZooAnimal:
class ZooAnimal {
public:
ostream &print( ostream& ) const;
// сделаны открытыми только ради демонстрации разных случаев
string is_a;
int ival;
private:
double dval;
};
и упрощенное определение производного класса Bear:
class Bear : public ZooAnimal {
public:
ostream &print( ostream& ) const;
// сделаны открытыми только ради демонстрации разных случаев
string name;
int ival;
};
Когда мы пишем:
Bear bear;
bear.is_a;
то имя разрешается следующим образом:
bear – это объект класса Bear. Сначала поиск имени is_a ведется в области видимости Bear. Там его нет.
Поскольку класс Bear производный от ZooAnimal, то далее поиск is_a ведется в области видимости последнего. Обнаруживается, что имя принадлежит его члену. Разрешение закончилось успешно.
Хотя к членам базового класса можно обращаться напрямую, как к членам производного, они сохраняют свою принадлежность к базовому классу. Как правило, не имеет значения, в каком именно классе определено имя. Но это становится важным, если в базовом и производном классах есть одноименные члены. Например, когда мы пишем:
bear.ival;
ival – это член класса Bear, найденный на первом шаге описанного выше процесса разрешения имени.
Иными словами, член производного класса, имеющий то же имя, что и член базового, маскирует последний. Чтобы обратиться к члену базового класса, необходимо квалифицировать его имя с помощью оператора разрешения области видимости:
bear.ZooAnimal::ival;
Тем самым мы говорим компилятору, что объявление ival следует искать в области видимости класса ZooAnimal.
Проиллюстрируем использование оператора разрешения области видимости на несколько абсурдном примере (надеемся, вы никогда не напишете чего-либо подобного в реальном коде):
int ival;
int Bear::mumble( int ival )
{
return ival + // обращение к параметру
::ival + // обращение к глобальному объекту
ZooAnimal::ival +
Bear::ival;
}
Неквалифицированное обращение к ival разрешается в пользу формального параметра. (Если бы переменная ival не была определена внутри mumble(), то имел бы место доступ к члену класса Bear. Если бы ival не была определена и в Bear, то подразумевался бы член ZooAnimal. А если бы ival не было и там, то речь шла бы о глобальном объекте.)
Разрешение имени члена класса всегда предшествует выяснению того, является ли обращение к нему корректным. На первый взгляд, это противоречит интуиции. Например, изменим реализацию mumble():
int dval;
int Bear::mumble( int ival )
{
// ошибка: разрешается в пользу закрытого члена ZooAnimal::dval
return ival + dval;
}
Можно возразить, что алгоритм разрешения должен остановиться на первом допустимом в данном контексте имени, а не на первом найденном. Однако в приведенном примере алгоритм разрешения выполняется следующим образом:
(a) Определено ли dval в локальной области видимости функции-члена класса Bear? Нет.
(b) Определено ли dval в области видимости Bear? Нет.
(c) Определено ли dval в области видимости ZooAnimal? Да. Обращение разрешается в пользу этого имени.
После того как имя разрешено, компилятор проверяет, возможен ли доступ к нему. В данном случае нет: dval является закрытым членом, и прямое обращение к нему из mumble() запрещено. Правильное (и, возможно, имевшееся в виду) разрешение требует явного употребления оператора разрешения области видимости:
return ival + ::dval; // правильно
Почему же имя члена разрешается перед проверкой уровня доступа? Чтобы предотвратить тонкие изменения семантики программы в связи с совершенно независимым, казалось бы, изменением уровня доступа к члену. Рассмотрим, например, такой вызов:
int dval;
int Bear::mumble( int ival )
{
foo( dval );
// ...
}
Если бы функция foo() была перегруженной, то перемещение члена ZooAnimal::dval из закрытой секции в защищенную вполне могло бы изменить всю последовательность вызовов внутри mumble(), а разработчик об этом даже и не подозревал бы.
Если в базовом и производном классах есть функции-члены с одинаковыми именами и сигнатурами, то их поведение такое же, как и поведение данных-членов: член производного класса лексически скрывает в своей области видимости член базового. Для вызова члена базового класса необходимо применить оператор разрешения области видимости:
ostream& Bear::print( ostream &os) const
{
// вызывается ZooAnimal::print(os)
ZooAnimal::print( os );
os << name;
return os;
}
Область видимости класса при множественном наследовании
Как влияет множественное наследование на алгоритм просмотра области видимости класса? Все непосредственные базовые классы просматриваются одновременно, что может приводить к неоднозначности в случае, когда в нескольких из них есть одноименные члены. Рассмотрим на нескольких примерах, как возникает неоднозначность и какие меры можно предпринять для ее устранения. Предположим, есть следующий набор классов:
class Endangered {
public:
ostream& print( ostream& ) const;
void highlight();
// ...
};
class ZooAnimal {
public:
bool onExhibit() const;
// ...
private:
bool highlight( int zoo_location );
// ...
};
class Bear : public ZooAnimal {
public:
ostream& print( ostream& ) const;
void dance( dance_type ) const;
// ...
};
Panda объявляется производным от двух классов:
class Panda : public Bear, public Endangered {
public:
void cuddle() const;
// ...
};
Хотя при наследовании функций print() и highlight() из обоих базовых классов Bear и Endangered имеется потенциальная неоднозначность, сообщение об ошибке не выдается до момента явно неоднозначного обращения к любой из этих функций.
В то время как неоднозначность двух унаследованных функций print() очевидна с первого взгляда, наличие конфликта между членами highlight() удивляет (ради этого пример и составлялся): ведь у них разные уровни доступа и разные прототипы. Более того, экземпляр из Endangered – это член непосредственного базового класса, а из ZooAnimal – член класса, стоящего на две ступеньки выше в иерархии.
Однако все это не имеет значения (впрочем, как мы скоро увидим, может иметь, но в случае виртуального наследования). Bear наследует закрытую функцию-член highlight() из ZooAnimal; лексически она видна, хотя вызывать ее из Bear или Panda запрещено. Значит, Panda наследует два лексически видимых члена с именем highlight, поэтому любое неквалифицированное обращение к этому имени приводит к ошибке компиляции.
Поиск имени начинается в ближайшей области видимости, объемлющей его вхождение. Например, в коде
int main()
{
Panda yin_yang;
yin_yang.dance( Bear::macarena );
}
ближайшей будет область видимости класса Panda, к которому принадлежит yin_yang. Если же мы напишем:
void Panda::mumble()
{
dance( Bear::macarena );
// ...
}
то ближайшей будет локальная область видимости функции-члена mumble(). Если объявление dance в ней имеется, то разрешение имени на этом благополучно завершится. В противном случае поиск будет продолжен в объемлющих областях видимости.
В случае множественного наследования имитируется одновременный просмотр всех поддеревьев наследования – в нашем случае это класс Endangered и поддерево Bear/ZooAnimal. Если объявление обнаружено только в поддереве одного из базовых классов, то разрешение имени заканчивается успешно, как, например, при таком вызове dance():
// правильно: Bear::dance()
yin_yang.dance( Bear::macarena );
Если же объявление найдено в двух или более поддеревьях, то обращение считается неоднозначным и компилятор выдает сообщение об ошибке. Так будет при неквалифицированном обращении к print():
int main()
{
// ошибка: неоднозначность: одна из
// Bear::print( ostream& ) const
// Endangered::print( ostream& ) const
Panda yin_yang;
yin_yang.print( cout );
}
На уровне программы в целом для разрешения неоднозначности достаточно явно квалифицировать имя нужной функции-члена с помощью оператора разрешения области видимости:
int main()
{
// правильно, но не лучшее решение
Panda yin_yang;
yin_yang.Bear::print( cout );
}
Предложенный способ неэффективен: теперь пользователь вынужден решать, каково правильное поведение класса Panda; однако лучше, если такого рода ответственность примет на себя проектировщик и класс Panda сам устранит все неоднозначности, свойственные его иерархии наследования. Простейший способ добиться этого – задать квалификацию уже в определении экземпляра в производном классе, указав тем самым требуемое поведение:
inline void Panda::highlight() {
Endangered::highlight();
}
inline ostream&
Panda::print( ostream &os ) const
{
Bear::print( os );
Endangered::print( os );
return os;
}
Поскольку успешная компиляция производного класса, наследующего нескольким базовым, не гарантирует отсутствия скрытых неоднозначностей, мы рекомендуем при тестировании вызывать все функции-члены, даже самые тривиальные.
Упражнение 18.9
Дана следующая иерархия классов:
class Base1 {
public:
// ...
protected:
int ival;
double dval;
char cval;
// ...
private:
int *id;
// ...
};
class Base2 {
public:
// ...
protected:
float fval;
// ...
private:
double dval;
// ...
};
class Derived : public Base1 {
public:
// ...
protected:
string sval;
double dval;
// ...
};
class MI : public Derived, public Base2 {
public:
// ...
protected:
int *ival;
complex<double> cval;
// ...
};
и структура функции-члена MI::foo():
int ival;
double dval;
void MI::
foo( double dval )
{
int id;
// ...
}
(a) Какие члены видны в классе MI? Есть ли среди них такие, которые видны в нескольких базовых?
(b) Какие члены видны в MI::foo()?
Упражнение 18.10
Пользуясь иерархией классов из упражнения 18.9, укажите, какие из следующих присваиваний недопустимы внутри функции-члена MI::bar():
void MI::
bar()
{
int sval;
// вопрос упражнения относится к коду, начинающемуся с этого места ...
}
(a) dval = 3.14159; (d) fval = 0;
(b) cval = 'a'; (e) sval = *ival;
(c) id = 1;
Упражнение 18.11
Даны иерархия классов из упражнения 18.9 и скелет функции-члена MI::foobar():
int id;
void MI::
foobar( float cval )
{
int dval;
// вопросы упражнения относятся к коду, начинающемуся с этого места ...
}
(a) Присвойте локальной переменной dval сумму значений члена dval класса Base1 и члена dval класса Derived.
(b) Присвойте вещественную часть члена cval класса MI члену fval класса Base2.
(c) Присвойте значение члена cval класса Base1 первому символу члена sval класса Derived.
Упражнение 18.12
Дана следующая иерархия классов, в которых имеются функции-члены print():
class Base {
public:
void print( string ) const;
// ...
};
class Derived1 : public Base {
public:
void print( int ) const;
// ...
};
class Derived2 : public Base {
public:
void print( double ) const;
// ...
};
class MI : public Derived1, public Derived2 {
public:
void print( complex<double> ) const;
// ...
};
(a) Почему приведенный фрагмент дает ошибку компиляции?
MI mi;
string dancer( "Nejinsky" );
mi.print( dancer );
(b) Как изменить определение MI, чтобы этот фрагмент компилировался и выполнялся правильно?
Обобщенные алгоритмы
Операции, описанные в предыдущих разделах, составляют набор, поддерживаемый непосредственно контейнерами vector и deque. Согласитесь, что это весьма небогатый интерфейс и ему явно не хватает базовых операций find(), sort(), merge() и т.д. Планировалось вынести общие для всех контейнеров операции в набор обобщенных алгоритмов, которые могут применяться ко всем контейнерным типам, а также к массивам встроенных типов. (Обобщенные алгоритмы описываются в главе 12 и в Приложении.) Эти алгоритмы связываются с определенным типом контейнера с помощью передачи им в качестве параметров пары соответствующих итераторов. Вот как выглядят вызовы алгоритма find() для списка, вектора и массива разных типов:
#include <list>
#include <vector>
int ia[ 6 ] = { 0, 1, 2, 3, 4, 5 };
vector<string> svec;
list<double> dtist;
// соответствующий заголовочный файл
#include <algorithm>
vector<string>::iterator viter;
list<double>::iterator liter;
#int *pia;
// find() возвращает итератор на найденный элемент
// для массива возвращается указатель ...
pia = find( &ia[0], &ia[6], some_int_value );
liter = find( dlist.begin(), dlist.end(), some_double_value );
viter = find( svec.begin(), svec.end(), some_string_value );
Контейнер list поддерживает дополнительные операции, такие, как sort() и merge(), поскольку в нем не реализован произвольный доступ к элементам. (Эти операции описаны в разделе 12.6.)
Теперь вернемся к нашей поисковой системе.
Упражнение 6.11
Напишите программу, в которой определены следующие объекты:
int ia[] = { 1, 5, 34 };
int ia2[] = { 1, 2, 3 };
int ia3[] = { 6, 13, 21, 29, 38, 55, 67, 89 };
vector<int> ivec;
Используя различные операции вставки и подходящие значения ia, ia2 и ia3, модифицируйте вектор ivec так, чтобы он содержал последовательность:
{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 }
Упражнение 6.12
Напишите программу, определяющую данные объекты:
int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 };
list<int> ilist( ia, ia+11 );
Используя функцию-член erase() с одним параметром, удалите из ilist все нечетные элементы.
Обобщенные алгоритмы
В нашу реализацию класса Array (см. главу 2) мы включили функции-члены для поддержки операций min(), max() и sort(). Однако в стандартном классе vector эти, на первый взгляд фундаментальные, операции отсутствуют. Для нахождения минимального или максимального значения элементов вектора следует вызвать один из обобщенных алгоритмов. Алгоритмами они называются потому, что реализуют такие распространенные операции, как min(), max(), find() и sort(), а обобщенными (generic)– потому, что применимы к различным контейнерным типам: векторам, спискам, массивам. Контейнер связывается с применяемым к нему обобщенным алгоритмом посредством пары итераторов (мы говорили о них в разделе 6.5), указывающих, какие элементы следует посетить при обходе контейнера. Специальные объекты-функции
позволяют переопределить семантику операторов в обобщенных алгоритмах. Итак, в этой главе рассматриваются обобщенные алгоритмы, объекты-функции и итераторы.
Обобщенные алгоритмы
Первые два аргумента любого обобщенного алгоритма (разумеется, есть исключения, которые только подтверждают правило) – это пара итераторов, обычно называемых first и last, ограничивающих диапазон элементов внутри контейнера или встроенного массива, к которым применяется этот алгоритм. Как правило, диапазон элементов (иногда его называют интервалом с включенной левой границей) обозначается следующим образом:
// читается так: включает первый и все последующие элементы,
// кроме последнего
[ first, last )
Эта запись говорит о том, что диапазон начинается с элемента first и продолжается до элемента last, исключая последний. Если
first == last
то говорят, что диапазон пуст.
К паре итераторов предъявляется следующее требование: если начать с элемента first и последовательно применять оператор инкремента, то возможно достичь элемента last. Однако компилятор не в состоянии проверить выполнение этого ограничения; если оно нарушается, поведение программы не определено, обычно все заканчивается аварийным остановом и дампом памяти.
В объявлении каждого алгоритма указывается минимально необходимая категория итератора (см. раздел 12.4). Например, для алгоритма find(), реализующего однопроходный обход контейнера с доступом только для чтения, требуется итератор чтения, но можно передать и однонаправленный или двунаправленный итератор, а также итератор с произвольным доступом. Однако передача итератора записи приведет к ошибке. Не гарантируется, что ошибки, связанные с передачей итератора не той категории, будут обнаружены во время компиляции, поскольку категории итераторов – это не собственно типы, а лишь параметры-типы, передаваемые шаблону функции.
Некоторые алгоритмы существуют в нескольких версиях: в одной используется встроенный оператор, а во второй – объект-функция или указатель на функцию, которая предоставляет альтернативную реализацию оператора. Например, unique() по умолчанию сравнивает два соседних элемента с помощью оператора равенства, определенного для типа объектов в контейнере. Но если такой оператор равенства не определен или мы хотим сравнивать элементы иным способом, то можно передать либо объект-функцию, либо указатель на функцию, обеспечивающую нужную семантику. Встречаются также алгоритмы с похожими, но разными именами. Так, предикатные версии всегда имеют имя, оканчивающееся на _if, например find_if(). Скажем, есть алгоритм replace(), реализованный с помощью встроенного оператора равенства, и replace_if(), которому передается объект-предикат или указатель на функцию.
Алгоритмы, модифицирующие контейнер, к которому они применяются, обычно имеют две версии: одна преобразует содержимое контейнера по месту, а вторая возвращает копию исходного контейнера, в которой и отражены все изменения. Например, есть алгоритмы replace() и replace_copy() (имя версии с копированием всегда заканчивается на _copy). Однако не у всех алгоритмов, модифицирующих контейнер, имеется такая версия. К примеру, ее нет у алгоритма sort(). Если же мы хотим, чтобы сортировалась копия, то создать и передать ее придется самостоятельно.
Для использования любого обобщенного алгоритма необходимо включить в программу заголовочный файл
#include <algorithm>
А для любого из четырех численных алгоритмов – adjacent_differences(), accumulate(), inner_product() и partial_sum() – включить также заголовок
#include <numeric>
Все существующие алгоритмы для удобства изложения распределены нами на девять категорий (они перечислены ниже). В Приложении алгоритмы рассматриваются в алфавитном порядке, и для каждого приводится пример применения.
Обобщенный список
Наш класс ilist имеет серьезный недостаток: он может хранить элементы только целого типа. Если бы он мог содержать элементы любого типа– как встроенного, так и определенного пользователем, – то его область применения была бы гораздо шире. Модифицировать ilist для поддержки произвольных типов данных позволяет механизм шаблонов (см. главу 16).
При использовании шаблона вместо параметра подставляется реальный тип данных. Например:
list< string > slist;
создает экземпляр списка, способного содержать объекты типа string, а
list< int > ilist;
создает список, в точности повторяющий наш ilist. С помощью шаблона класса можно обеспечить поддержку произвольных типов данных одним экземпляром кода. Рассмотрим последовательность действий, уделив особое внимание классу list_item.
Определение шаблона класса начинается ключевым словом template, затем следует список параметров в угловых скобках. Параметр представляет собой идентификатор, перед которым стоит ключевое слово class или typename. Например:
template <class elemType>
class list_item;
Эта инструкция объявляет list_item шаблоном класса с единственным параметром-типом. Следующее объявление эквивалентно предыдущему:
template <typename elemType>
class list_item;
Ключевые слова class и typename имеют одинаковое значение, можно использовать любое из них. Более удобное для запоминания typename появилось в стандарте С++ сравнительно недавно и поддерживается еще не всеми компиляторами. Поскольку наши тексты были написаны до появления этого ключевого слова, в них употребляется class. Шаблон класса list_item выглядит так:
template <class elemType>
class list_item {
public:
list_item( elemType value, list_item *item = 0 )
: _value( value ) {
if ( !item )
_next = 0;
else {
_next = item->_next;
item->_next = this;
}
}
elemType value() { return _value; }
list_item* next() { return _next; }
void next( list_item *link ) { _next = link; }
void value( elemType new_value ) { _value = new_value; }
private:
elemType _value;
list_item *_next;
};
Все упоминания типа int в определении класса ilist_item заменены на параметр elemType. Когда мы пишем:
list_item<doub1e> *ptr = new list_item<doub1e>( 3.14 );
компилятор подставляет double вместо elemType и создает экземпляр list_item, поддерживающий данный тип.
Аналогичным образом модифицируем класс ilist в шаблон класса list:
template <class elemType>
class list {
public:
list()
: _at_front( 0 ), _at_end( 0 ), _current( 0 ),
_size( 0 ) {}
1ist( const list& );
list& operator=( const list& );
~list() { remove_all(); }
void insert ( list_item<elemType> *ptr, elemType value );
void insert_end( elemType value );
void insert_front( elemType value );
void insert_all( const list &rhs );
int remove( elemType value );
void remove_front();
void remove_all();
list_item<elemType> *find( elemType value );
list_item<elemType> *next_iter();
list_item<elemType>* init_iter( list_item<elemType> *it );
void disp1ay( ostream &os = cout );
void concat( const list& );
void reverse ();
int size() { return _size; }
private:
void bump_up_size() { ++_size; }
void bump_down_size() { --_size; }
list_item<elemType> *_at_front;
1ist_item<elemType> *_at_end;
list_item<elemType> *_current;
int _size;
};
Объекты шаблона класса list используются точно так же, как и объекты класса ilist. Основное преимущество шаблона в том, что он обеспечивает поддержку произвольных типов данных с помощью единственного определения.
(Шаблоны являются важной составной частью концепции программирования на С++. В главе 6 мы рассмотрим набор классов контейнерных типов, предоставляемых стандартной библиотекой С++. Неудивительно, что она содержит шаблон класса, реализующего операции со списками, равно как и шаблон класса, поддерживающего векторы; мы рассматривали их в главах 2 и 3.)
Наличие класса списка в стандартной библиотеке представляет некоторую проблему. Мы выбрали для нашей реализации название list, но, к сожалению, стандартный класс также носит это название. Теперь мы не можем использовать в программе одновременно оба класса. Конечно, проблему решит переименование нашего шаблона, однако во многих случаях эта возможность отсутствует.
Более общее решение состоит в использовании механизма пространства имен, который позволяет разработчику библиотеки заключить все свои имена в некоторое поименованное пространство и таким образом избежать конфликта с именами из глобального пространства. Применяя нотацию квалифицированного доступа, мы можем употреблять эти имена в программах. Стандартная библиотека С++ помещает свои имена в пространство std. Мы тоже поместим наш код в собственное пространство:
namespace Primer_Third_Edition
{
template <typename elemType>
class list_item{ ... };
template <typename elemType>
class list{ ... };
// ...
}
Для использования такого класса в пользовательской программе необходимо написать следующее:
// наш заголовочный файл
#include "list.h"
// сделаем наши определения видимыми в программе
using namespace Primer_Third_Edition;
// теперь можно использовать наш класс list
list< int > ilist;
// ...
(Пространства имен описываются в разделах 8.5 и 8.6.)
Упражнение 5.16
Мы не определили деструктор для ilist_item, хотя класс содержит указатель на динамическую область памяти. Причина заключается в том, что класс не выделяет память для объекта, адресуемого указателем _next, и, следовательно, не несет ответственности за ее освобождение. Начинающий программист мог бы допустить ошибку, вызвав деструктор для ilist_item:
ilist_item::~ilist_item()
{
delete _next;
}
Посмотрите на функции remove_all() и remove_front() и объясните, почему наличие такого деструктора является ошибочным.
Упражнение 5.17
Наш класс ilist не поддерживает следующие операции:
void ilist::remove_end();
void ilist::remove( ilist_item* );
Как вы думаете, почему мы их не включили? Реализуйте их.
Упражнение 5.18
Модифицируйте функцию find() так, чтобы вторым параметром она принимала адрес элемента, с которого нужно начинать поиск. Если этот параметр не задан, поиск начинается с первого элемента. (Поскольку мы добавляем второй параметр, имеющий значение по умолчанию, открытый интерфейс данной функции не меняется. Программы, использующие предыдущую версию find(), будут работать без модификации.)
class ilist {
public:
// ...
ilist_item* find( int value, ilist_item *start_at = 0 );
// ...
};
Упражнение 5.19
Используя новую версию find(), напишите функцию count(), которая подсчитывает количество вхождений элементов с заданным значением. Подготовьте тестовую программу.
Упражнение 5.20
Модифицируйте insert(int value) так, чтобы она возвращала указатель на вставленный объект ilist_item.
Упражнение 5.21
Используя модифицированную версию insert(), напишите функцию:
void ilist::
insert( ilist_item *begin,
int *array_of_value,
int elem_cnt );
где array_of_value указывает на массив значений, который нужно вставить в ilist, elem_cnt – на размер этого массива, а begin – на элемент, после которого производится вставка. Например, если есть ilist:
(3)( 0 1 21 )
и массив:
int ia[] = { 1, 2, 3, 5, 8, 13 };
вызов этой новой функции
ilist_item *it = mylist.find( 1 );
mylist.insert( it, ia, 6 );
изменит список таким образом:
(9) ( 0 1 1 2 3 5 8 13 21 )
Упражнение 5.22
Функции concat() и reverse() модифицируют оригинальный список. Это не всегда желательно. Напишите аналогичную пару функций, которые создают новый объект ilist:
ilist ilist::reverse_copy();
ilist ilist::concat_copy( const ilist &rhs );
Обрабатываем знаки препинания
После того как мы разбили каждую строку на слова, необходимо избавиться от знаков препинания. Пока из строки
magical but untamed. "Daddy, shush, there is no such thing,"
у нас получился такой набор слов:
magical
but
untamed.
"Daddy,
shush,
there
is
no
such
thing,"
Как нам теперь удалить ненужные знаки препинания? Для начала определим строку, содержащую все символы, которые мы хотим удалить:
string filt_elems( "\",.;:!?)(\\/" );
(Обратная косая черта указывает на то, что следующий за ней символ должен в данном контексте восприниматься буквально, а не как специальная величина. Так, \" обозначает символ двойной кавычки, а не конец строки, а \\ – символ обратной косой черты.)
Теперь можно применить функцию-член find_first_of() для поиска всех вхождений нежелательных символов:
while (( pos = word.find_first_of( filt_elems, pos ))
!= string::npos )
Найденный символ удаляется с помощью функции-члена erase():
word.erase(pos,1);
Первый аргумент этой функции означает позицию подстроки, а второй – ее длину. Мы удаляем один символ, находящийся в позиции pos. Второй аргумент является необязательным; если его опустить, будут удалены все символы от pos до конца строки.
Вот полный текст функции filter_text(). Она имеет два параметра: указатель на вектор строк, содержащий текст, и строку с символами, которые нужно убрать.
void
filter_text( vector<string> *words, string filter )
{
vector<string>::iterator iter = words->begin();
vector<string>::iterator iter_end = words->end();
// Если filter не задан, зададим его сами
if ( ! filter.size() )
filter.insert( 0, "\".," );
while ( iter != iter_end ) {
string::size_type pos = 0;
// удалим каждый найденный элемент
while (( pos = (*iter).find_first_of( filter, pos ))
!= string::npos )
(*iter).erase(pos,1);
iter++;
}
}
Почему мы не увеличиваем значение pos на каждой итерации? Что было бы, если бы мы написали:
while (( pos = (*iter).find_first_of( filter, pos ))
!= string::npos )
{
(*iter).erase(pos,1);
++ pos; // неправильно...
}
Возьмем строку
thing,"
На первой итерации pos получит значение 5 , т.е. позиции, в которой находится запятая. После удаления запятой строка примет вид
thing"
Теперь в 5-й позиции стоит двойная кавычка. Если мы увеличим значение pos, то пропустим этот символ.
Так мы будем вызывать функцию filter_text():
string filt_elems( "\",.;:!?)(\\/" );
filter_text( text_locations->first, filt_elems );
А вот часть распечатки, сделанной тестовой версией filter_text():
filter_text: untamed.
found! : pos: 7.
after: untamed
filter_text: "Daddy,
found! : pos: 0.
after: Daddy,
found! : pos: 5.
after: Daddy
filter_text: thing,"
found! : pos: 5.
after: thing"
found! : pos: 5.
after: thing
filter_text: "I
found! : pos: 0.
after: I
filter_text: Daddy,
found! : pos: 5.
after: Daddy
filter_text: there?"
found! : pos: 5.
after: there"
found! : pos: 5.
after: there
Упражнение 6.15
Напишите программу, которая удаляет все символы, кроме STL из строки:
"/.+(STL).$1/"
используя сначала erase(pos,count), а затем erase(iter,iter).
Упражнение 6.16
Напишите программу, которая с помощью разных функций вставки из строк
string sentence( "kind of" );
string s1 ( "whistle" )
string s2 ( "pixie" )
составит предложение
"A whistling-dixie kind of walk"
Обработка исключений
Обработка исключений – это механизм, позволяющий двум независимо разработанным программным компонентам взаимодействовать в аномальной ситуации, называемой исключением. В этой главе мы расскажем, как генерировать, или возбуждать, исключение в том месте программы, где имеет место аномалия. Затем мы покажем, как связать catch-обработчик исключений с множеством инструкций программы, используя try-блок. Потом речь пойдет о спецификации исключений – механизме, с помощью которого можно связать список исключений с объявлением функции, и функция не сможет возбудить никаких других исключений. Закончится эта глава обсуждением решений, принимаемых при проектировании программы, в которой используются исключения.
Обработка исключения типа класса
Если исключения организуются в иерархии, то исключение типа некоторого класса может быть перехвачено обработчиком, соответствующим любому его открытому базовому классу. Например, исключение типа pushOnFull перехватывается обработчиками исключений типа stackExcp или Excp.
int main() {
try {
// ...
}
catch ( Excp ) {
// обрабатывает исключения popOnEmpty и pushOnFull
}
catch ( pushOnFull ) {
// обрабатывает исключение pushOnFull
}
Здесь порядок catch-обработчиков желательно изменить. Напоминаем, что они просматриваются в порядке появления после try-блока. Как только будет найден обработчик, способный обработать данное исключение, поиск прекращается. В примере выше Excp может обработать исключения типа pushOnFull, а это значит, что специализированный обработчик таких исключений задействован не будет. Правильная последовательность такова:
catch ( pushOnFull ) {
// обрабатывает исключение pushOnFull
}
catch ( Excp ) {
// обрабатывает другие исключения
}
catch-обработчик для производного класса должен идти первым. Тогда catch-обработчик для базового класса получит управление только в том случае, если более специализированного обработчика не нашлось.
Если исключения организованы в иерархии, то пользователи библиотеки классов могут выбрать в своем приложении уровень детализации при работе с исключениями, возбужденными внутри библиотеки. Например, кодируя функцию main(), мы решили, что исключения типа pushOnFull должны обрабатываться несколько иначе, чем прочие, и потому написали для них специализированный catch-обработчик. Что касается остальных исключений, то они обрабатываются единообразно:
catch ( pushOnFull eObj ) {
// используется функция-член value() класса pushOnFull
// см. раздел 11.3
cerr << "попытка поместить значение " << eObj.value()
<< " в полный стек\n";
}
catch ( Excp ) {
// используется функция-член print() базового класса
Excp::print( "произошло исключение" );
}
Как отмечалось в разделе 11.3, процесс поиска catch- обработчика для возбужденного исключения не похож на процесс разрешения перегрузки функций. При выборе наилучшей из устоявших функций принимаются во внимание все кандидаты, видимые в точке вызова, а при обработке исключений найденный catch-обработчик совсем не обязательно будет лучше остальных соответствовать типу исключения. Выбирается первый подходящий обработчик, т.е. первый из просмотренных, который способен обработать данное исключение. Поэтому в списке обработчиков наиболее специализированные должны стоять ближе к началу.
Объявление исключения в catch-обработчике (находящееся в скобках после слова catch) очень похоже на объявление параметра функции. В приведенном примере оно напоминает параметр, передаваемый по значению. Объект eObj инициализируется копией значения объекта-исключения точно так же, как передаваемый по значению формальный параметр функции инициализируется значением фактического аргумента. Как и в случае с параметрами функции, в объявлении исключения можно использовать ссылки. Тогда catch-обработчик имеет доступ непосредственно к объекту-исключению, созданному выражением throw, а не к его локальной копии. Чтобы избежать копирования больших объектов, параметры типа класса следует объявлять как ссылки; в объявлениях исключений тоже желательно делать исключения типа класса ссылками. В зависимости от того, что находится в таком объявлении (объект или ссылка), поведение обработчика различается (мы покажем эти различия в данном разделе).
В главе 11 были введены выражения повторного возбуждения исключения, которые используются в catch-обработчике для передачи исключения какому-то другому обработчику выше в цепочке вызовов. Такое выражение имеет вид
throw;
Как ведет себя эта инструкция, если она расположена в catch-обработчике исключений базового класса? Например, каким будет тип повторно возбужденного исключения, если mathFunc() возбуждает исключение типа divideByZero?
void calculate( int parm ) {
try {
mathFunc( parm ); // возбуждает исключение divideByZero
}
catch ( mathExcp mExcp ) {
// частично обрабатывает исключение
// и генерирует объект-исключение еще раз
throw;
}
}
Будет ли повторно возбужденное исключение иметь тип divideByZero–тот же, что и исключение, возбужденное функцией mathFunc()? Или тип mathExcp, который указан в объявлении исключения в catch-обработчике?
Напомним, что выражение throw повторно генерирует исходный объект-исключение. Так как исходный объект имеет тип divideByZero, то повторно возбужденное исключение будет такого же типа. В catch-обработчике объект mExcp инициализируется копией подобъекта объекта типа divideByZero, который соответствует его базовому классу MathExcp. Доступ к ней осуществляется только внутри catch-обработчика, она не является исходным объектом-исключением, который повторно генерируется.
Предположим, что классы в нашей иерархии исключений имеют деструкторы:
class pushOnFull {
public:
pushOnFull( int i ) : _value( i ) { }
int value() { return _value; }
~pushOnFull(); // вновь объявленный деструктор
private:
int _value;
};
Когда они вызываются? Чтобы ответить на этот вопрос, рассмотрим catch-обработчик:
catch ( pushOnFull eObj ) {
cerr << "попытка поместить значение " << eObj.value()
<< " в полный стек\n";
}
Поскольку в объявлении исключения eObj объявлен как локальный для catch-обработчика объект, а в классе pushOnFull есть деструктор, то eObj уничтожается при выходе из обработчика. Когда же вызывается деструктор для объекта-исключения, созданного в момент возбуждения исключения, – при входе в catch-обработчик или при выходе из него? Однако уничтожать исключение в любой из этих точек может быть слишком рано. Можете сказать, почему? Если catch-обработчик возбуждает исключение повторно, передавая его выше по цепочке вызовов, то уничтожать объект-исключение нельзя до момента выхода из последнего catch-обработчика.
Обратные итераторы
Операции begin() и end() возвращают соответственно итераторы, указывающие на первый элемент и на элемент, расположенный за последним. Можно также вернуть обратный итератор, обходящий контейнер от последнего элемента к первому. Во всех контейнерах для поддержки такой возможности используются операции rbegin() и rend(). Есть константные и неконстантные версии обратных итераторов:
vector< int > vec0;
const vector< int > vec1;
vector< int >::reverse_iterator r_iter0 = vec0.rbegin();
vector< int >::const_reverse_iterator r_iter1 = vec1.rbegin();
Обратный итератор применяется так же, как прямой. Разница состоит в реализации операторов перехода к следующему и предыдущему элементам. Для прямого итератора оператор ++ дает доступ к следующему элементу контейнера, тогда как для обратного – к предыдущему. Например, для обхода вектора в обратном направлении следует написать:
// обратный итератор обходит вектор от конца к началу
vector< type >::reverse_iterator r_iter;
for ( r_iter = vec0.rbegin(); // r_iter указывает на последний элемент
r_iter != vec0.rend(); // пока не достигли элемента перед первым
r_iter++ ) // переходим к предыдущему элементу
{ /* ... */ }
Инвертирование семантики операторов инкремента и декремента может внести путаницу, но зато позволяет программисту передавать алгоритму пару обратных итераторов вместо прямых. Так, для сортировки вектора в порядке убывания мы передаем алгоритму sort() пару обратных итераторов:
// сортирует вектор в порядке возрастания
sort( vec0.begin(), vec0.end() );
// сортирует вектор в порядке убывания
sort( vec0.rbegin(), vec0.rend() );
Очередь и очередь с приоритетами
Абстракция очереди реализует метод доступа FIFO (first in, first out – “первым вошел, первым вышел”): объекты добавляются в конец очереди, а извлекаются из начала. Стандартная библиотека предоставляет две разновидности этого метода: очередь FIFO, или простая очередь, и очередь с приоритетами, которая позволяет сопоставлять элементы с их приоритетами. Текущий элемент помещается не в конец такой очереди, а перед элементами с более низким приоритетом. Программист, определяющий такую структуру, задает способ вычисления приоритетов. В реальной жизни подобное можно увидеть, скажем, при регистрации багажа в аэропорту. Как правило, пассажиры, чей рейс через 15 минут, передвигаются в начало очереди, чтобы не опоздать на самолет. Примером из практики программирования служит планировщик операционной системы, определяющий последовательность выполнения процессов.
Для использования queue и priority_queue необходимо включить заголовочный файл:
#include <queue>
Полный набор операций с контейнерами queue и priority_queue приведен в таблице 6.6.
Таблица 6.6. Операции с queue и priority_queue
Операция | Действие | ||
empty() | Возвращает true, если очередь пуста, и false в противном случае | ||
size() | Возвращает количество элементов в очереди | ||
pop() | Удаляет первый элемент очереди, но не возвращает его значения. Для очереди с приоритетом первым является элемент с наивысшим приоритетом | ||
front() | Возвращает значение первого элемента очереди, но не удаляет его. Применимо только к простой очереди | ||
back() | Возвращает значение последнего элемента очереди, но не удаляет его. Применимо только к простой очереди | ||
top() | Возвращает значение элемента с наивысшим приоритетом, но не удаляет его. Применимо только к очереди с приоритетом | ||
push(item) | Помещает новый элемент в конец очереди. Для очереди с приоритетом позиция элемента определяется его приоритетом. |
Элементы priority_queue отсортированы в порядке убывания приоритетов. По умолчанию упорядочение основывается на операции “меньше”, определенной над парами элементов. Конечно, можно явно задать указатель на функцию или объект-функцию, которая будет использоваться для сортировки. (В разделе 12.3 можно найти более подробное объяснение и иллюстрации использования такой очереди.)
Ограничение прав на создание объекта
Доступность конструктора определяется тем, в какой секции класса он объявлен. Мы можем ограничить или явно запретить некоторые формы создания объектов, если поместим соответствующий конструктор в неоткрытую секцию. В примере ниже конструктор по умолчанию класса Account объявлен закрытым, а с двумя параметрами– открытым:
class Account {
friend class vector< Account >;
public:
explicit Account( const char*, double = 0.0 );
// ...
private:
Account();
// ...
};
Обычная программа сможет теперь определять объекты класса Account, лишь указав как имя владельца счета, так и начальный баланс. Однако функции-члены Account и дружественный ему класс vector могут создавать объекты, пользуясь любым конструктором.
Конструкторы, не являющиеся открытыми, в реальных программах C++ чаще всего используются для:
предотвращения копирования одного объекта в другой объект того же класса (эта проблема рассматривается в следующем подразделе);
указания на то, что конструктор должен вызываться только в случае, когда данный класс выступает в роли базового в иерархии наследования, а не для создания объектов, которыми программа может манипулировать напрямую (см. обсуждение наследования и объектно-ориентированного программирования в главе 17).
Окончательная программа
Ниже представлен полный текст программы, разработанной в этой главе, с двумя модификациями: мы инкапсулировали все структуры данных и функции в класс TextQuery (в последующих главах мы обсудим подобное использование классов), кроме того, текст был изменен, так как наш компилятор поддерживал стандарт С++ не полностью.
Например, библиотека iostream не соответствовала текущему стандарту. Шаблоны не поддерживали значения аргументов по умолчанию. Возможно, вам придется изменить кое-что в этой программе, чтобы она компилировалась в вашей системе.
// стандартные заголовочные файлы С++
#include <algorithm>
#include <string>
#include <vector>
#include <utility>
#include <map>
#include <set>
// заголовочный файл iostream, не отвечающий стандарту
#include <fstream.h>
// заголовочные файлы С
#include <stddef.h>
#include <ctype.h>
// typedef для удобства чтения
typedef pair<short,short> location;
typedef vector<location,allocator> loc;
typedef vector<string,allocator> text;
typedef pair<text*,loc*> text_loc;
class TextQuery {
public:
TextQuery() { memset( this, 0, sizeof( TextQuery )); }
static void
filter_elements( string felems ) { filt_elems = felems; }
void query_text();
void display_map_text();
void display_text_locations();
void doit() {
retrieve_text();
separate_words();
filter_text();
suffix_text();
strip_caps();
build_word_map();
}
private:
void retrieve_text();
void separate_words():
void filter_text();
void strip_caps();
void suffix_textQ;
void suffix_s( string& );
void build_word_map();
private:
vector<string,allocator> *lines_of_text;
text_loc *text_locations;
map< string,loc*,
less<string>,allocator> *word_map;
ааа static stringаааааааааааааааа filt_elems;
};
string TextQuery::filt_elems( "\", Х;: !?)(\V" );
int main()
{
ааа TextQuery tq;
ааа tq.doit();
ааа tq.query_text();
ааа tq.display_map_text();
}
void
TextQuery::
retrieve_text()
{
ааа string file_name;
ааа cout << " please enter file name: ";
ааа cin >> file_name;
ааа ifstream infile( file_name.c_str(), ios::in );
ааа if ( !infile ) {
ааааааа cerr << "oops' unable to open file "
аааааааааааа << file_name <<ааа " -- bailing out!\n";
аааааа аexit( -1 );
ааа }
ааа else cout << "\n";
ааа lines_of_text = new vector<string,allocator>;
ааа string textline;
ааа while ( getline( infile, textline, '\n' ))
ааааааа lines_of_text->push_back( textline );
}
void
TextQuery::
separate_words()
{
ааа vector<string,allocator> *words =
аааааааааааааа new vector<string,allocator>;
ааа vector<location,allocator> *locations =
аааааааааааааа new vector<location,allocator>;
ааа for ( short line_pos = 0; line_pos < lines_of_text->size();
ааааааа line_pos++ )
ааа {
ааааааа short word_pos = 0;
ааааааа string textline = (*lines_of_text)[ line_pos ];
ааааааа string::size_type eol = textline.1ength();
ааааааа string::size_type pos = 0, prev_pos = 0;
ааааааа while (( pos = textline.find_first_of( ' ', pos ))
ааааааааа аааааааааа!= string::npos )
ааааааа {
ааааааааааа words->push_back(
ааааааааааааааа textline.substr( prev_pos, pos - prev_pos ));
ааааааааааа locations->push_back(
ааааааааааааааа make_pair( line_pos, word_pos ));
ааааааааааа word_pos++; pos++; prev_pos = pos;
ааааааа }
ааааааа words->push_back(
ааааааааааа textline.substr( prev_pos, pos - prev_pos ));
ааааааа locations->push_back(make_pair(line_pos,word_pos));
ааа }
ааа text_locations = new text_loc( words, locations );
}
void
TextQuery::
filter_text()
{
ааа if ( filt_elems.empty() )
ааааааа return;
ааа vector<string,allocator> *words = text_locations->first;
ааа vector<string,allocator>:: iterator iter = words->begin();
ааа vector<string,allocator>::iterator iter_end = words->end();
ааа while ( iter != iter_end )
ааа {
ааааааа string::size_type pos = 0;
ааааааа while ((pos = (*iter).find_first_of(filt_elems, pos))
аааааааааааааааааа != string::npos )
ааааааааааа (*iter).erase(pos,l);
ааааааа ++iter;
ааа }
}
void
TextQuery::
suffix_text()
{
ааа vector<string,allocator> *words = text_locations->first;
ааа vector<string,allocator>::iterator iter = words->begin();
ааа vector<string,allocator>::iterator iter_end = words->end() ;
ааа while ( iter != iter_end ) {
ааааааа if ( (*iter).size() <= 3 )
аааааааа ааа{ iter++; continue; }
ааааааа if ( (*iter)[ (*iter).size()-l ] == 's' )
ааааааааааа suffix_s( *iter );
ааааааа // фюяюыэшЄхы№эр юсЁрсюЄър ёєЇЇшъёют...
ааааааа iter++;
ааа }
}
void
TextQuery::
suffix_s( string &word )
{
ааа string::size_type spos = 0;
ааа string::size_type pos3 = word.size()-3;
ааа // "ous", "ss", "is", "ius"
ааа string suffixes( "oussisius" );
ааа if ( ! word.compare( pos3, 3, suffixes, spos, 3 ) ||
аааааааа ! word.compare( pos3, 3, suffixes, spos+6, 3) ||
аааааааа ! word.compare( pos3+l, 2, suffixes, spos+2, 2 ) ||
аааааааа ! word.compare( pos3+l, 2, suffixes, spos+4, 2 ))
ааааааааааа return;
ааа string ies( "ies" );
ааа if ( ! word.compare( pos3, 3, ies ))
ааа {
ааааааа word.replace( pos3, 3, 1, 'є' );
ааааааа return;
ааа }
ааа string ses( "ses" );
ааа if ( ! word.compare( pos3, 3, ses ))
ааа {
ааааааа word.erase( pos3+l, 2 );
ааааааа return;
ааа }
ааа // єфрышь 's' т ъюэЎх
ааа word.erase( pos3+2 );
ааа // єфрышь "'s"
ааа if ( word[ pos3+l ] == '\'' )
ааааааа word.erase( pos3+l );
}
void
TextQuery::
strip_caps()
{
ааа vector<string,allocator> *words = text_locations->first;
ааа vector<string,allocator>::iterator iter = words->begin();
ааа vector<string,allocator>::iterator iter_end = words->end();
ааа string caps( "ABCDEFGHI3KLMNOPQRSTUVWXYZ" );
ааа while ( iter != iter_end ) {
ааааааа string::size_type pos = 0;
ааааааа while (( pos = (*iter).find_first_of( caps, pos ))
ааааааааааааааааааа != string::npos )
ааааааааааа (*iter)[ pos ] = to1ower( (*iter)[pos] );
ааааааа ++iter;
ааа }
}
void
TextQuery::
build_word_map()
{
ааа word_map = new map<string,loc*,less<string>,allocator>;
ааа typedef map<string,loc*,less<string>,allocator>::value_type
ааааааа value_type;
ааа typedef set<string,less<string>,allocator>::difference_type
ааааааа diff_type;
ааа set<string,less<string>,allocator> exclusion_set;
ааа ifstream infile( "exclusion_set" );
ааа if ( !infile )
ааа {
ааааааа static string default_excluded_words[25] = {
ааааааааа "the","and","but","that","then","are","been",
ааааааааа "can","can't","cannot","could","did","for",
ааааааааа "had","have","him","his","her","its"."into",
ааааааааа "were","which","when","with","would"
ааааааа };
ааааааа cerr <<
аааааааааа "warning! unable to open word exclusion file! -- "
аааааааааааа << "using default set\n";
ааааааа copy( default_excluded_words,
ааааааааааааа default_excluded_words+25,
ааааааааааааа inserter(exclusion_set, exclusion_set.begin()));
ааа }
ааа else {
ааааааа istream_iterator< string, diff_type >
ааааааааааа input_set( infile ), eos;
ааааааа copy( input_set, eos,
ааааааааааа inserter( exclusion_set, exclusion_set.begin() ));
ааа }
ааа // яЁюсхцшьё яю тёхь ёыютрь, тёЄрты ярЁv
ааа vector<string,allocator> *text_words =
ааааааа text_locations->first;
ааа vector<location,allocator> *text.locs =
ааааааа text_locations->second;
ааа register int elem_cnt = text_words->size();
ааа for ( int ix = 0; ix < elem_cnt; ++-ix )
ааа {
ааааааа string textword = ( *text_words )[ ix ];
ааааааа if ( textword.size() < 3 ||
ааааааааааа exclusion_set.count( textword ))
ааааааааааааааа continue;
ааааааа if ( ! word_map->count((*text_words)[ix] ))
ааааааа { // ёыютю юЄёєЄёЄтєхЄ, фюсртшь:
ааааааааааа loc *ploc = new vector<location,allocator>;
ааааааааааа ploc->push_back( (*text_locs)[ix] );
ааааааааааа word_map->
ааааааааааааааа insert( value_type( (*text_words)[ix],ploc ));
ааааааа }
ааааааа else (*word_map) [(*text_words) [ix]]->
ааааааааааааааа push_back( (*text_locs) [ix] );
ааа }
}
void
TextQuery::
query_text()
{
ааа string query_text;
ааа do {
ааааааа cout
ааааааа << "enter a word against which to search the text.\n"
ааааааа << "to quit, enter a single character ==> ";
ааааааа cin >> query_text;
ааааааа if ( query_text.size() < 2 ) break;
ааааааа string caps( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" );
ааааааа string::size_type pos = 0;
ааааааа while (( pos = query_text.find_first_of( caps, pos ))
ааааааааааааааааа аа!= string::npos )
ааааааааааа query_text[ pos ] = to1ower( query_text[pos] );
ааааааа // query_text фюыцэю сvЄ№ ттхфхэю
ааааааа if ( !word_map->count( query_text )) {
ааааааааааа cout << "\nSorry. There are no entries for "
аааааааааааааааа << query_text << ".\n\n";
ааааааааааа continue;
ааааааа }
ааааааа loc *ploc = (*word_map) [ query_text ];
ааааааа set<short,less<short>,allocator> occurrence_1i nes;
ааааааа loc::iterator liter = ploc->begin(),
ааааааааааааааааааааа liter_end = ploc->end();
ааааа ааwhile ( liter != liter_end ) {
аааааааааааа occurrence_lines.1nsert(
аааааааааааааааааа occurrence_lines.end(), (*liter).first);
аааааааааааа ++liter;
ааааааа }
ааааааа register int size = occurrence_lines.size();
ааааааа cout << "\n" << query_text
ааа ааааааааа<< " occurs " << size
аааааааааааа << (size == 1 ? " time:" : " times:")
аааааааааааа << "\n\n";
ааааааа set<short,less<short>,allocator>::iterator
ааааааааааааа it=occurrence_lines.begin();
ааааааа for ( ; it != occurrence_"lines.end(); ++it ) {
ааааааааааа int line = *it;
ааааааааааа cout << "\t( line "
аааааааааааааааа // сєфхь эєьхЁютрЄ№ ёЄЁюъш ё 1,
аааааааааааааааа // ъръ ¤Єю яЁшэ Єю тхчфх
аааааааааааааааа << line + 1 << " ) "
аааааааааааааааа << (*lines_of_text)[line] << endl;
ааааааа }
ааааааа cout << endl;
ааа }
ааа while ( ! query_text.empty() );
ааа cout << "Ok, bye!\n";
}
void
TextQuery::
display_map_text()
{
ааа typedef map<string,loc*, less<string>, allocator> map_text;
ааа map_text::iterator iter = word_map->begin(),
аааааааааааааааааааааа iter_end = word_map->end();
ааа while ( iter != iter_end ) {
ааааааа cout << "word: " << (*iter).first << " (";
ааааааа intаааааааааа loc_cnt = 0;
ааааааа locааааааааа *text_locs = (*iter).second;
ааааааа loc::iterator literаааа = text_locs->begin(),
ааааааааааааааааааааа liter_end = text_locs->end();
ааааааа while ( liter != liter_end )
ааааааа {
ааааааааааа if ( loc_cnt )
ааааааааааааааа cout << ",";
ааааааааааа else ++loc_cnt;
ааааааааааа cout << "(" << (*liter).first
аааааааааааааааа << "," << (*liter).second << ")";
ааааааааааа ++"liter;
ааааааа }
ааааааа cout << ")\n";
ааааааа ++iter;
ааа }
ааа cout << endl;
}
void
TextQuery::
disp1ay_text_locations()
{
ааа vector<string,allocator> *text_words =
ааааааа text_locations->first;
ааа vector<location,allocator> *text_locs =
ааааааа text_locations->second;
ааа register int elem_cnt = text_words->size();
ааа if ( elem_cnt != text_locs->size() )
ааа {
ааааааа cerr
аааааааа << "oops! internal error: word and position vectors "
аааааааа << "are of unequal size\n"
аааааааа << "words: " << elem_cnt << " "
аааааааа << "locs: " << text_locs->size()
аааааааа << " -- bailing out!\n";
ааааааа exit( -2 );
ааа }
ааа for ( int ix=0; ix < elem_cnt; ix++ )
ааа {
ааааааа cout << "word: " << (*text_words)[ ix ] << "\t"
аааааааааааа << "location: ("
аааааааааааа << (*text_locs)[ix].first << ","
аааааааааааа << (*text.locs)[ix].second << ")"
аааааааааааа << "\n";
ааа }
ааа cout << endl;
}
LяЁрцэхэшх 6.25
+с· ёэшЄх, яюўхьє эрь яюЄЁхсютрыё ёяхЎшры№эvщ ъырёё inserter фы чряюыэхэш эрсюЁр ёЄюя-ёыют (¤Єю єяюьшэрхЄё т Ёрчфхых 6.13.1, р фхЄры№эю ЁрёёьрЄЁштрхЄё т 12.4.1).
set<string> exclusion_set;
ifstreamааа infile( "exclusion_set" );
copy( default_excluded_words, default_excluded_words+25,
ааааа inserter(exclusion_set, exclusion_set.begin() ));
LяЁрцэхэшх 6.26
¦хЁтюэрўры№эр ЁхрышчрЎш яюшёъютющ ёшёЄхьv юЄЁрцрхЄ яЁюЎхфєЁэvщ яюфїюф: эрсюЁ уыюсры№эvї ЇєэъЎшщ юяхЁшЁєхЄ эрсюЁюь эхчртшёшьvї ёЄЁєъЄєЁ фрээvї. +ъюэўрЄхы№эvщ трЁшрэЄ яЁхфёЄрты хЄ ёюсющ ры№ЄхЁэрЄштэvщ яюфїюф, ъюуфр ьv шэъряёєышЁєхь ЇєэъЎшш ш фрээvх т ъырёё TextQuery. TЁртэшЄх юср ёяюёюср. ¦ръютv эхфюёЄрЄъш ш яЁхшьє•хёЄтр ърцфюую?
LяЁрцэхэшх 6.27
T фрээющ тхЁёшш яЁюуЁрььv шь Їрщыр ё ЄхъёЄюь ттюфшЄё яю чряЁюёє. +юыхх єфюсэю сvыю сv чрфртрЄ№ хую ъръ ярЁрьхЄЁ ъюьрэфэющ ёЄЁюъш; т уыртх 7 ьv яюърцхь, ъръ ¤Єю фхырхЄё . ¦ръшх х•х ярЁрьхЄЁv ъюьрэфэющ ёЄЁюъш цхырЄхы№эю ЁхрышчютрЄ№?
Опасность увеличения размера программы
Встроенный деструктор может стать причиной непредвиденного увеличения размера программы, поскольку он вставляется в каждой точке выхода внутри функции для каждого активного локального объекта. Например, в следующем фрагменте
Account acct( "Tina Lee" );
int swt;
// ...
switch( swt ) {
case 0:
return;
case 1:
// что-то сделать
return;
case 2:
// сделать что-то другое
return;
// и так далее
}
компилятор подставит деструктор перед каждой инструкцией return. Деструктор класса Account невелик, и затраты времени и памяти на его подстановку тоже малы. В противном случае придется либо объявить деструктор невстроенным, либо реорганизовать программу. В примере выше инструкцию return в каждой метке case можно заменить инструкцией break с тем, чтобы у функции была единственная точка выхода:
// переписано для обеспечения единственной точки выхода
switch( swt ) {
case 0:
break;
case 1:
// что-то сделать
break;
case 2:
// сделать что-то другое
break;
// и так далее
}
// единственная точка выхода
return;
Упражнение 14.6
Напишите подходящий деструктор для приведенного набора членов класса, среди которых pstring адресует динамически выделенный массив символов:
class NoName {
public:
~NoName();
// ...
private:
char *pstring;
int ival;
double dval;
};
Упражнение 14.7
Необходим ли деструктор для класса, который вы выбрали в упражнении 14.3? Если нет, объясните почему. В противном случае предложите реализацию.
Упражнение 14.8
Сколько раз вызываются деструкторы в следующем фрагменте:
void mumble( const char *name, fouble balance, char acct_type )
{
Account acct;
if ( ! name )
return;
if ( balance <= 99 )
return;
switch( acct_type ) {
case 'z': return;
case 'a':
case 'b': return;
}
// ...
}
Операции инкремента и декремента
Операции инкремента (++) и декремента (--) дают возможность компактной и удобной записи для изменения значения переменной на единицу. Чаще всего они используются при работе с массивами и коллекциями – для изменения величины индекса, указателя или итератора:
#include <vector>
#include <cassert>
int main()
{
int ia[10] = {0,1,2,3,4,5,6,7,8,9};
vector<int> ivec( 10 );
int ix_vec = 0, ix_ia = 9;
while ( ix_vec < 10 )
ivec[ ix_vec++ ] = ia[ ix_ia-- ];
int *pia = &ia[9];
vector<int>::iterator iter = ivec.begin();
while ( iter != ivec.end() )
assert( *iter++ == *pia-- );
}
Выражение
ix_vec++
является постфиксной
формой оператора инкремента. Значение переменной ix_vec увеличивается после того, как ее текущее значение употреблено в качестве индекса. Например, на первой итерации цикла значение ix_vec равно 0. Именно это значение применяется как индекс массива ivec, после чего ix_vec увеличивается и становится равным 1, однако новое значение используется только на следующей итерации. Постфиксная форма операции декремента работает точно так же: текущее значение ix_ia берется в качестве индекса для ia, затем ix_ia уменьшается на 1.
Существует и префиксная форма этих операторов. При использовании такой формы текущее значение сначала уменьшается или увеличивается, а затем используется новое значение. Если мы пишем:
// неверно: ошибки с границами индексов в
// обоих случаях
int ix_vec = 0, ix_ia = 9;
while ( ix_vec < 10 )
ivec[ ++ix_vec ] = ia[ --ix_ia ];
значение ix_vec увеличивается на единицу и становится равным 1 до первого использования в качестве индекса. Аналогично ix_ia получает значение 8 при первом использовании. Для того чтобы наша программа работала правильно, мы должны скорректировать начальные значения переменных ix_ivec и ix_ia:
// правильно
int ix_vec = -1, ix_ia = 8;
while ( ix_vec < 10 )
ivec[ ++ix_vec ] = ia[ --ix_ia ];
В качестве последнего примера рассмотрим понятие стека. Это фундаментальная абстракция компьютерного мира, позволяющая помещать и извлекать элементы в последовательности LIFO
(last in, fist out – последним вошел, первым вышел). Стек реализует две основные операции – поместить (push) и извлечь (pop).
Текущий свободный элемент называют вершиной стека. Операция push присваивает этому элементу новое значение , после чего вершина смещается вверх (становится на 1 больше). Пусть наш стек использует для хранения элементов вектор. Какую из форм операции увеличения следует применить? Сначала мы используем текущее значение, потом увеличиваем его. Это постфиксная форма:
stack[ top++ ] = value;
Что делает операция pop? Уменьшает значение вершины (текущая вершина показывает на пустой элемент), затем извлекает значение. Это префиксная форма операции уменьшения:
int value = stack[ --top ];
(Реализация класса stack приведена в конце этой главы. Стандартный класс stack рассматривается в разделе 6.16.)
Упражнение 4.8
Как вы думаете, почему язык программирования получил название С++, а не ++С?
Операции присваивания
Инициализация задает начальное значение переменной. Например:
int ival = 1024;
int *pi = 0;
В результате операции присваивания объект получает новое значение, при этом старое пропадает:
ival = 2048;
pi = &iva1;
Иногда путают инициализацию и присваивание, так как они обозначаются одним и тем же знаком =. Объект инициализируется только один раз– при его определении. В то же время операция может быть применена к нему многократно.
Что происходит, если тип объекта не совпадает с типом значения, которое ему хотят присвоить? Допустим,
ival = 3.14159; // правильно?
В таком случае компилятор пытается трансформировать тип объекта, стоящего справа, в тип объекта, стоящего слева. Если такое преобразование возможно, компилятор неявно изменяет тип, причем при потере точности обычно выдается предупреждение. В нашем случае вещественное значение 3.14159 преобразуется в целое значение 3, и это значение присваивается переменной ival.
Если неявное приведение типов невозможно, компилятор сигнализирует об ошибке:
pi = ival; // ошибка
Неявная трансформация типа int в тип указатель на int невозможна. (Набор допустимых неявных преобразований типов мы обсудим в разделе 4.14.)
Левый операнд операции присваивания должен быть l-значением. Очевидный пример неправильного присваивания:
1024 = ival; // ошибка
Возможно, имелось в виду следующее:
int value = 1024;
value = ival; // правильно
Однако недостаточно потребовать, чтобы операнд слева от знака присваивания был l-значением. Так, после определений
const int array_size = 8;
int ia[ array_size ] = { 0, 1, 2, 2, 3, 5, 8, 13 };
int *pia = ia;
выражение
array_size = 512; // ошибка
ошибочно, хотя array_size и является l-значением: объявление array_size константой не дает возможности изменить его значение. Аналогично
ia = pia; // ошибка
ia – тоже l-значение, но оно не может быть значением массива.
Неверна и инструкция
pia + 2=1; // ошибка
Хотя pia+2 дает адрес ia[2], присвоить ему значение нельзя. Если мы хотим изменить элемент ia[2], то нужно воспользоваться операцией разыменования. Корректной будет следующая запись:
*(pia + 2) = 1; // правильно
Операция присваивания имеет результат – значение, которое было присвоено самому левому операнду. Например, результатом такой операции
ival = 0;
является 0, а результат
ival = 3.14159;
равен 3. Тип результата – int в обоих случаях. Это свойство операции присваивания можно использовать в подвыражениях. Например, следующий цикл
extern char next_char();
int main()
{
char ch = next_char();
while ( ch != '\n' ) {
// сделать что-то ...
ch = next_char();
}
// ...
}
может быть переписан так:
extern char next_char();
int main()
{
char ch;
while (( ch = next_char() ) != '\n' ) {
// сделать что-то ...
}
// ...
}
Заметим, что вокруг выражения присваивания необходимы скобки, поскольку приоритет этой операции ниже, чем операции сравнения. Без скобок первым выполняется сравнение:
next_char() != '\n'
и его результат, true или false, присваивается переменной ch. (Приоритеты операций будут рассмотрены в разделе 4.13.)
Аналогично несколько операций присваивания могут быть объединены, если это позволяют типы операндов. Например:
int main ()
{
int ival, jval;
ival = jval = 0; // правильно: присваивание 0 обеим переменным
// ...
}
Обеим переменным ival и jval присваивается значение 0. Следующий пример неправилен, потому что типы pval и ival различны, и неявное преобразование типов невозможно. Отметим, что 0 является допустимым значением для обеих переменных:
int main ()
{
int ival; int *pval;
ival = pval = 0; // ошибка: разные типы
// ...
}
Верен или нет приведенный ниже пример, мы сказать не можем, , поскольку определение jval в нем отсутствует:
int main()
{
// ...
int ival = jval = 0; // верно или нет?
// ...
}
Это правильно только в том случае, если переменная jval определена в программе ранее и имеет тип, приводимый к int. Обратите внимание: в этом случае мы присваиваем 0 значение jval и инициализируем ival. Для того чтобы инициализировать нулем обе переменные, мы должны написать:
int main()
{
// правильно: определение и инициализация
int ival = 0, jval = 0;
// ...
}
В практике программирования часты случаи, когда к объекту применяется некоторая операция, а результат этой операции присваивается тому же объекту. Например:
int arraySum( int ia[], int sz )
{
int sum = 0;
for ( int i = 0; i < sz; ++i )
sum = sum + ia[ i ];
return sum;
}
Для более компактной записи С и С++ предоставляют составные операции присваивания. С использованием такого оператора данный пример можно переписать следующим образом:
int arraySum( int ia[], int sz )
{
int sum = 0;
for ( int i =0; i < sz; ++i )
// эквивалентно: sum = sum + ia[ i ];
sum += ia[ i ];
return sum;
}
Общий синтаксис составного оператора присваивания таков:
a op= b;
где op= является одним из десяти операторов:
+= -= *= /= %=
<<= >>= &= ^= |=
Запись a op= b в точности эквивалентна записи a = a op b.
Упражнение 4.6
Найдите ошибку в данном примере. Исправьте запись.
int main() {
float fval;
int ival;
int *pi;
fval = ival = pi = 0;
}
Упражнение 4.7
Следующие выражения синтаксически правильны, однако скорее всего работают не так, как предполагал программист. Почему? Как их изменить?
(a) if ( ptr = retrieve_pointer() != 0 )
(b) if ( ival = 1024 )
(c) ival += ival + 1;
Операции с комплексными числами
Класс комплексных чисел стандартной библиотеки С++ представляет собой хороший пример использования объектной модели. Благодаря перегруженным арифметическим операциям объекты этого класса используются так, как будто они принадлежат одному из встроенных типов данных. Более того, в подобных операциях могут одновременно принимать участие и переменные встроенного арифметического типа, и комплексные числа. (Отметим, что здесь мы не рассматриваем общие вопросы математики комплексных чисел. См. [PERSON68] или любую книгу по математике.) Например, можно написать:
#inc1ude <complex>
comp1ex< double > a;
comp1ex< double > b;
// ...
complex< double > с = a * b + a / b;
Комплексные и арифметические типы разрешается смешивать в одном выражении:
complex< double > complex_obj = a + 3.14159;
Аналогично комплексные числа инициализируются арифметическим типом, и им может быть присвоено такое значение:
double dval = 3.14159;
complex_obj = dval;
Или
int ival = 3;
complex_obj = ival;
Однако обратное неверно. Например, следующее выражение вызовет ошибку компиляции:
// ошибка: нет неявного преобразования
// в арифметический тип
double dval = complex_obj;
Нужно явно указать, какую часть комплексного числа – вещественную или мнимую – мы хотим присвоить обычному числу. Класс комплексных чисел имеет две функции, возвращающих соответственно вещественную и мнимую части. Мы можем обращаться к ним, используя синтаксис доступа к членам класса:
double re = complex_obj.real();
double im = complex_obj.imag();
или эквивалентный синтаксис вызова функции:
double re = real(complex_obj);
double im = imag(complex_obj);
Класс комплексных чисел поддерживает четыре составных оператора присваивания: +=, -=, *= и /=. Таким образом,
complex_obj += second_complex_obj;
Поддерживается и ввод/вывод комплексных чисел. Оператор вывода печатает вещественную и мнимую части через запятую, в круглых скобках. Например, результат выполнения операторов вывода
complex< double > complex0( 3.14159, -2.171 );
comp1ex< double > complex1( complexO.real() );
cout << complexO << " " << complex1 << endl;
выглядит так:
( 3.14159, -2.171 ) ( 3.14159, 0.0 )
Оператор ввода понимает любой из следующих форматов:
// допустимые форматы для ввода комплексного числа
// 3.14159 ==> comp1ex( 3.14159 );
// ( 3.14159 ) ==> comp1ex( 3.14159 );
// ( 3.14, -1.0 ) ==> comp1ex( 3.14, -1.0 );
// может быть считано как
// cin >> a >> b >> с
// где a, b, с - комплексные числа
3.14159 ( 3.14159 ) ( 3.14, -1.0 )
Кроме этих операций, класс комплексных чисел имеет следующие функции-члены: sqrt(), abs(), polar(), sin(), cos(), tan(), exp(), log(), log10() и pow().
Упражнение 4.9
Реализация стандартной библиотеки С++, доступная нам в момент написания книги, не поддерживает составных операций присваивания, если правый операнд не является комплексным числом. Например, подобная запись недопустима:
complex_obj += 1;
(Хотя согласно стандарту С++ такое выражение должно быть корректно, производители часто не успевают за стандартом.) Мы можем определить свой собственный оператор для реализации такой операции. Вот вариант функции, реализующий оператор сложения для complex<double>:
#include <complex>
inline complex<double>&
operator+=( complex<double> &cval, double dval )
{
return cval += complex<double>( dval );
}
(Это пример перегрузки оператора для определенного типа данных, детально рассмотренной в главе 15.)
Используя этот пример, реализуйте три других составных оператора присваивания для типа complex<double>. Добавьте свою реализацию к программе, приведенной ниже, и запустите ее для проверки.
#include <iostream>
#include <complex>
// определения операций...
int main() {
complex< double > cval ( 4.0, 1.0 );
cout << cval << endl;
cval += 1;
cout << cval << endl;
cval -= 1;
cout << cval << endl;
cval *= 2;
cout << cval << endl;
cout /= 2;
cout << cval << endl;
}
Упражнение 4.10
Стандарт С++ не специфицирует реализацию операций инкремента и декремента для комплексного числа. Однако их семантика вполне понятна: если уж мы можем написать:
cval += 1;
что означает увеличение на 1 вещественной части cval, то и операция инкремента выглядела бы вполне законно. Реализуйте эти операции для типа complex<double> и выполните следующую программу:
#include <iostream>
#include <complex>
// определения операций...
int main() {
complex< double > cval( 4.0, 1.0 );
cout << cval << endl;
++cva1;
cout << cval << endl;
}
Операции с последовательными контейнерами
Функция-член push_back() позволяет добавить единственный элемент в конец контейнера. Но как вставить элемент в произвольную позицию? А целую последовательность элементов? Для этих случаев существуют более общие операции.
Например, для вставки элемента в начало контейнера можно использовать:
vector< string > svec;
list< string > slist;
string spouse( "Beth" );
slist.insert( slist.begin(), spouse );
svec.insert( svec.begin(), spouse );
Первый параметр функции-члена insert() (итератор, адресующий некоторый элемент контейнера) задает позицию, а второй – вставляемое перед этой позицией значение. В примере выше элемент добавляется в начало контейнера. А так можно реализовать вставку в произвольную позицию:
string son( "Danny" );
list<string>::iterator iter;
iter = find( slist.begin(), slist.end(), son );
slist.insert( iter, spouse );
Здесь find() возвращает позицию элемента в контейнере, если элемент найден, либо итератор end(), если ничего не найдено. (Мы вернемся к функции find() в конце следующего раздела.) Как можно догадаться, push_back() эквивалентен следующей записи:
// эквивалентный вызов: slist.push_back( value );
slist.insert( slist.end(), value );
Вторая форма функции-члена insert() позволяет вставить указанное количество одинаковых элементов, начиная с определенной позиции. Например, если мы хотим добавить десять элементов Anna в начало вектора, то должны написать:
vector<string> svec;
string anna( "Anna" );
svec.insert( svec.begin(), 10, anna );
insert() имеет и третью форму, помогающую вставить в контейнер несколько элементов. Допустим, имеется следующий массив:
string sarray[4] = { "quasi", "simba", "frollo", "scar" };
Мы можем добавить все его элементы или только некоторый диапазон в наш вектор строк:
svec.insert( svec.begin(), sarray, sarray+4 );
svec.insert( svec.begin() + svec.size()/2,
sarray+2, sarray+4 );
Такой диапазон отмечается и с помощью пары итераторов
// вставляем элементы svec
// в середину svec_two
svec_two.insert( svec_two.begin() + svec_two.size()/2,
svec.begin(), svec.end() );
или любого контейнера, содержащего строки:[14]
list< string > slist;
// ...
// вставляем элементы svec
// перед элементом, содержащим stringVal
list< string >::iterator iter =
find( slist.begin(), slist.end(), stringVal );
slist.insert( iter, svec.begin(), svec.end() );
Операции сравнения и логические операции
Таблица 4.2. Операции сравнения и логические операции
Символ операции | Значение | Использование | |||
! | Логическое НЕ | !expr | |||
< | Меньше | expr1 < expr2 | |||
<= | Меньше или равно | expr1 <= expr2 | |||
> | Больше | expr1 > expr2 | |||
>= | Больше или равно | expr1 >= expr2 | |||
== | Равно | expr1 == expr2 | |||
!= | Не равно | expr1 != expr2 | |||
&& | Логическое И | expr1 && expr2 | |||
|| | Логическое ИЛИ | expr1 || expr2 | |||
Примечание. Все операции в результате дают значение типа bool |
Операции сравнения и логические операции в результате дают значение типа bool, то есть true или false. Если же такое выражение встречается в контексте, требующем целого значения, true преобразуется в 1, а false – в 0. Вот фрагмент кода, подсчитывающего количество элементов вектора, меньших некоторого заданного значения:
vector<int>::iterator iter = ivec.beg-in() ;
while ( iter != ivec.end() ) {
// эквивалентно: e1em_cnt = e1em_cnt + (*iter < some_va1ue)
// значение true/false выражения *iter < some_va1ue
// превращается в 1 или 0
e1em_cnt += *iter < some_va1ue;
++iter;
}
Мы просто прибавляем результат операции “меньше” к счетчику. (Пара += обозначает составной оператор присваивания, который складывает операнд, стоящий слева, и операнд, стоящий справа. То же самое можно записать более компактно: elem_count = elem_count + n. Мы рассмотрим такие операторы в разделе 4.4.)
Логическое И (&&) возвращает истину только тогда, когда истинны оба операнда. Логическое ИЛИ (||) дает истину, если истинен хотя бы один из операндов. Гарантируется, что операнды вычисляются слева направо и вычисление заканчивается, как только результирующее значение становится известно. Что это значит? Пусть даны два выражения:
expr1 && expr2
expr1 || expr2
Если в первом из них expr1 равно false, значение всего выражения тоже будет равным false вне зависимости от значения expr2, которое даже не будет вычисляться. Во втором выражении expr2 не оценивается, если expr1 равно true, поскольку значение всего выражения равно true вне зависимости от expr2.
Подобный способ вычисления дает возможность удобной проверки нескольких выражений в одном операторе AND:
while ( ptr != О &&
ptr->va1ue < upperBound &&
ptr->va1ue >= 0 &&
notFound( ia[ ptr->va1ue ] ))
{ ... }
Указатель с нулевым значением не указывает ни на какой объект, поэтому применение к нулевому указателю операции доступа к члену вызвало бы ошибку (ptr->value). Однако, если ptr равен 0, проверка на первом шаге прекращает дальнейшее вычисление подвыражений. Аналогично на втором и третьем шагах проверяется попадание величины ptr->value в нужный диапазон, и операция взятия индекса не применяется к массиву ia, если этот индекс неправилен.
Операция логического НЕ дает true, если ее единственный оператор равен false, и наоборот. Например:
bool found = false;
// пока элемент не найден
// и ptr указывает на объект (не 0)
while ( ! found && ptr ) {
found = 1ookup( *ptr );
++ptr;
}
Подвыражение
! found
дает true, если переменная found равна false. Это более компактная запись для
found == false
Аналогично
if ( found )
эквивалентно более длинной записи
if ( found == true )
Использование операций сравнения достаточно очевидно. Нужно только иметь в виду, что, в отличие от И и ИЛИ, порядок вычисления операндов таких выражений не определен. Вот пример, где возможна подобная ошибка:
// Внимание! Порядок вычислений не определен!
if ( ia[ index++ ] < ia[ index ] )
// поменять местами элементы
Программист предполагал, что левый операнд оценивается первым и сравниваться будут элементы ia[0] и ia[1]. Однако компилятор не гарантирует вычислений слева направо, и в таком случае элемент ia[0] может быть сравнен сам с собой. Гораздо лучше написать более понятный и машинно-независимый код:
if ( ia[ index ] < ia[ index+1 ] )
// поменять местами элементы
++index;
Еще один пример возможной ошибки. Мы хотели убедиться, что все три величины ival, jval и kval различаются. Где мы промахнулись?
// Внимание! это не сравнение 3 переменных друг с другом
if ( ival != jva1 != kva1 )
// do something ...
Значения 0, 1 и 0 дают в результате вычисления такого выражения true. Почему? Сначала проверяется ival != jval, а потом итог этой проверки (true/false – преобразованной к 1/0) сравнивается с kval. Мы должны были явно написать:
if ( ival != jva1 && ival != kva1 && jva1 != kva1 )
// сделать что-то ...
Упражнение 4.4
Найдите неправильные или непереносимые выражения, поясните. Как их можно изменить? (Заметим, что типы объектов не играют роли в данных примерах.)
(a) ptr->iva1 != 0
(с) ptr != 0 && *ptr++
(e) vec[ iva1++ ] <= vec[ ival ];
(b) ival != jva1 < kva1 (d) iva1++ && ival
Упражнение 4.5
Язык С++ не диктует порядок вычисления операций сравнения для того, чтобы позволить компилятору делать это оптимальным образом. Как вы думаете, стоило бы в данном случае пожертвовать эффективностью, чтобы избежать ошибок, связанных с предположением о вычислении выражения слева направо?
Операция list_merge()
void list::merge( list rhs );
template <class Compare>
void list::merge( list rhs, Compare comp );
Элементы двух упорядоченных списков объединяются либо на основе оператора “меньше”, определенного для типа элементов в контейнере, либо на основе указанной пользователем операции сравнения. (Заметьте, что элементы списка rhs перемещаются в список, для которого вызвана функция-член merge(); по завершении операции список rhs будет пуст.) Например:
int array1[ 10 ] = { 34, 0, 8, 3, 1, 13, 2, 5, 21, 1 };
int array2[ 5 ] = { 377, 89, 233, 55, 144 };
list< int > ilist1( array1, array1 + 10 );
list< int > ilist2( array2, array2 + 5 );
// для объединения требуется, чтобы оба списка были упорядочены
ilist1.sort(); ilist2.sort();
ilist1.merge( ilist2 );
После выполнения операции merge() список ilist2 пуст, а ilist1 содержит первые 15 чисел Фибоначчи в порядке возрастания.
Операция list::remove()
void list::remove( const elemType &value );
Операция remove() удаляет все элементы с заданным значением:
ilist1.remove( 1 );
Операция list::remove_if()
template < class Predicate >
void list::remove_if( Predicate pred );
Операция remove_if() удаляет все элементы, для которых выполняется указанное условие, т.е. предикат pred возвращает true. Например:
class Even {
public:
bool operator()( int elem ) { return ! (elem % 2 ); }
};
ilist1.remove_if( Even() );
удаляет все четные числа из списка, определенного при рассмотрении merge().
Операция list::reverse()
void list::reverse();
Операция reverse() изменяет порядок следования элементов списка на противоположный:
ilist1.reverse();
Операция list::sort()
void list::sort();
template <class Compare>
void list::sort( Compare comp );
По умолчанию sort() упорядочивает элементы списка по возрастанию с помощью оператора “меньше”, определенного в классе элементов контейнера. Вместо этого можно явно передать в качестве аргумента оператор сравнения. Так,
list1.sort();
упорядочивает list1 по возрастанию, а
list1.sort( greater<int>() );
упорядочивает list1 по убыванию, используя оператор “больше”.
Операция list::splice()
void list::splice( iterator pos, list rhs );
void list::splice( iterator pos, list rhs, iterator ix );
void list::splice( iterator pos, list rhs,
iterator first, iterator last );
Операция splice() имеет три формы: перемещение одного элемента, всех элементов или диапазона из одного списка в другой. В каждом случае передается итератор, указывающий на позицию вставки, а перемещаемые элементы располагаются непосредственно перед ней. Если даны два списка:
int array[ 10 ] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 };
list< int > ilist1( array, array + 10 );
list< int > ilist2( array, array + 2 ); // содержит 0, 1
то следующее обращение к splice() перемещает первый элемент ilist1 в ilist2. Теперь ilist2 содержит элементы 0, 1 и 0, тогда как в ilist1 элемента 0 больше нет.
// ilist2.end() указывает на позицию, куда нужно переместить элемент
// элементы вставляются перед этой позицией
// ilist1 указывает на список, из которого перемещается элемент
// ilist1.begin() указывает на сам перемещаемый элемент
ilis2.splice( ilist2.end(), ilist1, ilist1.begin() );
В следующем примере применения splice() передаются два итератора, ограничивающие диапазон перемещаемых элементов:
list< int >::iterator first, last;
first = ilist1.find( 2 );
last = ilist1.find( 13 );
ilist2.splice( ilist2.begin(), ilist1, first, last );
В данном случае элементы 2, 3, 5 и 8 удаляются из ilist1 и вставляются в начало ilist2. Теперь ilist1 содержит пять элементов 1, 1, 13, 21 и 34. Для их перемещения в ilist2 можно воспользоваться третьей вариацией операции splice():
list< int >::iterator pos = ilist2.find( 5 );
ilist2.splice( pos, ilist1 );
Итак, список ilist1 пуст. Последние пять элементов перемещены в позицию списка ilist2, предшествующую той, которую занимает элемент 5.
Операция list::unique()
void list::unique();
template <class BinaryPredicate>
void list::unique( BinaryPredicate pred );
Операция unique() удаляет соседние дубликаты. По умолчанию при сравнении используется оператор равенства, определенный для типа элементов контейнера. Например, если даны значения {0,2,4,6,4,2,0}, то после применения unique() список останется таким же, поскольку в соседних позициях дубликатов нет. Но если мы сначала отсортируем список, что даст {0,0,2,2,4,4,6}, а потом применим unique(), то получим четыре различных значения {0,2,4,6}.
ilist.unique();
Вторая форма unique() принимает альтернативный оператор сравнения. Например,
class EvenPair {
public:
bool operator()( int val1, val2 )
{ return ! (val2 % val1 ); }
};
ilist.unique( EvenPair() );
удаляет соседние элементы, если второй элемент без остатка делится на первый.
Эти операции, являющиеся членами класса, следует предпочесть соответствующим обобщенным алгоритмам при работе со списками. Остальные обобщенные алгоритмы, такие, как find(), transform(), for_each() и т.д., работают со списками так же эффективно, как и с другими контейнерами (еще раз напомним, что подробно все алгоритмы рассматриваются в Приложении).
Упражнение 12.8
Измените программу из раздела 12.2, используя список вместо вектора.
Часть IV
Объектное программирование
В части 4 мы сосредоточимся на объектном программировании, т.е. на применении классов C++ для определения новых типов, манипулировать которыми так же просто, как и встроенными. Создавая новые типы для описания предметной области, C++ помогает программисту писать более легкие для понимания приложения. Классы позволяют отделить детали, касающиеся реализации нового типа, от определения интерфейса и операций, предоставляемых пользователю. При этом уделяется меньше внимания мелочам, из-за чего программирование становится таким утомительным занятием. Значимые для приложения типы можно реализовать всего один раз, после чего использовать повторно. Средства, обеспечивающие инкапсуляцию данных и функций, необходимых для реализации типа, помогают значительно упростить последующее сопровождение и развитие приложения.
В главе 13 мы рассмотрим общий механизм классов: порядок их определения, концепцию сокрытия информации (т.е. отделение открытого интерфейса от закрытой реализации), способы определения и манипулирования объектами класса, область видимости, вложенные классы и классы как члены пространства имен.
В главе 14 изучаются предоставляемые C++ средства инициализации и уничтожения объектов класса, а также присваивания им значений путем применения таких специальных функций-членов класса, как конструкторы, деструкторы и копирующие конструкторы. Мы рассмотрим вопрос о почленной инициализации и копировании, когда объект класса инициализируется или ему присваивается значение другого объекта того же класса.
В главе 15 мы расскажем о перегрузке операторов, которая позволяет использовать операнды типа класса со встроенными операторами, описанными в главе 4. Таким образом, работа с объектами типа класса может быть сделана столь же понятной, как и работа со встроенными типами. В начале главы 15 представлены общие концепции и соображения, касающиеся проектирования перегрузки операторов, а затем рассмотрены конкретные операторы, такие, как присваивание, взятие индекса, вызов, а также специфичные для классов операторы new и delete. Иногда необходимо объявить перегруженный оператор, как друга класса, наделив его специальными правами доступа, в данной главе объясняется, зачем это нужно. Здесь же представлен еще один специальный вид функций-членов – конвертеры, которые позволяют программисту определить стандартные преобразования. Конвертеры неявно применяются компилятором, когда объекты класса используются в качестве фактических аргументов функции или операндов встроенного либо перегруженного оператора. Завершается глава изложением правил разрешения перегрузки функций с учетом аргументов типа класса, функций-членов и перегруженных операторов.
Тема главы 16 – шаблоны классов. Шаблон – это предписание для создания класса, в котором один или несколько типов параметризованы. Например, vector может быть параметризован типом элементов, хранящихся в нем, а buffer – типом элементов в буфере или его размером. В этой главе объясняется, как определить и конкретизировать шаблон. Поддержка классов в C++ теперь рассматривается иначе – в свете наличия шаблонов, и снова обсуждаются функции-члены, объявления друзей и вложенные типы. Здесь мы еще раз вернемся к модели компиляции шаблонов, описанной в главе 10, чтобы показать, какое влияние оказывают на нее шаблоны классов.
Оператор =
Присваивание одного объекта другому объекту того же класса выполняется с помощью копирующего оператора присваивания. (Этот специальный случай был рассмотрен в разделе 14.7.)
Для класса могут быть определены и другие операторы присваивания. Если объектам класса надо присваивать значения типа, отличного от этого класса, то разрешается определить такие операторы, принимающие подобные параметры. Например, чтобы поддержать присваивание C-строки объекту String:
String car ("Volks");
car = "Studebaker";
мы предоставляем оператор, принимающий параметр типа const char*. Эта операция уже была объявлена в нашем классе:
class String {
public:
// оператор присваивания для char*
String& operator=( const char * );
// ...
private:
int _size;
char *string;
};
Такой оператор реализуется следующим образом. Если объекту String присваивается нулевой указатель, он становится “пустым”. В противном случае ему присваивается копия C-строки:
String& String::operator=( const char *sobj )
{
// sobj - нулевой указатель
if (! sobj ) {
_size = 0;
delete[] _string;
_string = 0;
}
else {
_size = strlen( sobj );
delete[] _string;
_string = new char[ _size + 1 ];
strcpy( _string, sobj );
}
return *this;
}
_string ссылается на копию той C-строки, на которую указывает sobj. Почему на копию? Потому что непосредственно присвоить sobj члену _string нельзя:
_string = sobj; // ошибка: несоответствие типов
sobj – это указатель на const и, следовательно, не может быть присвоен указателю на “не-const” (см. раздел 3.5). Изменим определение оператора присваивания:
String& String::operator=( const *sobj ) { // ... }
Теперь _string прямо ссылается на C-строку, адресованную sobj. Однако при этом возникают другие проблемы. Напомним, что C-строка имеет тип const char*. Определение параметра как указателя на не-const делает присваивание невозможным:
car = "Studebaker"; // недопустимо с помощью operator=( char *) !
Итак, выбора нет. Чтобы присвоить C- строку объекту типа String, параметр должен иметь тип const char*.
Хранение в _string прямой ссылки на C-строку, адресуемую sobj, порождает и иные сложности. Мы не знаем, на что именно указывает sobj. Это может быть массив символов, который модифицируется способом, неизвестным объекту String. Например:
char ia[] = { 'd', 'a', 'n', 'c', 'e', 'r' };
String trap = ia; // trap._string ссылается на ia
ia[3] = 'g'; // а вот это нам не нужно:
// модифицируется и ia, и trap._string
Если trap._string напрямую ссылался на ia, то объект trap демонстрировал бы своеобразное поведение: его значение может изменяться без вызова функций-членов класса String. Поэтому мы полагаем, что выделение области памяти для хранения копии значения C-строки менее опасно.
Обратите внимание, что в операторе присваивания используется delete. Член _string содержит ссылку на массив символов, расположенный в хипе. Чтобы предотвратить утечку, память, выделенная под старую строку, освобождается с помощью delete до выделения памяти под новую. Поскольку _string адресует массив символов, следует использовать версию delete для массивов (см. раздел 8.4).
И последнее замечание об операторе присваивания. Тип возвращаемого им значения – это ссылка на класс String. Почему именно ссылка? Дело в том, что для встроенных типов операторы присваивания можно сцеплять:
// сцепление операторов присваивания
int iobj, jobj;
iobj = jobj = 63;
Они ассоциируются справа налево, т.е. в предыдущем примере присваивания выполняются так:
iobj = (jobj = 63);
Это удобно и при работе с объектами класса String: поддерживается, к примеру, следующая конструкция:
String ver, noun;
verb = noun = "count";
При первом присваивании из этой цепочки вызывается определенный ранее оператор для const char*. Тип полученного результата должен быть таким, чтобы его можно было использовать как аргумент для копирующего оператора присваивания класса String. Поэтому, хотя параметр данного оператора имеет тип const char *, возвращается все же ссылка на String.
Операторы присваивания бывают перегруженными. Например, в нашем классе String есть такой набор:
// набор перегруженных операторов присваивания
String& operator=( const String & );
String& operator=( const char * );
Отдельный оператор присваивания может существовать для каждого типа, который разрешено присваивать объекту String. Однако все такие операторы должны быть определены как функции-члены класса.
Оператор dynamic_cast
Оператор dynamic_cast можно применять для преобразования указателя, ссылающегося на объект типа класса в указатель на тип класса из той же иерархии. Его также используют для трансформации l-значения объекта типа класса в ссылку на тип класса из той же иерархии. Приведение типов с помощью оператора dynamic_cast, в отличие от других имеющихся в C++ способов, осуществляется во время выполнения программы. Если указатель или l-значение не могут быть преобразованы в целевой тип, то dynamic_cast завершается неудачно. В случае приведения типа указателя признаком неудачи служит возврат нулевого значения. Если же l-значение нельзя трансформировать в ссылочный тип, возбуждается исключение. Ниже мы приведем примеры неудачного выполнения этого оператора.
Прежде чем перейти к более детальному рассмотрению dynamic_cast, посмотрим, зачем его нужно применять. Предположим, что в программе используется библиотека классов для представления различных категорий служащих компании. Входящие в иерархию классы поддерживают функции-члены для вычисления зарплаты:
class employee {
public:
virtual int salary();
};
class manager : public employee {
public:
int salary();
};
class programmer : public employee {
public:
int salary();
};
void company::payroll( employee *pe ) {
// используется pe->salary()
}
В компании есть разные категории служащих. Параметром функции-члена payroll() класса company является указатель на объект employee, который может адресовать один из типов manager или programmer. Поскольку payroll() обращается к виртуальной функции-члену salary(), то вызывается подходящая замещающая функция, определенная в классе manager или programmer, в зависимости от того, какой объект адресован указателем.
Допустим, класс employee перестал удовлетворять нашим потребностям, и мы хотим его модифицировать, добавив еще одну функцию-член bonus(), используемую совместно с salary() при расчете платежной ведомости. Для этого нужно включить новую функцию-член в классы, составляющие иерархию employee:
class employee {
public:
virtual int salary(); // зарплата
virtual int bonus(); // премия
};
class manager : public employee {
public:
int salary();
};
class programmer : public employee {
public:
int salary();
int bonus();
};
void company::payroll( employee *pe ) {
// используется pe->salary() и pe->bonus()
}
Если параметр pe функции payroll() указывает на объект типа manager, то вызывается виртуальная функция-член bonus() из базового класса employee, поскольку в классе manager она не замещена. Если же pe указывает на объект типа programmer, то вызывается виртуальная функция-член bonus() из класса programmer.
После добавления новых виртуальных функций в иерархию классов придется перекомпилировать все функции-члены. Добавить bonus() можно, если у нас есть доступ к исходным текстам функций-членов в классах employee, manager и programmer. Однако если иерархия была получена от независимого поставщика, то не исключено, что в нашем распоряжении имеются только заголовочные файлы, описывающие интерфейс библиотечных классов и объектные файлы с их реализацией, а исходные тексты функций-членов недоступны. В таком случае перекомпиляция всей иерархии невозможна.
Если мы хотим расширить функциональность библиотеки классов, не добавляя новые виртуальные функции-члены, можно воспользоваться оператором dynamic_cast.
Этот оператор применяется для получения указателя на производный класс, чтобы иметь возможность работать с теми его элементами, которые по-другому не доступны. Предположим, что мы расширяем библиотеку за счет добавления новой функции-члена bonus() в класс programmer. Ее объявление можно включить в определение programmer, находящееся в заголовочном файле, а саму функцию определить в одном из своих исходных файлов:
class employee {
public:
virtual int salary();
};
class manager : public employee {
public:
int salary();
};
class programmer : public employee {
public:
int salary();
int bonus();
};
Напомним, что payroll() принимает в качестве параметра указатель на базовый класс employee. Мы можем применить оператор dynamic_cast для получения указателя на производный programmer и воспользоваться им для вызова функции-члена bonus():
void company::payroll( employee *pe )
{
programmer *pm = dynamic_cast< programmer* >( pe );
// если pe указывает на объект типа programmer,
// то dynamic_cast выполнится успешно и pm будет
// указывать на начало объекта programmer
if ( pm ) {
// использовать pm для вызова programmer::bonus()
}
// если pe не указывает на объект типа programmer,
// то dynamic_cast выполнится неудачно
// и pm будет содержать 0
else {
// использовать функции-члены класса employee
}
}
Оператор
dynamic_cast< programmer* >( pe )
приводит свой операнд pe к типу programmer*. Преобразование будет успешным, если pe ссылается на объект типа programmer, и неудачным в противном случае: тогда результатом dynamic_cast будет 0.
Таким образом, оператор dynamic_cast осуществляет сразу две операции. Он проверяет, выполнимо ли запрошенное приведение, и если это так, выполняет его. Проверка производится во время работы программы. dynamic_cast безопаснее, чем другие операции приведения типов в C++, поскольку проверяет возможность корректного преобразования.
Если в предыдущем примере pe действительно указывает на объект типа programmer, то операция dynamic_cast завершится успешно и pm будет инициализирован указателем на объект типа programmer. В противном случае pm получит значение 0. Проверив значение pm, функция company::payroll() может узнать, указывает ли pm на объект programmer. Если это так, то она вызывает функцию-член programmer::bonus() для вычисления премии программисту. Если же dynamic_cast завершается неудачно, то pe указывает на объект типа manager, а значит, необходимо применить более общий алгоритм расчета, не использующий новую функцию-член programmer::bonus().
Оператор dynamic_cast употребляется для безопасного приведения указателя на базовый класс к указателю на производный. Такую операцию часто называют понижающим приведением
(downcasting). Она применяется, когда необходимо воспользоваться особенностями производного класса, отсутствующими в базовом. Манипулирование объектами производного класса с помощью указателей на базовый обычно происходит автоматически, с помощью виртуальных функций. Однако иногда использовать виртуальные функции невозможно. В таких ситуациях dynamic_cast предлагает альтернативное решение, хотя этот механизм в большей степени подвержен ошибкам, чем виртуализация, и должен применяться с осторожностью.
Одна из возможных ошибок – это работа с результатом dynamic_cast без предварительной проверки на 0: нулевой указатель нельзя использовать для адресации объекта класса. Например:
void company::payroll( employee *pe )
{
programmer *pm = dynamic_cast< programmer* >( pe );
// потенциальная ошибка: pm используется без проверки значения
static int variablePay = 0;
variablePay += pm->bonus();
// ...
}
Результат, возвращенный dynamic_cast, всегда следует проверять, прежде чем использовать в качестве указателя. Более правильное определение функции company::payroll() могло бы выглядеть так:
void company::payroll( employee *pe )
{
// выполнить dynamic_cast и проверить результат
if ( programmer *pm = dynamic_cast< programmer* >( pe ) ) {
// использовать pm для вызова programmer::bonus()
}
else {
// использовать функции-члены класса employee
}
}
Результат операции dynamic_cast используется для инициализации переменной pm внутри условного выражения в инструкции if. Это возможно, так как объявления в условиях возвращают значения. Ветвь, соответствующая истинности условия, выполняется, если pm не равно нулю: мы знаем, что операция dynamic_cast завершилась успешно и pe указывает на объект programmer. В противном случае результатом объявления будет 0 и выполняется ветвь else. Поскольку теперь оператор и проверка его результата находятся в одной инструкции программы, то невозможно случайно вставить какой-либо код между выполнением dynamic_cast и проверкой, так что pm будет использоваться только тогда, когда содержит правильный указатель.
В предыдущем примере операция dynamic_cast преобразует указатель на базовый класс в указатель на производный. Ее также можно применять для трансформации l-значения типа базового класса в ссылку на тип производного. Синтаксис такого использования dynamic_cast следующий:
dynamic_cast< Type & >( lval )
где Type& – это целевой тип преобразования, а lval – l-значение типа базового класса. Операнд lval успешно приводится к типу Type& только в том случае, когда lval действительно относится к объекту класса, для которого один из производных имеет тип Type.
Поскольку нулевых ссылок не бывает (см. раздел 3.6), то проверить успешность выполнения операции путем сравнения результата (т.е. возвращенной оператором dynamic_cast ссылки) с нулем невозможно. Если вместо указателей используются ссылки, условие
if ( programmer *pm = dynamic_cast< programmer* >( pe ) )
нельзя переписать в виде
if ( programmer &pm = dynamic_cast< programmer& >( pe ) )
Для извещения об ошибке в случае приведения к ссылочному типу оператор dynamic_cast возбуждает исключение. Следовательно, предыдущий пример можно записать так:
#include <typeinfo>
void company::payroll( employee &re )
{
try {
programmer &rm = dynamic_cast< programmer & >( re );
// использовать rm для вызова programmer::bonus()
}
catch ( std::bad_cast ) {
// использовать функции-члены класса employee
}
}
В случае неудачного завершения ссылочного варианта dynamic_cast возбуждается исключение типа bad_cast. Класс bad_cast определен в стандартной библиотеке; для ссылки на него необходимо включить в программу заголовочный файл <typeinfo>. (Исключения из стандартной библиотеки мы будем рассматривать в следующем разделе.)
Когда следует употреблять ссылочный вариант dynamic_cast вместо указательного? Это зависит только от желания программиста. При его использовании игнорировать ошибку приведения типа и работать с результатом без проверки (как в указательном варианте) невозможно; с другой стороны, применение исключений увеличивает накладные расходы во время выполнения программы (см. главу 11).
Оператор размещения new *
Существует третья форма оператора new, которая создает объект без отведения для него памяти, то есть в памяти, которая уже была выделена. Эту форму называют оператором размещения new. Программист указывает адрес области памяти, в которой размещается объект:
new (place_address) type-specifier
place_address должен быть указателем. Такая форма (она включается заголовочным файлом <new>) позволяет программисту предварительно выделить большую область памяти, которая впоследствии будет содержать различные объекты. Например:
#include <iostream>
#include <new>
const int chunk = 16;
class Foo {
public:
int val() { return _val; }
FooQ(){ _val = 0; }
private:
int _val;
};
// выделяем память, но не создаем объектов Foo
char *buf = new char[ sizeof(Foo) * chunk ];
int main() {
// создаем объект Foo в buf
Foo *pb = new (buf) Foo;
// проверим, что объект помещен в buf
if ( pb.val() == 0 )
cout << "Оператор new сработал!" << endl;
// здесь нельзя использовать pb
delete[] buf;
return 0;
}
Результат работы программы:
Оператор new сработал!
Для оператора размещения new нет парного оператора delete: он не нужен, поскольку эта форма не выделяет память. В предыдущем примере необходимо освободить память, адресуемую указателем buf, а не pb. Это происходит в конце программы, когда буфер больше не нужен. Поскольку buf ссылается на символьный массив, оператор delete имеет форму
delete[] buf;
При уничтожении buf прекращают существование все объекты, созданные в нем. В нашем примере pb больше не ссылается на существующий объект класса Foo.
Упражнение 8.5
Объясните, почему приведенные операторы new ошибочны:
(a) const float *pf = new const float[100];
(b) double *pd = new doub1e[10] [getDim()];
(c) int (*pia2)[ 1024 ] = new int[ ][ 1024 ];
(d) const int *pci = new const int;
Упражнение 8.6
Как бы вы уничтожили pa?
typedef int arr[10];
int *pa = new arr;
Упражнение 8.7
Какие из следующих операторов delete содержат потенциальные ошибки времени выполнения и почему:
int globalObj;
char buf[1000];
void f() {
int *pi = &global0bj;
double *pd = 0;
float *pf = new float(O);
int *pa = new(buf)int[20];
delete pi; // (a)
delete pd; // (b)
delete pf; // (c)
de1ete[] pa; // (d)
}
Упражнение 8.8
Какие из данных объявлений auto_ptr неверны или грозят ошибками времени выполнения? Объясните каждый случай.
int ix = 1024;
int *pi = & ix;
int *pi2 = new int ( 2048 );
(a) auto_ptr<int> p0(ix);
(b) auto_ptr<int> pl(pi);
(c) auto_ptr<int> p2(pi2);
(d) auto_ptr<int> p3(&ix);
(e) auto_ptr<int> p4(new int(2048));
(f) auto_ptr<int> p5(p2.get());
(9) auto_ptr<int> p6(p2.release());
(h) auto_ptr<int> p7(p2);
Упражнение 8.9
Объясните разницу между следующими инструкциями:
int *pi0 = p2.get();
int *pi1 = p2.release() ;
Для каких случаев более приемлем тот или иной вызов?
Упражнение 8.10
Пусть мы имеем:
auto_ptr< string > ps( new string( "Daniel" ) );
В чем разница между этими двумя вызовами assign()?Какой их них предпочтительнее и почему?
ps.get()->assign( "Danny" );
ps->assign( "Danny" );
Оператор размещения new() и оператор delete()
Оператор-член new() может быть перегружен при условии, что все объявления имеют разные списки параметров. Первый параметр должен иметь тип size_t:
class Screen {
public:
void *operator new( size_t );
void *operator new( size_t, Screen * );
// ...
};
Остальные параметры инициализируются аргументами размещения, заданными при вызове new:
void func( Screen *start ) {
Screen *ps = new (start) Screen;
// ...
}
Та часть выражения, которая находится после ключевого слова 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 operator delete( void*, Screen* );
Если такой оператор будет найден, то он вызывается для освобождения памяти в случае, когда new() возбуждает исключение. (Иначе – не вызывается.)
Разработчик класса принимает решение, предоставлять ли delete(), соответствующий некоторому new(), в зависимости от того, выделяет ли этот оператор new() память самостоятельно или пользуется уже выделенной. В первом случае delete() необходимо включить для освобождения памяти, если конструктор возбудит исключение; иначе в нем нет необходимости.
Можно также перегрузить оператор размещения new[]() и оператор delete[]() для массивов:
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* );
// ...
};
Оператор new[]() используется в случае, когда в выражении, содержащем new для распределения массива, заданы соответствующие аргументы размещения:
void func( Screen *start ) {
// вызывается Screen::operator new[]( size_t, Screen* )
Screen *ps = new (start) Screen[10];
// ...
}
Если при работе оператора new конструктор возбуждает исключение, то автоматически вызывается соответствующий delete[]().
Упражнение 15.9
Объясните, какие из приведенных инициализаций ошибочны:
class iStack {
public:
iStack( int capacity )
: _stack( capacity ), _top( 0 ) {}
// ...
private:
int _top;
vatcor< int > _stack;
};
(a) iStack *ps = new iStack(20);
(b) iStack *ps2 = new const iStack(15);
(c) iStack *ps3 = new iStack[ 100 ];
Упражнение 15.10
Что происходит в следующих выражениях, содержащих new и delete?
class Exercise {
public:
Exercise();
~Exercise();
};
Exercise *pe = new Exercise[20];
delete[] ps;
Измените эти выражения так, чтобы вызывались глобальные операторы new() и delete().
Упражнение 15.11
Объясните, зачем разработчик класса должен предоставлять оператор delete().
Оператор разрешения области видимости
Имя члена пользовательского пространства дополняется поставленным спереди именем этого пространства и оператором разрешения области видимости (::). Использование неквалифицированного члена, например matrix, является ошибкой. Компилятор не знает, к какому объявлению относится это имя:
// определение интерфейса библиотеки
#include "primer.h"
// ошибка: нет объявления для matrix
void func( matrix &m );
Объявление члена пространства имен скрыто в своем пространстве. Если мы не укажем компилятору, где именно искать объявление, он произведет поиск только в текущей области видимости и в областях, включающих текущую. Допустим, если переписать предыдущую программу так:
// определение интерфейса библиотеки
#include "primer.h"
class matrix { /* пользовательское определение */ };
// правильно: глобальный тип matrix найден
void func( matrix &m );
то определение класса matrix компилятор находит в глобальной области видимости и программа компилируется без ошибок. Поскольку объявление matrix как члена пространства имен cplusplus_primer скрыто в этом пространстве, оно не конфликтует с классом, объявленным в глобальной области видимости.
Именно поэтому мы говорим, что пространства имен решают проблему засорения глобального пространства: имена их членов невидимы, если имя пространства не указано явно, с помощью оператора разрешения области видимости. Существуют и другие механизмы, позволяющие сделать объявление члена пространства имен видимым вне его. Это using-объявления и using-директивы. Мы рассмотрим их в следующем разделе.
Отметим, что оператор области видимости может быть использован и для того, чтобы сослаться на элемент глобального пространства имен. Поскольку это пространство не имеет имени, запись
::member_name
относится к его элементу. Такой способ полезен для указания членов глобального пространства, если их имена оказываются скрыты именами, объявленными во вложенных локальных областях видимости.
Следующий пример демонстрирует использование оператора области видимости для обращения к скрытому члену глобального пространства имен. Функция вычисляет последовательность чисел Фибоначчи. В программе два определения переменной max. Глобальная переменная указывает максимальное значение элемента последовательности, при превышении которого вычисление прекращается, а локальная – желаемую длину последовательности при данном вызове функции. (Напоминаем, что параметры функции относятся к ее локальной области видимости.) Внутри функции должны быть доступны обе переменных. Однако неквалифицированное имя max ссылается на локальное объявление этой переменной. Чтобы получить глобальную переменную, нужно использовать оператор разрешения области видимости ::max. Вот текст программы:
#include <iostream>
const int max = 65000;
const int lineLength = 12;
void fibonacci( int max )
{
if ( max < 2 ) return;
cout << "0 1 ";
int v1 = 0, v2 = 1, cur;
for ( int ix = 3; ix <= max; ++ix ) {
cur = v1 + v2;
if ( cur > ::max ) break;
cout << cur << " ";
vl = v2;
v2 = cur;
if (ix % "lineLength == 0) cout << end"!;
}
}
Так выглядит функция main(), вызывающая fibonacci():
#include <iostream>
void fibonacci( int );
int main() {
cout << "Числа Фибоначчи: 16\n";
fibonacci( 16 );
return 0;
}
Результат работы программы:
Числа Фибоначчи: 16
0 1 1 2 3 5 8 13 21 34 55 89
144 233 377 610
Оператор sizeof
Оператор sizeof возвращает размер в байтах объекта или типа данных. Синтаксис его таков:
sizeof ( type name );
sizeof ( object );
sizeof object;
Результат имеет специальный тип size_t, который определен как typedef в заголовочном файле cstddef. Вот пример использования обеих форм оператора sizeof:
#include <cstddef>
int ia[] = { 0, 1, 2 };
// sizeof возвращает размер всего массива
size_t array_size = sizeof ia;
// sizeof возвращает размер типа int
size_t element_size = array_size / sizeof( int );
Применение sizeof к массиву дает количество байтов, занимаемых массивом, а не количество его элементов и не размер в байтах каждого из них. Так, например, в системах, где int хранится в 4 байтах, значением array_size будет 12. Применение sizeof к указателю дает размер самого указателя, а не объекта, на который он указывает:
int *pi = new int[ 3 ];
size_t pointer_size = sizeof ( pi );
Здесь значением pointer_size будет память под указатель в байтах (4 в 32-битных системах), а не массива ia.
Вот пример программы, использующей оператор sizeof:
#include <string>
#include <iostream>
#include <cstddef>
int main() {
size_t ia;
ia = sizeof( ia ); // правильно
ia = sizeof ia; // правильно
// ia = sizeof int; // ошибка
ia = sizeof( int ); // правильно
int *pi = new int[ 12 ];
cout << "pi: " << sizeof( pi )
<< " *pi: " << sizeof( pi )
<< endl;
// sizeof строки не зависит от
// ее реальной длины
string stl( "foobar" );
string st2( "a mighty oak" );
string *ps = &stl;
cout << " st1: " << sizeof( st1 )
<< " st2: " << sizeof( st2 )
<< " ps: sizeof( ps )
<< " *ps: " << sizeof( *ps )
<< endl;
cout << "short :\t" << sizeof(short) << endl;
cout << "shorf" :\t" << sizeof(short*) << endl;
cout << "short& :\t" << sizeof(short&) << endl;
cout << "short[3] :\t" << sizeof(short[3]) << endl;
}
Результатом работы программы будет:
pi: 4 *pi: 4
st1: 12 st2: 12 ps: 4 *ps:12
short : 2
short* : 4
short& : 2
short[3] : 6
Из данного примера видно, что применение sizeof к указателю позволяет узнать размер памяти, необходимой для хранения адреса. Если же аргументом sizeof является ссылка, мы получим размер связанного с ней объекта.
Гарантируется, что в любой реализации С++ размер типа char равен 1.
// char_size == 1
size_t char_size = sizeof( char );
Значение оператора sizeof вычисляется во время компиляции и считается константой. Оно может быть использовано везде, где требуется константное значение, в том числе в качестве размера встроенного массива. Например:
// правильно: константное выражение
int array[ sizeof( some_type_T )];
Оператор “стрелка”
Оператор “стрелка”, разрешающий доступ к членам, может перегружаться для объектов класса. Он должен быть определен как функция-член и обеспечивать семантику указателя. Чаще всего этот оператор используется в классах, которые предоставляют “интеллектуальный указатель” (smart pointer), ведущий себя аналогично встроенным, но поддерживают и некоторую дополнительную функциональность.
Допустим, мы хотим определить тип класса для представления указателя на объект Screen (см. главу 13):
class ScreenPtr {
// ...
private:
Screen *ptr;
};
Определение ScreenPtr должно быть таким, чтобы объект этого класса гарантировано указывал на объект Screen: в отличие от встроенного указателя, он не может быть нулевым. Тогда приложение сможет пользоваться объектами типа ScreenPtr, не проверяя, указывают ли они на какой-нибудь объект Screen. Для этого нужно определить класс ScreenPtr с конструктором, но без конструктора по умолчанию (детально конструкторы рассматривались в разделе 14.2):
class ScreenPtr {
public:
ScreenPtr( const Screen &s ) : ptr( &s ) { }
// ...
};
В любом определении объекта класса ScreenPtr должен присутствовать инициализатор– объект класса Screen, на который будет ссылаться объект ScreenPtr:
ScreenPtr p1; // ошибка: у класса ScreenPtr нет конструктора по умолчанию
Screen myScreen( 4, 4 );
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*:
#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;
}
Разумеется, подобные манипуляции с указателями на объекты классов не так эффективны, как работа со встроенными указателями. Поэтому интеллектуальный указатель должен предоставлять дополнительную функциональность, важную для приложения, чтобы оправдать сложность своего использования.
Оператор typeid
Второй оператор, входящий в состав RTTI, – это typeid, который позволяет выяснить фактический тип выражения. Если оно принадлежит типу класса и этот класс содержит хотя бы одну виртуальную функцию-член, то ответ может и не совпадать с типом самого выражения. Так, если выражение является ссылкой на базовый класс, то typeid сообщает тип производного класса объекта:
#include <typeinfo>
programmer pobj;
employee &re = pobj;
// с функцией name() мы познакомимся в подразделе, посвященном type_info
// она возвращает C-строку "programmer"
coiut << typeid( re ).name() << endl;
Операнд re оператора typeid имеет тип employee. Но так как re – это ссылка на тип класса с виртуальными функциями, то typeid говорит, что тип адресуемого объекта – programmer (а не employee, на который ссылается re). Программа, использующая такой оператор, должна включать заголовочный файл <typeinfo>, что мы и сделали в этом примере.
Где применяется typeid? В сложных системах разработки, например при построении отладчиков, а также при использовании устойчивых объектов, извлеченных из базы данных. В таких системах необходимо знать фактический тип объекта, которым программа манипулирует с помощью указателя или ссылки на базовый класс, например для получения списка его свойств во время сеанса работы с отладчиком или для правильного сохранения или извлечения объекта из базы данных. Оператор typeid допустимо использовать с выражениями и именами любых типов. Например, его операндами могут быть выражения встроенных типов и константы. Если операнд не принадлежит к типу класса, то typeid просто возвращает его тип:
int iobj;
cout << typeid( iobj ).name() << endl; // печатается: int
cout << typeid( 8.16 ).name() << endl; // печатается: double
Если операнд имеет тип класса, в котором нет виртуальных функций, то typeid возвращает тип операнда, а не связанного с ним объекта:
class Base { /* нет виртуальных функций */ };
class Derived : public Base { /* нет виртуальных функций */ };
Derived dobj;
Base *pb = &dobj;
cout << typeid( *pb ).name() << endl; // печатается: Base
Операнд typeid имеет тип Base, т.е. тип выражения *pb. Поскольку в классе Base нет виртуальных функций, результатом typeid будет Base, хотя объект, на который указывает pb, имеет тип Derived.
Результаты, возвращенные оператором typeid, можно сравнивать. Например:
#include <typeinfo>
employee *pe = new manager;
employee& re = *pe;
if ( typeid( pe ) == typeid( employee* ) ) // истинно
// что-то сделать
/*
if ( typeid( pe ) == typeid( manager* ) ) // ложно
if ( typeid( pe ) == typeid( employee ) ) // ложно
if ( typeid( pe ) == typeid( manager ) ) // ложно
*/
Условие в инструкции if сравнивает результаты применения typeid к операнду, являющемуся выражением, и к операнду, являющемуся именем типа. Обратите внимание, что сравнение
typeid( pe ) == typeid( employee* )
возвращает истину. Это удивит пользователей, привыкших писать:
// вызов виртуальной функции
pe->salary();
что приводит к вызову виртуальной функции salary() из производного класса manager. Поведение typeid(pe) не подчиняется данному механизму. Это связано с тем, что pe – указатель, а для получения типа производного класса операндом typeid должен быть тип класса с виртуальными функциями. Выражение typeid(pe) возвращает тип pe, т.е. указатель на employee. Это значение совпадает со значением typeid(employee*), тогда как все остальные сравнения дают ложь.
Только при употреблении выражения *pe в качестве операнда typeid результат будет содержать тип объекта, на который указывает pe:
typeid( *pe ) == typeid( manager ) // истинно
typeid( *pe ) == typeid( employee ) // ложно
В этих сравнениях *pe – выражение типа класса, который имеет виртуальные функции, поэтому результатом применения typeid будет тип адресуемого операндом объекта manager.
Такой оператор можно использовать и со ссылками:
typeid( re ) == typeid( manager ) // истинно
typeid( re ) == typeid( employee ) // ложно
typeid( &re ) == typeid( employee* ) // истинно
typeid( &re ) == typeid( manager* ) // ложно
В первых двух сравнениях операнд re имеет тип класса с виртуальными функциями, поэтому результат применения typeid содержит тип объекта, на который ссылается re. В последних двух сравнениях операнд &re имеет тип указателя, следовательно, результатом будет тип самого операнда, т.е. employee*.
На самом деле оператор typeid возвращает объект класса типа type_info, который определен в заголовочном файле <typeinfo>. Интерфейс этого класса показывает, что можно делать с результатом, возвращенным typeid. (В следующем подразделе мы подробно рассмотрим этот интерфейс.)
Оператор вывода <<
Оператор вывода обычно применяется для записи на стандартный вывод cout. Например, программа
#include <iostream>
int main()
{
cout << "сплетница Анна Ливия\n";
}
печатает на терминале строку:
сплетница Анна Ливия
Имеются операторы, принимающие аргументы любого встроенного типа данных, включая const char*, а также типов string и complex из стандартной библиотеки. Любое выражение, включая вызов функции, может быть аргументом оператора вывода при условии, что результатом его вычисления будет тип, принимаемый каким-либо вариантом этого оператора. Например, программа
#include <iostream>
#include <string.h>
int main()
{
cout << "Длина 'Улисс' равна:\t";
cout << strlen( "Улисс" );
cout << '\n';
cout << "Размер 'Улисс' равен:\t";
cout << sizeof( "Улисс" );
cout << endl;
}
выводит на терминал следующее:
Длина 'Улисс' равна:7
Размер 'Улисс' равен:8
endl – это манипулятор вывода, который вставляет в выходной поток символ перехода на новую строку, а затем сбрасывает буфер объекта ostream. (С буферизацией мы познакомимся в разделе 20.9.)
Операторы вывода, как правило, удобнее сцеплять в одну инструкцию. Например, предыдущую программу можно записать таким образом:
#include <iostream>
#include <string.h>
int main()
{
// операторы вывода можно сцеплять
cout << "Длина 'Улисс' равна:\t";
<< strlen( "Улисс" ) << '\n';
cout << "Размер 'Улисс' равен:\t"
<< sizeof( "Улисс" ) << endl;
}
Сцепление операторов вывода (и ввода тоже) возможно потому, что результатом выражения
cout << "некоторая строка";
служит левый операнд оператора вывода, т.е. сам объект cout. Затем этот же объект передается следующему оператору и далее по цепочке (мы говорим, что оператор << левоассоциативен).
Имеется также предопределенный оператор вывода для указательных типов, который печатает адрес объекта. По умолчанию адреса отображаются в шестнадцатеричном виде. Например, программа
#include <iostream>
int main()
{
int i = 1024;
int *pi = &i;
cout << "i: " << i
<< "\t&i:\t" << &i << '\n';
cout << "*pi: " << *pi
<< "\tpi:\t" << pi << endl
<< "\t\t&pi:\t" << &pi << endl;
}
выводит на терминал следующее:
i: 1024 &i: 0x7fff0b4
*pi: 1024 pi: 0x7fff0b4
&pi: 0x7fff0b0
Позже мы покажем, как напечатать адреса в десятичном виде.
Следующая программа ведет себя странно. Мы хотим напечатать адрес, хранящийся в переменной pstr:
#include <iostream>
const char *str = "vermeer";
int main()
{
const char *pstr = str;
cout << "Адрес pstr равен: "
<< pstr << endl;
}
Но после компиляции и запуска программа неожиданно выдает такую строку:
Адрес pstr равен: vermeer
Проблема в том, что тип const char* интерпретируется как C-строка. Чтобы все же напечатать адрес, хранящийся в pstr, необходимо подавить обработку типа const char* по умолчанию. Для этого мы сначала убираем спецификатор const, а затем приводим pstr к типу void*:
<< static_cast<void*>(const_cast<char*>(pstr))
Теперь программа выводит ожидаемый результат:
Адрес pstr равен: 0x116e8
А вот еще одна загадка. Нужно напечатать большее из двух чисел:
#include <iostream>
inline void
max_out( int val1, int val2 )
{
cout << ( val1 > val2 ) ? val1 : val2;
}
int main()
{
int ix = 10, jx = 20;
cout << "Большее из " << ix
<< ", " << jx << " равно ";
max_out( ix, jx );
cout << endl;
}
Однако программа выдает неправильный результат:
Большее из 10, 20 равно 0
Проблема в том, что оператор вывода имеет более высокий приоритет, чем оператор условного выражения, поэтому печатается результат сравнения val1 и val2. Иными словами, выражение
cout << ( val1 > val2 ) ? val1 : val2;
вычисляется как
(cout << ( val1 > val2 )) ? val1 : val2;
Поскольку val1 не больше val2, то результатом сравнения будет false, обозначаемый нулем. Чтобы изменить приоритет операций, весь оператор условного выражения следует заключить в скобки:
cout << ( val1 > val2 ? val1 : val2 );
Теперь результат получается правильный:
Большее из 10, 20 равно 20
Такого рода ошибку было бы проще найти, если бы значения литералов true и false типа bool печатались как строки, а не как 1 и 0. Тогда мы увидели бы строку:
Большее из 10, 20 равно false
и все стало бы ясно. По умолчанию литерал false печатается как 0, а true – как 1. Это можно изменить, воспользовавшись манипулятором boolalpha(), что и сделано в следующей программе:
int main()
{
cout << "печать значений типа bool по умолчанию: "
<< true << " " << false
<< "\nи в виде строк: "
<< boolalpha()
<< true << " " << false
<< endl;
}
Вот результат:
печать значений типа bool по умолчанию: 1 0
и в виде строк: true false
Для вывода массива, а также вектора или отображения, необходимо обойти все элементы и напечатать каждый из них:
#include <iostream>
#include <vector>
#include <string>
string pooh_pals[] = {
"Тигра", "Пятачок", "Иа-Иа", "Кролик"
};
int main()
{
vector<string> ppals( pooh_pals, pooh_pals+4 );
vector<string>::iterator iter = ppals.begin();
vector<string>::iterator iter_end = ppals.end();
cout << "Это друзья Пуха: ";
for ( ; iter != iter_end; iter++ )
cout << *iter << " ";
cout << endl;
}
Вместо того чтобы явно обходить все элементы контейнера, выводя каждый по очереди, можно воспользоваться потоковым итератором ostream_iterator. Так выглядит эквивалентная программа, где используется эта техника (подробное обсуждение итератора ostream_iterator см. в разделе 12.4):
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
string pooh_pals[] = {
"Тигра", "Пятачок", "Иа-Иа", "Кролик"
};
int main()
{
vector<string> ppals( pooh_pals, pooh_pals+4 );
vector<string>::iterator iter = ppals.begin();
vector<string>::iterator iter_end = ppals.end();
cout << "Это друзья Пуха: ";
// копируем каждый элемент в cout ...
ostream_iterator< string > output( cout, " " );
copy( iter, iter_end, output );
cout << endl;
}
Программа печатает такую строку:
Это друзья Пуха: Тигра Пятачок Иа-Иа Кролик
Упражнение 20.1
Даны следующие определения объектов:
string sa[4] = { "пух", "тигра", "пятачок", "иа-иа" };
vector< string > svec( sa, sa+4 );
string robin( "кристофер робин" );
const char *pc = robin.c_str();
int ival = 1024;
char blank = ' ';
double dval = 3.14159;
complex purei( 0, 7 );
(a) Направьте значение каждого объекта в стандартный вывод.
(b) Напечатайте значение адреса pc.
(c) Напечатайте наименьшее из двух значений ival и dval, пользуясь оператором условного выражения:
ival < dval ? ival : dval
Оператор вызова функции
Оператор вызова функции может быть перегружен для объектов типа класса. (Мы уже видели, как он используется, при рассмотрении объектов-функций в разделе 12.3.) Если определен класс, представляющий некоторую операцию, то для ее вызова перегружается соответствующий оператор. Например, для взятия абсолютного значения числа типа int можно определить класс absInt:
class absInt {
public:
int operator()( int val ) {
int result = val < 0 ? -val : val;
return result;
}
};
Перегруженный оператор operator() должен быть объявлен как функция-член с произвольным числом параметров. Параметры и возвращаемое значение могут иметь любые типы, допустимые для функций (см. разделы 7.2, 7.3 и 7.4). operator() вызывается путем применения списка аргументов к объекту того класса, в котором он определен. Мы рассмотрим, как он используется в одном из обобщенных алгоритмов, описанных в главе 12. В следующем примере обобщенный алгоритм transform() вызывается для применения определенной в absInt операции к каждому элементу вектора ivec, т.е. для замены элемента его абсолютным значением.
#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() );
// ...
}
Первый и второй аргументы transform() ограничивают диапазон элементов, к которым применяется операция absInt. Третий указывает на начало вектора, где будет сохранен результат применения операции.
Четвертый аргумент – это временный объект класса absInt, создаваемый с помощью конструктора по умолчанию. Конкретизация обобщенного алгоритма transform(), вызываемого из main(), могла бы выглядеть так:
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, указывающий на тот элемент вектора, для которого мы хотим получить абсолютное значение.
Оператор взятия индекса
Оператор взятия индекса 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 ];
}
В следующем фрагменте нулевому элементу массива color присваивается символ 'V':
String color( "violet" );
color[ 0 ] = 'V';
Обратите внимание, что в определении оператора проверяется выход индекса за границы массива. Для этого используется библиотечная C-функция assert(). Можно также возбудить исключение, показывающее, что значение elem меньше 0 или больше длины C-строки, на которую ссылается _string. (Возбуждение и обработка исключений обсуждались в главе 11.)