Глава 23. ОПЕРАТОР ПРИСВОЕНИЯ...271
Глава 24. ИСПОЛЬЗОВАНИЕ ПОТОКОВ ВВОДА-ВЫВОДА...277
Глава 25. ОБРАБОТКА ОШИБОК И ИСКЛЮЧЕНИЯ...290
Глава 26. МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ...298
Глава 28. СТАНДАРТНАЯ БИБЛИОТЕКА ШАБЛОНОВ...317
В этой части...
В этой книге не ставится цель сделать из вас профессионала в области С++, а всего лишь предполагается дать вам твёрдое понимание основ С++ и объектно-ориентированного программирования.
Освоив предыдущие части книги, вы приобрели самые необходимые знания по созданию качественной объектно-ориентированной программы. Конечно же, С++ весьма обширный и богатый разнообразными возможностями язык, и осталось ещё немало особенностей, которые требуют освещения. В этой части представлено краткое описание дополнительных и, по моему мнению, наиболее полезных возможностей языка, которые стоит использовать в первую очередь ( хотя это и не обязательно ).
В этой главе...
►Сравнение операторов и функций 271
►Мелкое копирование — глубокие проблемы 272
►Переопределение оператора присвоения 273
Встроенные ( intrinsic ) типы данных — это типы данных, которые компилятор "знает" изначально, такие как int, float, double и другие, а также различные типы указателей. В главах 3, "Выполнение математических операций", и 4, "Выполнение логических операций", были описаны операторы, определённые в С++ для встроенных типов. С++ позволяет программисту определять операторы для создаваемых им классов в дополнение к встроенным операторам. Эта возможность называется перегрузкой операторов.
Обычно перегрузка операторов необязательна и не используется новичками в программировании на С++. Более того, многие опытные программисты вообще считают данную возможность излишней и опасной. Однако есть один оператор, который вы будете просто вынуждены переопределять: это оператор присвоения.
Оператор представляет собой не более чем встроенную функцию с определённым синтаксисом. Так, сложение а+b можно рассматривать, как если бы это была запись operator+( a , b ). С++ даёт каждому оператору имя в стиле функции. Такое функциональное имя оператора состоит из ключевого слова operator, за которым следует символ оператора, а за ним — соответствующие типы аргументов. Например, оператор +, который суммирует целые числа и возвращает целое число, имеет имя int operator+( int , int ).
Любой оператор может быть определён для пользовательского класса. Так, я могу создать Complex operator*( Complex& , Complex& ), который позволит мне умножить два объекта типа Complex. Новый оператор может иметь ту же семантику, что и перегружаемый, но не обязан. При перегрузке операторов действуют следующие правила.
■■■
■ Программист не может перегрузить операторы ., ::, * ( разыменование ) и &.
■ Программист не может вводить новые операторы, например, х$у.
■ Формат оператора не может быть изменён. Например, вы не можете определить оператор %i, поскольку % — бинарный оператор.
■ Приоритет операторов не может быть изменён. Программа не может заставить оператор + выполняться раньше оператора *.
■ Операторы не могут быть переопределены для встроенных типов — вы не в состоянии изменить смысл записи 1+2. Существующие операторы могут быть перегружены только для вновь создаваемых типов.
■■■
Перегрузка операторов — одна из тех вещей, которые выглядят лучше, чем есть на самом деле. По моему опыту, перегрузка операторов создаёт больше проблем, чем решает, с двумя важными исключениями, которые будут рассмотрены далее в этой главе.
Операторы считывания из потока и записи в него, << и >>, — это не что иное, как переопределённые операторы левого и правого сдвига для набора классов, представляющих потоки ввода-вывода. Эти определения находятся в файле iostream. Таким образом, запись cout <<"some string" означает вызов функции operator<<( " some string" ). Наши старые знакомые сin и cout представляют собой предопределённые объекты, связанные с клавиатурой и монитором соответственно. Подробнее мы поговорим об этом в главе 24, "Использование потоков ввода-вывода".
Независимо от того, что думаете вы и многие другие о переопределении операторов, вам всё равно придётся переопределять оператор присвоения для множества ваших классов. С++ предоставляет определение operator=( ) по умолчанию, но этот оператор просто выполняет почленное копирование. Такое присвоение отлично работает для встроенных операторов типа int.
int i ;
i = 10 ;
Точно так же ведёт себя присвоение по умолчанию и для пользовательских классов. В следующем примере каждый член source копируется в соответствующий член destination.
void fn( )
{
MyStruct source , destination ;
destination = source ;
}
Оператор присвоения по умолчанию вполне работоспособен для большинства классов, однако при выделении ресурсов, таких как память из кучи, начинаются проблемы. В этом случае программист должен переопределить оператор operator=( ) для корректной передачи ресурса.
Оператор присвоения очень похож на конструктор копирования, а при использовании они практически идентичны.
void fn( MyClass& mc )
{
MyClass newMC( mc ) ; /* Здесь используется конструктор копирования */
MyClass newerMC = mc ; /* Менее очевидно, что здесь также используется конструктор копирования */
MyClass newestMC ; /* Создание объекта по умолчанию */
newestMC = mc ; /* Присвоение */
}
_________________
272 стр. Часть 5. Полезные особенности
Создание newMC следует стандартному шаблону создания нового объекта как зеркального отображения существующего с использованием копирующего конструктора MyClass( MyClass& ). Несколько менее очевидно, что объект newerMC также создаётся при помощи конструктора копирования. Запись MyClass а = b — всего лишь другой способ записи MyClass a( b ). То, что в первом варианте записи имеется символ "=", не приводит к вызову оператора присвоения. Однако в случае с объектом newestMC всё не совсем так. Сначала этот объект создаётся с использованием конструктора по умолчанию, а затем перезаписывается объектом mc с помощью оператора присвоения.
Подобно конструктору копирования, оператор присвоения должен быть переопределён, если мелкое копирование приводит к некорректным результатам ( см. материал, представленный в главе 18, "Копирующий конструктор" ). Простейшее правило: если у класса есть пользовательский конструктор копирования, то переопределите для него и оператор присвоения.
«Главное правило заключается в следующем: конструктор копирования используется при создании нового объекта, а оператор присвоения — если объект слева от символа присвоения уже существует.»
[Советы]
Следующая программа демонстрирует переопределение оператора присвоения. В программе также представлен конструктор копирования — просто для сравнения.
//
/* DemoAssignmentOperator — демонстрация оператора */
/* присвоения для пользовательского класса */
//
#include
#include
#include
#include
using namespace std ;
/* Name — класс для демонстрации присвоения и конструктора копирования */
class Name
{
public :
Name( char *pszN = 0 )
{
copyName( pszN , " " ) ;
}
Name( Name& s )
{
cout << "Вызов конструктора копирования" << endl ;
copyName( s.pszName , " ( copy ) " ) ;
}
~Name( )
{
deleteName( ) ;
}
/* Оператор присвоения */
Name& operator=( Name& s )
{
cout << "Выполнение присвоения" << endl ;
_________________
273 стр. Глава 23. Оператор присвоения
/* Удаляем выделенную память... */
deleteName( ) ;
/* ...перед заменой её новым блоком */
copyName( s.pszName , " ( replaced ) " ) ;
/* Возвращаем ссылку на существующий объект */
return *this ;
}
/* Очень простая функция доступа */
char* out( ) { return pszName ; }
protected :
void copyName( char* pszN , char* pszAdd ) ;
void deleteName( ) ;
char *pszName ;
} ;
/* copyName( ) — Выделение памяти из кучи и сохранение строк в ней */
void Name::copyName( char* pszN , char* pszAdd )
{
pszName = 0 ;
if ( pszN )
{
pszName = new char[ strlen( pszN ) +
strlen( pszAdd ) + 1 ] ;
strcpy( pszName , pszN ) ;
strcat( pszName , pszAdd ) ;
}
}
/* deleteName( ) — возврат памяти в куче */
void Name::deleteName( )
{
if ( pszName )
{
delete pszName ;
pszName = 0 ;
}
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
/* Создание двух объектов */
Name n1( "Claudette" ) ;
Name n2( "Greg" ) ;
cout << "n1 ( " << n1.out( ) << " ) и "
<<"n2 ( " << n2.out( ) << " ) — "
<< "вновь созданные объекты"
<< endl ;
/* Конструктор копирования */
cout << "Name n3( n1 ) ;" << endl ;
Name n3( n1 ) ;
cout << "n3 ( " << n3.out( ) << " ) — копия n1" << endl ;
_________________
274 стр. Часть 5. Полезные особенности
/* Создание нового объекта с использованием формата с "=" для обращения к конструктору копирования */
cout << "Name n4 = n1 ;" << endl ;
Name n4 = n1 ;
cout << "n4 ( " << n4.out( ) << " ) — ещё одна копия n1"
<< endl ;
/* Перезапись n2 объектом n1 */
cout << "n2 = n1" << endl ;
n2 = n1 ;
cout << "n1 ( " << n1.out( ) << " ) присвоен объекту "
<< "n2 ( " << n2.out( ) << " )" << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Класс Name содержит указатель на имя человека, которое записывается в блок памяти, выделяемый из кучи. Конструктор и деструктор класса Name аналогичны представленным в главах 17, "Аргументация конструирования", и 18, "Копирующий конструктор". Конструктор Name( char* ) копирует переданное ему имя в член pszName. Этот конструктор служит также в роли конструктора по умолчанию. Конструктор копирования Name( Name& ) копирует имя переданного объекта при помощи функции-члена copyName( ). Деструктор освобождает блок памяти при помощи вызова deleteName( ).
Оператор присвоения operator=( ) является методом класса. Он выглядит как деструктор, за которым тут же следует конструктор копирования, что представляет собой вполне типичную ситуацию. Рассмотрим присвоение n2 = n1. Объект n2 уже имеет связанное с ним имя "Greg". В процессе присвоения память, выделенная для этого имени, освобождается при помощи вызова deleteName( ), так же, как это делается в деструкторе. Затем оператор присвоения вызывает copyName( ) для копирования новой информации в объект, подобно тому, как это делается в конструкторе копирования.
Конструктору копирования не нужно вызывать deleteName( ), поскольку объект в этот момент ещё не существует, и память из кучи не выделена. Соответственно, деструктору не надо вызывать функцию копирования.
Есть ещё пара деталей, о которых следует упомянуть. Во-первых, возвращаемый оператором присвоения тип — Name&. Выражения, включающие оператор присвоения, имеют тип и значение, которые совпадают с типом и значением левого аргумента после присвоения. В следующем примере значение operator=( ) равно 2.0 , а его тип — double.
double d1 , d2 ;
void fn( double )
d1 = 2.0 ; /* Значение этого выражения равно 2.0 */
Это позволяет программисту написать следующее:
d2 = d1 = 2.0 ;
fn( d2 = 3.0 ) ; /* Выполняет присвоение и передаёт полученное значение функции fn( ) */
Значение присвоения d1 = 2.0, равное 2.0, и его тип double передаются для присвоения d2. Во втором примере значение присвоения d2 = 3.0 передаётся функции fn( ).
Во-вторых, оператор присвоения является функцией-членом. Её левый аргумент — текущий объект ( this ). В отличие от других операторов, оператор присвоения не может быть перегружен при помощи функции — не члена класса.
_________________
275 стр. Глава 23. Оператор присвоения
Оснащение вашего класса оператором присвоения может значительно повысить его гибкость. Однако, если это требует слишком большого объёма работы или вы не хотите, чтобы С++ создавал копии вашего объекта, перегрузка оператора присвоения защищённой функцией оградит вас от нежелательного мелкого почленного копирования.
class Name
{
/* ...всё, как и раньше... */
protected :
/* Конструктор копирования */
Name( Name& ){ } ;
/* Оператор присвоения */
Name& operator=( Name& s ) { return *this ; }
}
Присвоения наподобие приведённого далее ( при таком определении ) будут запрещены[ 18 ].
void fn( Name& n )
{
Name newN ;
newN = n ; /* Ошибка компиляции — функция не */
/* имеет права доступа к operator=( ) */
}
Такая защита от копирования спасает вас от перегрузки оператора присвоения, но при этом снижает гибкость вашего класса.
«Если ваш класс использует какие-либо ресурсы, например, память из кучи, вы обязаны либо разработать удовлетворительные оператор присвоения и конструктор копирования, либо сделать их защищёнными для предотвращения их использования.»
[Советы]
______________
18В определении тела защищённых конструктора копирования и оператора присвоения нет необходимости, поскольку они никогда не будут вызываться. Таким образом, вы можете просто указать их в защищённой части объявления класса, никак их не реализуя. — Прим. ред.
_________________
276 стр. Часть 5. Полезные особенности
В этой главе...
►Как работают потоки ввода-вывода 277
►Знакомство с подклассами fstream 278
►Что такое endl 284
Все программы, которые встречались в книге, читали информацию из объекта сin и выводили её в объект cout. Может, это и не интересовало вас, но эта технология ввода-вывода представляет собой подмножество того, что известно под названием потоков ввода-вывода.
В этой главе потоки ввода-вывода описываются более детально. Но должен предупредить вас: это слишком большая тема, чтобы всесторонне осветить её в одной главе; ей посвящены отдельные книги. К счастью для всех нас, написание подавляющего большинства программ не требует глубоких знаний в области потоков ввода-вывода.
Потоки ввода-вывода основаны на перегруженных операторах operator>>( ) и operator<<( ). Объявления этих операторов находятся в заголовочном файле iostream, который мы включаем во все программы в данной книге. Коды этих функций находятся в стандартной библиотеке, с которой компонуются ваши программы. Вот листинг некоторых прототипов из файла iostream.
/* Операторы для ввода: */
istream& operator>>( istream& source , char* pDest ) ;
istream& operator>>( istream& source , int& dest ) ;
istream& operator>>( istream& source , char& dest ) ;
/* ...и так далее... */
/* Операторы для вывода: */
istream& operator<<( ostream& dest , char* pSource ) ;
istream& operator<<( ostream& dest , int& source ) ;
stream& operator<<( ostream& dest , char& source ) ;
/* ...и так далее... */
Оператор operator>>( ) называется оператором извлечения из потока, а operator<<( ) — оператором вставки в поток. Класс istream является базовым для ввода информации из файла или устройства ввода типа клавиатуры. При запуске программы на выполнение С++ открывает объект cin класса istream. Аналогично, ostream представляет собой базовый класс для файлового вывода, a cout — объект класса ostream по умолчанию.
_________________
277 стр. Глава 24. Использование потоков ввода-вывода
«Рассмотрим, что получится, если написать следующий код ( имеющийся на прилагаемом компакт-диске ).»
[Диск]
/* DefaultStreamOutput */
#include
#include
using namespace std ;
void fn( ostream& out )
{
out << "Меня зовут Стефан\n" ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
fn( cout ) ;
system( "PAUSE" ) ; return 0 ;
}
Программа передаёт функции fn( ) поток cout. Функция использует оператор operator<<( ). Сначала С++ определит, что левый аргумент имеет тип ostream, а правый — тип char*. Вооружённый этими знаниями, он найдёт прототип функции operator<<( ostream& , char* ) в заголовочном файле iostream. Затем С++ вызовет функцию вставки в поток для char*, передавая ей строку "Меня зовут Стефан\n" и объект cout в качестве аргументов. Другими словами, он вызовет функцию operator<<( cout , "Меня зовут Стефан\n" ). Функция для вставки char* в поток, которая является частью стандартной библиотеки С++, выполнит необходимый вывод.
Но откуда компилятору известно, что cout является объектом класса ostream? Этот и ещё несколько глобальных объектов объявлены в файле iostream.h ( их список приведён в табл. 24.1 ). Эти объекты автоматически конструируются при запуске программы, до того как main( ) получает управление.
Таблица 24.1. Стандартные потоки ввода-вывода
_________________
Объект — Класс — Назначение
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
cin — istream — Стандартный ввод
cout — ostream — Стандартный вывод
cerr — ostream — Стандартный небуферизованный вывод сообщений об ошибках
clog — ostream — Стандартный буферизованный вывод сообщений об ошибках
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Подклассы ofstream, ifstream и fstream объявлены в заголовочном файле fstream.h и обеспечивают потоки ввода-вывода в дисковые файлы. Эти три класса предоставляют множество функций для управления вводом и выводом, многие из которых наследуются от ostream и istream. Полный список этих функций вы можете найти в документации к компилятору, а здесь я приведу только несколько из них, чтобы вы могли с чего-то начать.
_________________
278 стр. Часть 5. Полезные особенности
Класс ofstream, который используется для файлового вывода, имеет несколько конструкторов; наиболее часто применяется следующий:
ofstream::ofstream( char* pFileName ,
int mode = ios::out ,
int prot = filebuff::openprot ) ;
Первый аргумент этого конструктора — указатель на имя открываемого файла. Второй и третий аргументы определяют, как именно должен быть открыт файл. Корректные значения аргумента mode приведены в табл. 24.2, a prot — в табл. 24.3. Эти значения являются битовыми полями, к которым применяется оператор побитового ИЛИ ( классы ios и filebuff — родительские по отношению к ostream ).
«Выражение ios::out представляет статический член-данные класса ios.»
[Советы]
Таблица 24.2. Значения аргумента mode в конструкторе класса ofstream
_________________
Флаг — Назначение
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
ios::app — Дописывать в конец файла. Вернуть ошибку, если файл не существует
ios::ate — Дописывать в конец файла, если он существует
ios::in — Открыть файл для ввода ( подразумевается для istream )
ios::out — Открыть файл для вывода ( подразумевается для ostream )
ios::trunc — Обрезать файл до нулевой длины, если он существует ( используется по умолчанию )
ios::nocreate — Если файла не существует, вернуть сообщение об ошибке
ios::noreplace — Если файл существует, вернуть сообщение об ошибке
ios::binary — Открыть файл в бинарном режиме ( альтернатива текстовому режиму )
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Таблица 24.3. Значения аргумента prot в конструкторе класса ofstream
_________________
Флаг — Назначение
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
filebuf::openprot — Режим совместного чтения и записи
filebuf::sh_none — Исключительный режим без совместного доступа
filebuf::sh_read — Режим совместного чтения
filebuf::sh_write — Режим совместной записи
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Приведённая ниже программа открывает файл MyName.txt, а затем записывает в него некоторую важную информацию.
/* StreamOutput — простой вывод в файл */
#include
using namespace std ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
ofstream my( "MyName.txt" ) ;
my << "С++ для чайников — очень хорошая книга"
<< endl ;
system( "PAUSE" ) ; return 0 ;
}
_________________
279 стр. Глава 24. Использование потоков ввода-вывода
Конструктор ofstream::ofstream( char* ) получает только имя, а потому использует для режима открытия файла значения по умолчанию. Если файл MyName.txt уже существует, он урезается; в противном случае создаётся новый файл MyName.txt. Кроме того, файл открывается в режиме совместного чтения и записи.
Второй конструктор, ofstream::ofstream( char* , int ), позволяет программисту указывать другие режимы ввода-вывода. Например, если бы я захотел открыть файл в бинарном режиме и произвести запись в конец этого файла ( если он уже существует ), я мог бы создать объект класса ofstream так, как это показано ниже ( напомню, что в бинарном режиме при выводе не выполняется преобразование символа новой строки \n в пару символов перевода каретки и новой строки \r\n, так же как при вводе не происходит обратного преобразования ).
void fn( )
{
/* Откроем бинарный файл BINFILE для записи; если он существует, дописываем информацию в конец файла */
ofstream bfile( "BINFILE", ios::binary | ios::ate ) ;
/* ...продолжение программы... */
}
Потоковые объекты хранят информацию о состоянии процесса ввода-вывода. Функция-член bad( ) возвращает TRUE, если при работе с файловым объектом произошло что-то "плохое". Сюда входят такие неприятности, как невозможность открыть файл, нарушение внутренней структуры и т.п. Функция fail( ) указывает, что либо произошла ошибка, либо последнее чтение было неудачным. Функция good( ) возвращает TRUE, если и bad( ), и fail( ) возвращают FALSE. Функция clear( ) используется для сброса флага ошибки. Вот как выглядит добавление простейшей обработки ошибок к рассмотренной нами программе.
/* StreamOutputWithErrorChecking — простой вывод в файл */
#include
#include
using namespace std ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
const static char fileName[ ] = "MyName.txt" ;
ofstream my( fileName ) ;
if ( my.bad( ) ) /* Открыть не удалось... */
{
cerr << "Ошибка открытия файла "
<< fileName
<< endl ;
return 0 ; /* ...вывод сообщения и завершение работы */
}
my << "С++ для чайников — очень хорошая книга"
<< endl ;
if ( my.bad( ) )
{
cerr << "Ошибка записи в файл "
<< fileName
<< endl ;
}
system( "PAUSE" ) ; return 0 ;
}
_________________
280 стр. Часть 5. Полезные особенности
«Все попытки обратиться к объекту класса ofstream, который содержит ошибку, не вызовут никакого действия, пока флаг ошибки не будет сброшен с помощью функции clear( ).»
[Советы]
Деструктор класса ofstream автоматически закрывает файл. В предыдущем примере файл был закрыт при выходе из функции.
Класс ifstream работает для ввода почти так же, как ofstream для вывода, что и демонстрирует приведённый ниже пример.
/* StreamInput — ВВОД ДАННЫХ С ИСПОЛЬЗОВАНИЕМ fstream */
#include
#include
#include
using namespace std ;
ifstream* openFile( )
{
ifstream* pFileStream = 0 ;
for ( ; ; )
{
/* Открытие файла, указанного пользователем */
char fileName[ 80 ] ;
cout << "Введите имя файла с целыми числами"
<< endl ;
cin >> fileName ;
/* Открываем файл для чтения; не создавать файл, если его не существует */
pFileStream = new ifstream( fileName ) ;
if ( pFileStream -> good( ) )
{
break ;
}
cerr << "Невозможно открыть " << fileName << endl ;
delete pFileStream ;
}
return pFileStream ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
/* Получение файлового потока */
ifstream* pFileStream = openFile( ) ;
/* Остановиться по достижению конца файла */
while ( !pFileStream -> eof( ) )
{
/* Чтение значения */
int nValue = 0 ;
( *pFileStream ) >> nValue ;
/* Останов при ошибке чтения ( например, считывается не целое число, или считан символ новой строки, после которого ничего нет */
if ( pFileStream -> fail( ) )
{
break ;
}
/* Вывод считанного значения */
cout << nValue << endl ;
}
delete pFileStream ;
system( "PAUSE" ) ; return 0 ;
}
_________________
281 стр. Глава 24. Использование потоков ввода-вывода
Функция openFile( ) запрашивает у пользователя имя открываемого файла и создаёт поток с этим именем. Создание объекта ifstream автоматически открывает файл для ввода. Если файл открыт корректно, функция возвращает указатель на объект ifstream, который используется для чтения. В противном случае объект удаляется и повторяется попытка открыть файл. Единственный способ выйти из цикла — ввести правильное имя файла или завершить выполнение программы.
«Не забывайте о необходимости удаления pFileStream, если вы не смогли открыть файл. Это позволит избежать утечек памяти.»
[Советы]
Программа считывает целые числа до тех пор, пока не дойдёт до конца файла ( проверяется при помощи функции-члена eof( ) ) или не произойдёт ошибки чтения ( функция fail( ) ). Попытка прочитать информацию с помощью объекта класса ifstream с установленным флагом ошибки приведёт к немедленному возврату без считывания данных. Для сброса флага ошибки используйте функцию clear( ).
«Ещё раз напомню, что при чтении из потока в состоянии ошибки ничего считано не будет. Более того, буфер останется неизменным, так что программа может прийти к ошибочному выводу, что прочитано такое же значение, как и перед этим. Кстати, при наличии ошибки функция eof( ) никогда не вернёт true.»
[Советы]
Вывод этой программы имеет следующий вид.
Введите имя файла с целыми числами
testfile
Невозможно открыть testfile
Введите имя файла с целыми числами
integers.txt
123
456
234
654
4363
48923
78237
Press any key to continue...
Операторы вставки и извлечения обеспечивают удобный механизм чтения форматированного ввода. Однако бывают ситуации, когда надо просто прочесть нечто из потока, не заботясь о формате входной информации. В этом случае вам могут помочь два метода. Функция-член getline( ) возвращает строку символов, считанную из потока до появления в нём некоторого символа-терминатора — по умолчанию символа новой строки. Данная функция удаляет терминатор из строки, но не делает никаких других попыток каким-либо образом изменить или интерпретировать вводимую строку.
_________________
282 стр. Часть 5. Полезные особенности
Вторая функция-член read( ) носит ещё более фундаментальный характер. Она просто считывает указанное вами количество символов ( либо меньшее, если в процессе чтения достигается конец файла ). Функция gcount( ) возвращает количество реально считанных символов.
Далее приведена демонстрационная программа, которая использует описанные функции для чтения произвольного файла и вывода его на дисплей.
/* FileInput — чтение блока данных из файла */
#include
#include
#include
using namespace std ;
ifstream* openFile( istream& input )
{
for ( ; ; )
{
/* Открытие определённого пользователем файла */
char fileName[ 80 ] ;
cout << " Введите имя файла" << endl ;
/* Чтение вводимого пользователем имени ( не более 80 символов, что обеспечивает невозможность переполнения буфера ) */
input.getline( fileName , 80 ) ;
/* Открываем файл для чтения; если его нет — заново его не создаём */
ifstream* pFileStream = new ifstream( fileName ) ;
if ( pFileStream -> good( ) )
{
return pFileStream ;
}
cerr << "Невозможно найти файл " << fileName << endl ;
}
return 0 ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
/* Получаем поток */
ifstream* pFileStream = openFile( cin ) ;
/* Читаем его блоками по 80 байт */
char buffer[ 80 ] ;
while ( !pFileStream -> eof( ) && pFileStream -> good( ) )
{
/* Чтение блоками; gcount( ) возвращает количество реально считанных байт */
pFileStream -> read( buffer , 80 ) ;
int noBytes = pFileStream -> gcount( ) ;
/* Работа с блоком */
for ( int i = 0 ; i < noBytes ; i++ )
{
cout << buffer[ i ] ;
}
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
283 стр. Глава 24. Использование потоков ввода-вывода
В данной программе сначала вызывается функция openFile( ), открывающая файл, имя которого вводится пользователем. Здесь есть два интересных момента. Во-первых, функция читает объект istream так же, как ранее — cin ( функция main( ) передаёт функции openFile( ) поток cin в качестве аргумента ). Однако данная функция может использовать произвольный поток istream без каких-либо модификаций.
Во-вторых, функция openFile( ) использует для чтения из потока функцию getline( ), одним из аргументов которой является размер буфера для ввода информации. Функция getline( ) не может считать больше указанного количества символов.
«Использование этой функции для чтения информации безопаснее чтения информации в массив символов при помощи оператора извлечения, так как оператор извлечения может прочесть больше символов, чем может поместить входной буфер.»
[Советы]
Функция main( ) читает открытый файл блоками по 80 символов, проверяя реально считанное количество символов с помощью функции gcount( ). Для вывода прочитанной информации используется обычный оператор вставки в поток. Вот как может выглядеть вывод данной программы.
Введите имя файла
integers.txt
123 456 234 654
4363 48923 78237 dhbj
dnbsd
93276823 4329
Press any key to continue...
Большинство программ в данной книге завершают вывод в поток вставкой объекта endl. Однако некоторые программы включают в выводимый текст символ \n. В чём тут дело?
Символ \n — символ новой строки. Так, выражение соut<<"Первая строка\nВторая строка" выведет две строки. При вставке объекта endl также произойдёт вывод символа новой строки, но при этом выполняется ещё одно действие.
Диски — медленные устройства, и чтобы вывод на диск меньше замедлял работу программы, fstream накапливает выводимые данные во внутреннем буфере. Класс выводит буфер на диск по его заполнении. Вставка же объекта endl заставляет сбросить на диск всё, что есть в буфере, независимо от его заполненности. Сбросить буфер без вывода символа новой строки можно при помощи явного вызова функции-члена flush( ).
_________________
284 стр. Часть 5. Полезные особенности
Потоковые классы позволяют программисту разбивать входные данные на числа и массивы символов. Так называемые "строковые потоки" позволяют использовать операции, определённые для файлов в классах fstream, для строк в памяти. Соответствующие классы istringstream и ostringstream определены в заголовочном файле sstream.
«В старых версиях С++ эти классы назывались istrstream и ostrstream и были определены в заголовочном файле strstream.»
[Советы]
Строковые потоки используют ту же семантику, что и соответствующие базовые классы для файлов, как видно из приведённой далее демонстрационной программы.
/* StringStream — чтение и разбор содержимого файла */
#include
#include
#include
using namespace std ;
/* parseAccountInfo — чтение переданного */
/* буфера как если бы */
/* это был файл. */
/* Формат: имя, счёт, баланс. */
/* При корректной работе */
/* возвращает true. */
bool parseString( char* pString ,
char* pName ,
int arraySize ,
long& accountNum ,
double& balance )
{
/* Связывает объект istringstream с входной строкой */
istringstream inp( pString ) ;
/* Чтение до разделяющей запятой */
inp.getline( pName , arraySize , ',' ) ;
// Номер счёта
inp >> accountNum ;
// и его баланс
inp >> balance ;
/* Возврат состояния ошибки */
return !inp.fail( ) ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
setlocale ( LC_ALL ,".1251" ) ; /* печать русских текстов */
/* Файловый поток */
ifstream* pFileStream = new ifstream( "Accounts.txt" ) ;
if ( !pFileStream -> good( ) )
{
cout << "Невозможно открыть Accounts.txt" << endl ;
return 0 ;
}
/* Считываем строку файла, разбираем и выводим результат */
_________________
285 стр. Глава 24. Использование потоков ввода-вывода
for ( ; ; )
{
/* Добавляем разделитель */
cout << "=================" << endl ;
/* Читаем в буфер */
char buffer[ 256 ] ;
pFileStream -> getline( buffer , 256 ) ;
if ( pFileStream -> fail( ) )
{
break ;
}
/* Разбираем ввод на поля */
char name[ 80 ] ;
long accountNum ;
double balance ;
bool result = parseString( buffer , name , 80 ,
accountNum , balance ) ;
/* Вывод результата */
cout << buffer << "\n" ;
if ( result == false )
{
cout << "Ошибка разбора строки\n" ;
continue ;
}
cout << "Имя = " << name << ","
<< "Счёт = " << accountNum << ", "
<< "Баланс = " << balance << endl ;
/* Выводим поля в другом порядке ( вставка ends гарантирует нуль-завершённость буфера ) */
ostringstream out ;
out << name << ", "
<< balance << " "
<< accountNum << ends ;
/* Вывод результата. Класс istringstream работает и с классом string, но пока что мы не будем использовать эту возможность */
string oString = out.str( ) ;
cout << oString << "\n" << endl ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Программа начинает работу с открытия файла Accounts.txt, содержащего информацию в формате
Имя, номер счёта, баланс, \n
В предположении, что файл открыт успешно, программа входит в цикл, считывающий файл построчно до его полного прочтения. Строки считываются при помощи функции-члена getline( ), который читает их до символа новой строки. Затем считанная строка передаётся функции parseString( ).
_________________
286 стр. Часть 5. Полезные особенности
Функция parseString( ) связывает с символьной строкой объект istringstream. Программа считывает символы из строки до достижения символа '.' ( или конца строки ) при помощи функции-члена getline( ), после чего используются обычные операторы извлечения из потока для чтения номера счёта и баланса. Чтение выполнено успешно, если inp.fail( ) возвращает false.
После вызова функции parseString( ) выполняется вывод в буфер обработанной информации. Результат работы программы выглядит следующим образом.
================
Chester, 12345 56.60
Имя = Chester,Счёт = 12345, Баланс = 56.6
Chester, 56.6 12345
================
Arthur, 34567 67.50
Имя = Arthur,Счёт = 34567, Баланс = 67.5
Arthur, 67.5 34567
================
Trudie, 56x78 78.90
Ошибка разбора строки
================
Valerie, 78901 89.10
Имя = Valerie,Счёт = 78901, Баланс = 89.1
Valerie, 89.1 78901
===============
Press any key to continue...
Обратите внимание, как программа способна восстановиться после ошибок во входном файле. Оцените также простоту функции parseString( ), использующей возможности класса istringstream.
Обычно потоки ввода-вывода для выведения чисел и символов используют формат по умолчанию, который оказывается вполне подходящим для решения большинства задач.
Например, мне совсем не нравится, когда общая сумма в моей любимой финансовой программе выводится как 249.600006 вместо ожидаемого 249.6 ( а ещё лучше — 249.60 ). Необходимо каким-то образом указать программе количество выводимых цифр после десятичной точки. И такой способ есть; более того, в С++ он не единственный.
«В зависимости от установок по умолчанию вашего компилятора, вы можете увидеть на экране 249.6. Однако хотелось бы добиться того, чтобы выводилось именно 249.60.»
[Атас!]
Во-первых, форматом можно управлять с помощью серии функций-членов объекта потока. Например, количество разрядов для отображения можно установить, используя функцию precision( ):
#include
void fn( float interest , float dollarAmount )
{
cout << "Сумма в долларах = " ;
cout.precision( 2 ) ;
cout << dollarAmount ;
cout.precision( 4 ) ;
cout << interest
<< "\n" ;
}
_________________
287 стр. Глава 24. Использование потоков ввода-вывода
В этом примере с помощью функции precision( ) вывод значения dollarAmount устанавливается с точностью двух знаков после запятой. Благодаря этому вы можете увидеть на экране число 249.60 — именно то, что требовалось. Затем устанавливается вывод процентов с точностью четырёх знаков после запятой.
Второй путь связан с использованием так называемых манипуляторов. ( Звучит страшновато, не так ли? ) Манипуляторы — это объекты, определённые в заголовочном файле iomanip.h, которые приводят к тому же эффекту, что и описанные выше функции-члены ( чтобы иметь возможность пользоваться манипуляторами, вы должны не забыть включить iomanip.h в программу ). Единственное преимущество манипуляторов в том, что программа может включать их прямо в поток, не прибегая к вызову отдельной функции.
Если вы перепишете предыдущий пример так, чтобы в нём использовались манипуляторы, программа будет иметь следующий вид:
#include
#include
void fn( float interest , float dollarAmount )
{
cout << "Сумма в долларах = " ;
<< setprecision( 2 ) << dollarAmount
<< setprecision( 4 ) << interest
<< "\n" ;
}
Наиболее распространённые манипуляторы и их назначение приведены в табл. 24.4.
Таблица 24.4. Основные манипуляторы и функции управления форматом потока
_________________
Манипулятор — Функция-член — Описание
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
dec — flags( 10 ) — Перейти в десятичную систему счисления
hex — flags( 16 ) — Перейти в шестнадцатеричную систему счисления
oct — flags( 8 ) — Перейти в восьмеричную систему счисления
setfill( с ) — fill( c ) Установить символ заполнения с
setprecision( с ) — precision( с ) — Установить количество отображаемых знаков после запятой в с
setw( n ) — width( n ) — Установить ширину поля равной n символов*
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
_________________
*Примечание: это значение воздействует на вывод одного поля, после чего происходит возврат к значению по умолчанию.
«Внимательно следите за параметром ширины поля ( функция width( n ) либо манипулятор setw( n ) ). Большинство параметров сохраняют своё значение до тех пор, пока оно не будет изменено новым вызовом, однако для параметра ширины поля это не так. Этот параметр возвращается к значению по умолчанию, как только будет выполнен следующий вывод в поток. Например, приведённый ниже фрагмент кода не выведет два целочисленных значения длиной в 8 символов.»
[Атас!]
_________________
288 стр. Часть 5. Полезные особенности
#include
#include
void fn( )
{
cout << setw( 8 ) /* ширина поля равна 8... */
<< 10 /* ...для 10 , но... */
<< 20 /* для 20 равна значению по умолчанию */
<< "\n" ;
}
В результате выполнения этого кода сначала будет выведено восьмисимвольное целое число, а за ним — двухсимвольное. Для вывода двух восьмисимвольных значений нужно сделать так:
#include
#include
void fn( )
{
cout << setw( 8 ) /* установить ширину... */
<< 10
<< setw( 8 )
<< 20 /* ...обновить её */
<< "\n" ;
}
Таким образом, если вам нужно вывести несколько значений, но вас не устраивает длина поля по умолчанию, для каждого значения необходимо включать в вывод манипулятор setw( ).
Какой же метод лучше — с использованием манипуляторов или функций? Функции-члены предоставляют больше контроля над свойствами потока — хотя бы потому, что их больше. Кроме того, функции-члены обязательно возвращают предыдущее значение изменяемого параметра, так что у вас всегда есть возможность восстановить прежнее значение параметра. И наконец, существуют версии этих функций, позволяющие узнать текущее значение параметра, не изменяя его. Использование этой возможности показано в приведённом ниже примере.
#include
void fn( float value )
{
int previousPrecision ;
/* Вы можете узнать текущую точность так: */
previousPrecision = cout.precision( ) ;
/* Можно также сохранить старое значение, одновременно изменяя его на новое */
previousPrecision = cout.precision( 2 ) ;
cout << value ;
/* Восстановим предыдущее значение */
cout.precision( previousPrecision ) ;
}
Несмотря на все преимущества "функционального" подхода, манипуляторы более распространены; возможно, это просто потому, что они "круче" выглядят. Используйте то, что вам больше нравится, но в чужом коде будьте готовы увидеть оба варианта.
_________________
289 стр. Глава 24. Использование потоков ввода-вывода
В этой главе...
►Зачем нужен новый механизм обработки ошибок 291
►Механизм исключительных ситуаций 293
►Так что же мы будем бросать? 295
Трудно с этим смириться, но факт остаётся фактом: иногда функции работают неправильно. Традиционно вызывающей программе сообщается об ошибке посредством возвращаемого функцией значения. Однако язык С++ предоставляет новый, улучшенный механизм выявления и обработки ошибок с помощью исключительных ситуаций, или просто исключений ( exceptions ). Исключение — это отступление от общего правила, т.е. случай, когда то или иное правило либо принцип неприменимы. Можно дать и такое определение: исключение — это неожиданное ( и, надо полагать, нежелательное ) состояние, которое возникает во время выполнения программы.
Механизм исключительных ситуаций базируется на ключевых словах try ( попытаться, пробовать, попытка — [trai] — [трай] ), throw ( бросить, бросание, бросок — [θrou] — [сроу] ) и catch ( поймать, схватить, ловить — [kætʃ] — [кэчь] ). В общих чертах этот механизм работает так: функция пытается ( пробует — try ) выполнить фрагмент кода. Если в коде содержится ошибка, функция бросает ( генерирует — throw ) сообщение об ошибке, которое должна поймать ( перехватить — catch ) вызывающая функция. Это продемонстрировано в приведённой ниже программе.
//
/* FactorialException — демонстрация исключений */
/* при использовании факториала */
//
#include
#include
#include
#include
using namespace std ;
/* factorial — вычисление факториала */
int factorial( int n )
{
/* Функция не обрабатывает отрицательные значения аргумента */
if ( n < 0 )
{
throw string( "Аргумент отрицателен" ) ;
}
/* Вычисляем факториал */
int accum = 1 ;
while ( n > 0 )
{
accum *= n ;
_________________
290 стр. Часть 5. Полезные особенности
n-- ;
}
return accum ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
try
{
/* Здесь всё в порядке */
cout << "3! = " << factorial( 3 ) << endl ;
/* Здесь генерируется исключение */
cout << "-1!= " << factorial( -1 ) << endl ;
/* Этот код так и остаётся не выполнен */
cout << "Factorial of 5 is " << factorial( 5 )
<< endl ;
}
/* Обработка исключения */
catch( string error )
{
cout << "Ошибка: " << error << endl ;
}
catch ( ... )
{
cout << "Неизвестное исключение" << endl ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Функция main( ) начинается с блока, выделенного ключевым словом try. В этом блоке выполнение кода ничем не отличается от выполнения вне блока. В данном случае main( ) пытается вычислить факториал отрицательного числа. Однако функцию factorial( ) не так легко одурачить, поскольку она достаточно умно написана и обнаруживает, что запрос некорректен. При этом она генерирует сообщение об ошибке с помощью ключевого слова throw. Управление передаётся фрагменту, находящемуся сразу за закрывающей фигурной скобкой блока try и отвечающему за перехват сообщения об ошибке. Следующий за ошибочным вызов функции factorial так и не выполняется.
Что плохого в методе возврата ошибки, подобном применяемому в FORTRAN? Факториал не может быть отрицательным, поэтому я мог бы сказать что-то вроде: "Если функция factorial( ) обнаруживает ошибку, она возвращает отрицательное число. Значение отрицательного числа будет указывать на источник проблемы". Чем же плох такой метод? Ведь так было всегда.
_________________
291 стр. Глава 25. Обработка ошибок и исключения
К сожалению, здесь возникает несколько проблем. Во-первых, хотя результат факториала не может быть отрицательным, другим функциям повезло гораздо меньше. Например, вы не можете взять логарифм от отрицательного числа, но сам логарифм может быть как отрицательным, так и положительным, а поэтому возврат отрицательного числа не обязательно будет означать ошибку.
Во-вторых, в целочисленной переменной не передашь много информации. Можно, конечно, обозначить ситуацию "аргумент отрицательный" как -1, ситуацию "аргумент слишком большой" как -2 и т.д. Но если аргумент слишком большой, я хотел бы знать, какой именно, поскольку это поможет мне найти источник проблемы. Однако в целочисленной переменной такую информацию не сохранишь.
В-третьих, проверка возвращаемого значения вовсе не обязательна. Чтобы понять, что я имею в виду, представьте себе, что кто-то написал функцию factorial( ), которая послушно проверяет, находится ли её аргумент в допустимых границах. Однако, если код, вызвавший эту функцию, не будет проверять возвращаемое значение, это не приведёт ни к чему хорошему. Конечно, я мог бы ввести в функцию всякие страшные угрозы наподобие "Вы обязательно должны проверить сообщения об ошибках, иначе...", но, думаю, не стоит объяснять, что язык не может никого ни к чему принудить.
Даже если моя функция проверяет наличие ошибки в factorial( ) или любой другой функции, что она может с ней сделать? Пожалуй, только вывести сообщение об ошибке ( которое я сам написал ) и вернуть другое значение, указывающее на наличие ошибки, вызывающей функцию, которая, скорее всего, повторит весь этот процесс. В результате программа будет переполнена кодом, подобным приведённому ниже.
errRtn = someFunc( ) ;
if ( errRtn )
{
errorOut( "Ошибка при вызове someFn( )" ) ;
return MY_ERROR_1
}
errRtn = someOtherFunc( ) ;
if ( errRtn )
{
errorOut( "Ошибка при вызове someOtherFn( )" ) ;
return MY_ERROR_1
}
Такой механизм имеет ряд недостатков.
■■■
■ Изобилует повторениями.
■ Заставляет программиста отслеживать множество разных ошибок и писать код для обработки всех возможных вариантов.
■ Смешивает код, отвечающий за обработку ошибок, с обычным кодом, что не добавляет ясности программе.
■■■
Эти недостатки кажутся безобидными в простом примере, но могут превратиться в большие проблемы, когда программа станет более сложной. В результате такой подход приводит к тому, что обработкой ошибок занимается 90% кода.
Механизм исключительных ситуаций позволяет обойти эти проблемы, отделяя код обработки ошибок от обычного кода. Кроме того, наличие исключений делает обработку ошибок обязательной. Если ваша функция не обрабатывает сгенерированное исключение, управление передаётся далее по цепочке вызывающих функций, пока С++ не найдёт функцию, которая обработает возникшую проблему. Это также даёт возможность игнорировать ошибки, которые вы не в состоянии обработать.
_________________
292 стр. Часть 5. Полезные особенности
Познакомимся поближе с тем, как программа обрабатывает исключительную ситуацию. При возникновении исключения ( throw ) С++ первым делом копирует сгенерированный объект в некоторое нейтральное место. После этого просматривается конец текущего блока try.
Если блок try в данной функции не найден, управление передаётся вызывающей функции, где и осуществляется поиск обработчика. Если и здесь не найден блок try, поиск повторяется далее, вверх по стеку вызывающих функций. Этот процесс называется разворачиванием стека.
Важная особенность разворачивания стека состоит в том, что на каждом его этапе все объекты, которые выходят из области видимости, уничтожаются так же, как если бы функция выполнила команду return. Это оберегает программу от потери ресурсов и "праздно шатающихся" неуничтоженных объектов.
Когда необходимый блок try найден, программа ищет первый блок catch ( который должен находиться сразу за закрывающей скобкой блока try ). Если тип сгенерированного объекта совпадает с типом аргумента, указанным в блоке catch, управление передаётся этому блоку; если же нет, проверяется следующий блок catch. Если в результате подходящий блок не найден, программа продолжает поиск уровнем выше, пока не будет обнаружен необходимый блок catch. Если искомый блок не обнаружен, программа аварийно завершается. Рассмотрим приведённый ниже пример.
/* CascadingException — при компиляции данная программа */
/* может вызвать предупреждения о */
/* том, что переменные f, i и pMsg */
/* не используются */
#include
#include
#include
#include
using namespace std ;
class Obj
{
public :
Obj( char c )
{
label = c ;
cout << "Конструирование объекта " << label << endl ;
}
~Obj ( )
{
cout << "Деструкция объекта " << label << endl ;
}
protected :
char label ;
} ;
void f1( ) ;
void f2( ) ;
int f3( )
{
Obj a( 'a' ) ;
try
{
_________________
293 стр. Глава 25. Обработка ошибок и исключения
Obj b( 'b' ) ;
f1( ) ;
}
catch( float f )
{
cout << "Перехват float" << endl ;
}
catch( int i )
{
cout << "Перехват int" << endl ;
}
catch ( ... )
{
cout << string( "Обобщённое исключение" ) << endl ;
}
return 0;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
f3( ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
void f1( )
{
try
{
Obj c( 'c' ) ;
f2( ) ;
}
catch( string msg )
{
cout << "Перехват строки" << endl ;
}
}
void f2( )
{
Obj d( 'd' ) ;
throw 10 ;
}
В результате работы этой программы на экран будет выведен следующий текст:
Конструирование объекта а
Конструирование объекта b
Конструирование объекта с
Конструирование объекта d
Деструкция объекта d
Деструкция объекта с
Деструкция объекта b
Перехват int
Деструкция объекта а
Press any key to continue...
_________________
294 стр. Часть 5. Полезные особенности
Как видите, прежде чем в функции f2( ) происходит исключение int, конструируются четыре объекта — а, b, с и d. Поскольку в f2( ) блок try не определён, С++ разворачивает стек вызовов функций, что приводит к ликвидации объекта d при разворачивании стека f2( ). В функции f1( ) определён блок try, но его блок catch воспринимает только char*, что не совпадает с брошенным объектом int. Поэтому С++ продолжает просмотр, что приводит к разворачиванию стека функции f1( ) ( при этом ликвидируется объект с ).
В функции f3( ) С++ находит ещё один блок try. Выход из этого блока приводит к выходу из области видимости объекта и b. Первый за блоком try блок catch принимает float, что вновь не совпадает с нашим int, поэтому пропускается и этот блок. Однако следующий блок catch наконец-то воспринимает int, и управление переходит к нему. Последний блок catch, который воспринимает любой объект, пропускается, поскольку необходимый блок catch уже найден и исключение обработано.
За ключевым словом throw следует выражение, которое создаёт объект некоторого типа. В приведённых здесь примерах мы генерировали переменные типа int, но на самом деле ключевое слово throw работает с любым типом объекта. Это значит, что вы можете "бросать" любое количество информации. Рассмотрим приведённое ниже определение класса.
//
/* CustomExceptionClass — демонстрация исключений */
/* при использовании факториала */
#include
#include
#include
#include
using namespace std ;
/* Exception — обобщённый класс исключения */
class Exception
{
public :
Exception( char* pMsg , int n , char* pFile , int nLine )
: msg( pMsg ) , errorValue( n ) , file( pFile ) , lineNum( nLine )
{ }
virtual string display( )
{
ostringstream out ;
out << "Ошибка <" << msg
<< " - значение равно " << errorValue
<< ">\n" ;
out << "@" << file << "-" << lineNum << endl ;
return out.str( ) ;
}
protected :
/* Сообщение об ошибке */
string msg ;
int errorValue ;
/* Имя файла и номер строки, где произошла ошибка */
string file ;
_________________
295 стр. Глава 25. Обработка ошибок и исключения
int lineNum ;
} ;
/* factorial — вычисление факториала */
int factorial( int n )
{
/* Функция не обрабатывает отрицательные значения аргумента */
if ( n < 0 )
{
throw Exception( "Аргумент факториала отрицателен" ,
n , __FILE__ , __LINE__ ) ;
}
/* Вычисляем факториал */
int accum = 1 ;
while ( n > 0 )
{
accum *= n ;
n-- ;
}
return accum ;
}
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
try
{
/* Этот код работает корректно */
cout << "3! = " << factorial( 3 ) << endl ;
/* Здесь генерируется исключение */
cout << "Factorial of -1 is " << factorial( -1 ) << endl ;
/* Этот код остаётся невыполненным */
cout << "Factorial of 5 is " << factorial( 5 ) << endl ;
}
/* Перехват исключения */
catch( Exception e )
{
cout << "Ошибка: \n" << e.display( ) << endl ;
}
catch ( ... )
{
cout << "Неизвестное исключение" << endl ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Программа выглядит практически идентичной программе вычисления факториала, приведённой в начале главы. Отличие заключается в том, что здесь для генерации исключения применяется пользовательский класс Exception, который содержит существенно большее количество информации о происшедшей ошибке: сообщение об ошибке, неверный аргумент и точное место, где произошла ошибка.
_________________
296 стр. Часть 5. Полезные особенности
«Встроенные макроопределения _FILE_ и _LINE_ представляют собой имя исходного файла и текущую строку в нём.»
[Советы]
Перехватчик исключения использует функцию-член display( ) для вывода сообщения об ошибке. Вывод программы выглядит следующим образом.
3! = 6
Ошибка:
Ошибка <Аргумент факториала отрицателен — значение равно -1> @С:/Documents/Dial/CppDummy/CustomExceptionClass.срр-46
Press any key to continue...
Класс Exception представляет собой базовый класс для сообщений об ошибках. Вы можете наследовать его и получить класс, который обеспечит более детальную информацию об ошибке. Например, я могу определить класс InvalidArgumentException, который в дополнение к прочему хранит значение неверного аргумента.
class InvalidArgumentException : public Exception
{
public :
InvalidArgumentException( int arg , char*pFile , int nLine )
: Exception( "Некорректный аргумент" , pFile , nLine )
{
invArg = arg ;
}
virtual void display( ostream& out )
{
Exception::display( out ) ;
out << "Аргумент " << invArg << endl ;
}
protected :
int invArg ;
}
Вызывающая функция автоматически обработает новое исключение, поскольку InvalidArgumentException ЯВЛЯЕТСЯ Exception, а функция-член display( ) — полиморфна.
_________________
297 стр. Глава 25. Обработка ошибок и исключения
В этой главе...
►Механизм множественного наследования 298
►Устранение неоднозначностей множественного наследования 300
►Отрицательные стороны множественного наследования 306
В иерархиях классов, которые рассматривались в этой книге, каждый класс наследовался от одного прародителя. Такое одиночное наследование подходит для описания большинства объектов реального мира. Однако некоторые классы представляют собой сочетание нескольких классов в одном.
Примером такого класса может служить диван-кровать. Как видно из названия, это и диван, и кровать ( правда, кровать не очень удобная ). Таким образом, этот предмет интерьера наследует свойства как дивана, так и кровати. В терминалах С++ эту ситуацию можно описать следующим образом: класс может быть наследником более чем одного базового класса. Такое наследование называется множественным.
Чтобы увидеть множественное наследование в действии, я продолжу пример с диваном-кроватью. На рис. 26.1 приведена схема наследования дивана-кровати ( класс SleeperSofa ). Обратите внимание, что этот класс наследует свойства и от класса Bed ( Кровать ), и от класса Sofa ( Диван ), т.е. наследует свойства обоих классов.
Рис. 26.1. Иерархия классов дивана-кровати
_________________
298 стр. Часть 5. Полезные особенности
Программная реализация класса SleeperSofa выглядит следующим образом.
//
/* MultipleInheritance — класс, являющийся наследником */
/* нескольких базовых классов */
//
#include
#include
#include
using namespace std ;
class Bed
{
public :
Bed( ) { }
void sleep( ) { cout << "Спим" << endl ; }
int weight ;
} ;
class Sofa
{
public :
Sofa( ) { }
void watchTV( ) { cout << "Смотрим телевизор" << endl ; }
int weight ;
} ;
/* SleeperSofa — диван-кровать */
class SleeperSofa : public Bed , public Sofa
{
public :
SleeperSofa( ) { }
void foldOut( ) { cout << "Раскладываем диван-кровать"
<< endl ; }
} ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
SleeperSofa ss ;
/* Посмотрим телевизор на диване... */
ss.watchTV( ) ; /* Sofa::watchTV( ) */
/* ...разложим его в кровать... */
ss.foldOut( ) ; /* SleeperSofa::foldOut( ) */
/* ...и ляжем спать */
ss.sleep( ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
В этом примере класс SleeperSofa наследует оба класса — Bed и Sofa. Это видно из их наличия в объявлении класса SleeperSofa, который наследует все члены от обоих базовых классов. Таким образом, допустимы оба вызова — как ss.sleep( ), так и ss.watchTV( ). Вы можете использовать SleeperSofa и как Bed, и как Sofa. Кроме того, класс SleeperSofa имеет собственные члены, например foldOut( ). В результате мы получим следующий вывод программы:
Смотрим телевизор
Раскладываем диван-кровать
Спим
Press any key to continue...
_________________
299 стр. Глава 26. Множественное наследование
Будучи весьма мощной возможностью языка, множественное наследование может стать в то же время и источником проблем. Одну из них можно увидеть уже в предыдущем примере. Обратите внимание, что оба класса — Bed и Sofa — содержат член weight ( вес ). Это логично, потому что они оба имеют некоторый вполне измеримый вес. Вопрос: какой именно член weight наследует класс SleeperSofa?
Ответ прост: оба. Класс SleeperSofa наследует отдельный член Bed::weight и отдельный член Sofa::weight. Поскольку они оба имеют одно и то же имя, обращения к weight теперь являются двузначными, если только не указывать явно, к какому именно weight мы намерены обратиться. Это демонстрирует следующий фрагмент кода:
#include
void fn( )
{
SleeperSofa ss ;
cout << "Beс = "
<< ss.weight /* неправильно — какой именно вес? */
<< "\n" ;
}
Теперь в программе нужно явно указывать, какая именно переменная weight нужна, используя для этого имя базового класса. Приведённый ниже пример вполне корректен.
#include
void fn( )
{
SleeperSofa ss ;
cout << "Вес дивана = "
<< ss.Sofa::weight /*укажем, какой именно вес */
<< "\n" ;
}
Хотя такое решение и устраняет ошибку, указание имени базового класса во внешнем приложении нежелательно: ведь при этом информация о внутреннем устройстве класса должна присутствовать за его пределами. В нашем примере функция fn( ) должна располагать сведениями о том, что класс SleeperSofa наследуется от класса Sofa. Такие конфликты имён невозможны при одиночном наследовании, но служат постоянным источником неприятностей при наследовании множественном.
_________________
300 стр. Часть 5. Полезные особенности
В случае класса SleeperSofa конфликт имён weight является, по сути, небольшим недоразумением. Ведь на самом деле диван-кровать не имеет отдельного веса как кровать, и отдельного веса как диван. Конфликт возник потому, что такая иерархия классов не вполне адекватно описывает реальный мир. Дело в том, что разложение на классы оказалось неполным.
Если немного подумать над этой проблемой, становится ясно, что и кровать и диван являются частными случаями некоторой более фундаментальной концепции мебели ( думаю, можно было предложить нечто ещё более фундаментальное, но для нас достаточно ограничиться мебелью ). Вес является свойством любой мебели, что показано на рис. 26.2.
Рис. 26.2. Выделение общих свойств кровати и дивана
«Если отделить класс Furniture ( мебель ), конфликт имён будет устранен. Итак, с чувством глубокого удовлетворения и облегчения, в предвкушении успеха реализуем новую иерархию классов в программе MultipleInheritanceFactoring, которую вы можете найти на прилагаемом компакт-диске:»
[Диск]
//
/* MultipleInheritanceFactoring — класс, являющийся */
/* наследником нескольких */
/* базовых классов */
//
#include
#include
#include
using namespace std ;
/* Furniture — фундаментальная концепция, обладающая весом */
class Furniture
{
_________________
301 стр. Глава 26. Множественное наследование
public :
Furniture( int w ) : weight( w ) { }
int weight ;
} ;
class Bed : public Furniture
{
public :
Bed( int weight ) : Furniture( weight ) { }
void sleep( ) { cout << "Спим" << endl ; }
} ;
class Sofa : public Furniture
{
public :
Sofa( int weight ) : Furniture( weight ) { }
void watchTV( ) { cout << "Смотрим телевизор" << endl ; }
} ;
/* SleeperSofa — диван-кровать */
class SleeperSofa : public Bed , public Sofa
{
public :
SleeperSofa( int weight ) : Sofa( weight ) , Bed( weight ) { }
void foldOut( ) { cout << "Раскладываем диван-кровать"
<< endl ; }
} ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
SleeperSofa ss( 10 ) ;
/* Section 1 — неоднозначность: Furniture::Sofa или Furniture::Bed? */
/*
cout << "Beс = "
<< ss.weight
<< endl ;
*/
/* Section 2 — Один из способов устранения неоднозначности */
SleeperSofa* pSS = &ss ;
Sofa* pSofa = ( Sofa* )pSS ;
Furniture* pFurniture = ( Furniture* )pSofa ;
cout << "Beс = "
<< pFurniture -> weight
<< endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
302 стр. Часть 5. Полезные особенности
М-да... "Не говори "гоп", пока не переехал Чоп" — новая иерархия классов совершенно нас не спасает, weight остаётся неоднозначным. Попробуем привести ss к классу Furniture.
#include
void fn( )
{
SleeperSofa ss ;
Furniture* pF ;
pF = ( Furniture* )&ss ;
cout << "Beс = "
<< pF -> weight
<< "\n" ;
} ;
Приведение ss к классу Furniture тоже ничего не даёт. Более того, я получил какое-то подозрительное сообщение о том, что приведение SleeperSofa* к классу Furniture* неоднозначно. Да что, в конце концов, творится?
На самом деле всё довольно просто. Класс SleeperSofa не наследуется напрямую от класса Furniture. Сначала Furniture наследуют классы Bed и Sofa, а уж потом SleeperSofa наследуется от этих классов. Класс SleeperSofa выглядит в памяти так, как показано на рис. 26.3.
Рис. 26.3. Расположение класса SleeperSofa в памяти
Как видите, SleeperSofa состоит из класса Bed, за которым в полном составе следует класс Sofa, а после него — уникальные члены класса SleeperSofa. Каждый из подобъектов класса SleeperSofa имеет свою собственную часть Furniture, поскольку они оба наследуются от этого класса. В результате объекты класса SleeperSofa содержат два объекта класса Furniture.
Таким образом, становится ясно, что я не сумел создать иерархию, показанную на рис. 26.2. Иерархия наследования, которая была создана в результате выполнения предыдущей программы, показана на рис. 26.4.
SleeperSofa содержит два объекта класса Furniture — явная бессмыслица! Необходимо, чтобы SleeperSofa наследовал только одну копию Furniture и чтобы Bed и Sofa имели к ней доступ. В С++ это достигается виртуальным наследованием, поскольку в этом случае используется ключевое слово virtual.
_________________
303 стр. Глава 26. Множественное наследование
Рис. 26.4. Результат попытки создания иерархии классов
«В данном случае произошло смешение терминов, однако необходимо принять к сведению, что виртуальное наследование не имеет ничего общего с виртуальными функциями!»
[Советы]
Вооружённый новыми знаниями, я возвращаюсь к классу SleeperSofa и реализую его так, как показано ниже.
//
/* VirtualInheritance — виртуальное */
/* наследование позволяет */
/* классам Bed и Sofa использовать */
/* общий базовый класс */
//
#include
#include
#include
using namespace std ;
/* Furniture — фундаментальная концепция, обладающая весом */
class Furniture
{
public :
Furniture( int w = 0 ) : weight( w ) { }
int weight ;
} ;
class Bed : virtual public Furniture
{
public :
Bed( ) { }
void sleep( ) { cout << "Спим" << endl ; }
} ;
class Sofa : virtual public Furniture
_________________
304 стр. Часть 5. Полезные особенности
{
public :
Sofa( ) { }
void watchTV( ) { cout << "Смотрим телевизор" << endl ; }
} ;
/* SleeperSofa — диван-кровать */
class SleeperSofa : public Bed , public Sofa
{
public :
SleeperSofa( int weight ) : Furniture( weight ) { }
void foldOut( ) { cout << "Раскладываем диван-кровать"
<< endl ; }
} ;
int main( int nNumberofArgs , char* pszArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
SleeperSofa ss( 10 ) ;
/* Section 1 — неоднозначности больше нет, есть только один вес */
cout << "Вес = "
<< ss.weight
<< endl ;
/* Section 2 — Один из способов устранения неоднозначности */
SleeperSofa* pSS = &ss ;
Sofa* pSofa = ( Sofa* )pSS ;
Furniture* pFurniture = ( Furniture* )pSofa ;
cout << "Bec = "
<< pFurniture -> weight
<< endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Обратите внимание на ключевое слово virtual, используемое при наследовании классов Bed и Sofa от класса Furniture. Оно означает примерно следующее: "Дайте-ка мне копию Furniture, но если она уже существует, то я использую именно её". В итоге класс SleeperSofa будет выглядеть, как показано на рис. 26.5.
Из этого рисунка видно, что класс SleeperSofa включает Furniture, а также части классов Bed и Sofa, не содержащие Furniture. Далее находятся уникальные для класса SleeperSofa члены ( элементы в памяти не обязательно будут располагаться именно в таком порядке, но в данном обсуждении это несущественно ).
Теперь обращение к члену weight в функции fn( ) не многозначно, поскольку SleeperSofa содержит только одну копию Furniture. Наследуя этот класс виртуально, мы получили желаемую структуру наследования ( см. рис. 26.2 ).
_________________
305 стр. Глава 26. Множественное наследование
Если виртуальное наследование так хорошо решает проблему неоднозначности, почему оно не является нормой? Во-первых, потому, что виртуально наследуемый класс обрабатывается иначе, чем обычный наследуемый базовый класс, что, в частности, выражается в повышенных накладных расходах. Во-вторых, у вас может появиться желание иметь две копии базового класса ( хотя это случается весьма редко ). Вспомним наши старые упражнения со студентами и преподавателями и допустим, что TeacherAssistant ( помощник преподавателя ) является одновременно и Teacher ( преподавателем ) и Student ( студентом ), которые, в свою очередь, являются подклассами Academician. Если университет даст помощнику преподавателя два идентификатора — и студента и преподавателя, то классу TeacherAssistant понадобятся две копии класса Academician.
Рис. 26.5. Расположение класса SleeperSofa в памяти при использовании виртуального наследования
При конструировании объектов с использованием множественного наследования должен выполняться ряд правил.
20. Сначала вызываются конструкторы для каждого виртуального базового класса в порядке наследования.
21. Затем вызываются конструкторы каждого невиртуального базового класса в порядке наследования.
22. После этого вызываются конструкторы всех объектов-членов класса в том порядке, в котором эти объекты-члены объявлены в классе.
23. И наконец, вызывается конструктор самого класса.
Обратите внимание, что базовые классы конструируются в порядке наследования, а не в порядке расположения в строке конструктора.
Должен признаться, что не все, кто работает с объектно-ориентированным программированием, считают механизм множественного наследования удачным. Кроме того, многие объектно-ориентированные языки вообще не поддерживают множественного наследования, реализация которого, кстати, далеко не самая простая вещь. Конечно, множественное наследование — это проблема компилятора ( вернее, того, кто пишет компилятор ). Однако оно требует больших накладных расходов по сравнению с программой с обычным наследованием, а эти накладные расходы становятся уже проблемой программиста.
_________________
306 стр. Часть 5. Полезные особенности
Не менее важно и то, что множественное наследование открывает путь к дополнительным ошибкам. Во-первых, неоднозначность, подобная описанной в предыдущем разделе, может превратиться в большую проблему. Во-вторых, при наличии множественного наследования преобразования указателя на подкласс в указатель на базовый класс иногда приводят к запутанным и непонятным изменениям этого указателя. Все эти тонкости я оставляю на совести разработчиков языка и компилятора.
Думаю, вам стоит избегать множественного наследования, пока вы в полной мере не освоите С++. Обычное наследование тоже достаточно мощный механизм. Исключением может стать библиотека Microsoft Foundation Classes ( MFC ), в которой множественное наследование используется сплошь и рядом. Однако эти классы тщательно выверены профессиональными высококвалифицированными программистами.
Только не поймите меня неправильно! Я не против множественного наследования. То, что Microsoft и другие компании эффективно используют множественное наследование в своих классах, доказывает, что так можно делать. Если бы этот механизм не стоил того, они бы его не использовали. Однако это отнюдь не значит, что множественное наследование имеет смысл применять, едва познакомившись с ним.
_________________
307 стр. Глава 26. Множественное наследование
В этой главе...
►Обобщение функции в шаблон 309
►Шаблоны классов 311
►Зачем нужны шаблоны классов 314
►Советы по использованию шаблонов 316
Стандартная библиотека С++ предоставляет программисту множество различных функций. В ней представлены математические функции, функции для работы со временем и датами, функции ввода-вывода, системные функции. Во многих программах в этой книге использованы, например, функции для работы с нуль-завершёнными строками ( эти функции объявлены в заголовочном файле strings.h ). Типы аргументов большинства функций фиксированы. Так, например, оба аргумента функции strcpy( char* , const char* ) являются указателями на нуль-завершённые строки — любые другие типы аргументов для данной функции просто лишены смысла.
Есть функции, которые могут быть применены к различным типам данных. Рассмотрим, например, функцию maximum( ), которая возвращает больший из двух аргументов. Все объявления функции, приведённые в табл. 27.1, имеют смысл.
Таблица 27.1. Возможные варианты функции maximum( )
_________________
Имя функции — Выполняемые действия
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
maximum( int , int ) — Возвращает большее из двух целых чисел
maximum( unsigned int , unsigned int ) — Возвращает большее из двух беззнаковых целых чисел
maximum( double , double ) — Возвращает большее из двух чисел с плавающей точкой
maximum( char , char ) — Возвращает символ, находящийся далее в алфавитном порядке
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Я бы хотел реализовать функцию maximum( ) для всех четырёх случаев. Само собой, С++ может привести все эти типы к типу double, т.е. мне достаточно разработать функцию maximum ( double , double ), которая удовлетворит все мои потребности. Так ли это? Рассмотрим следующий код.
/* Прототип функции maximum */
double maximum( double , double ) ;
/* Пользовательская функция */
void fn ( int nArg1 , int nArg2 )
{
int nLarger = ( int )maximum( ( double )nArg1 ,
( double )nArg2 ) ;
/* ... прочие действия ... */
}
_________________
308 стр. Часть 5. Полезные особенности
В такой ситуации целочисленные параметры должны быть сначала приведены к типу double ( с потерей точности ). Функция возвращает значение double, которое теперь надо преобразовать к типу int. Функция может работать и без потери точности, но выполнение всех этих преобразований делает её куда более медленной, чем она могла бы быть. Словом, эта функция в любом случае работает не так, как ожидает ( или надеется ) пользователь.
Конечно, функцию maximum( ) можно просто перегрузить.
double maximum( double d1 , double d2 )
{
if ( d > d2 ) return d1 ;
return d2 ;
}
int maximum( int n1 , int n2 )
{
if ( n1 > n2 ) return n1 ;
return n2 ;
}
char maximum( char c1 , char c2 )
{
if ( c1 > c2 ) return c1 ;
return c2 ;
}
/* Определения функции для других типов */
Такой подход вполне работоспособен. Теперь С++ выберет наиболее подходящую функцию, а именно — maximum( int , int ). Однако создание одной и той же функции для переменных каждого типа требует массу времени.
Исходный код всех функций maximum( Т , Т ) следует одному и тому же шаблону для всех Т, представляющих числовые типы. Было бы удобно, если бы можно было написать функцию один раз и позволить С++ самостоятельно подставлять в неё нужные типы.
Шаблонная функция позволяет вам написать нечто, выглядящее как обычная функция, но в отличие от обычной, такая функция может использовать один или несколько фиктивных заменителей типов, которые С++ затем преобразует в реальные типы во время компиляции. Вот программа, в которой определяется шаблон обобщённой функции maximum( ).
/* MaxTemplate — шаблон функции maximum( ), возвращающей */
/* наибольшее значение из двух аргументов */
#include
#include
#include
using namespace std ;
template < class T >
T maximum( T t1 , T t2 )
{
if ( t1 > t2 )
{
return t1 ;
}
return t2 ;
} ;
int main( int argc , char* pArgs[ ] )
{
/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */
setlocale ( LC_ALL , ".1251" ) ;
/* Ищем максимум из двух int */
cout << "Максимум из 1 и 2 равен "
<< maximum( 1 , 2 )
<< endl ;
/* Ищем максимум из двух double */
cout << "Максимум из 1.5 и 2.5 равен "
<< maximum( 1.5 , 2.5 )
<< endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
309 стр. Глава 27. Шаблоны С++
Обратите внимание на ключевое слово template, за которым следуют угловые скобки — в скобках могут содержаться заменители типов, каждому из которых предшествует слово class, или константы. В нашем случае определение функции maximum( ) использует "неизвестный тип" Т. После угловых скобок идёт обычное определение функции, которая в нашем случае возвращает большее из двух значений типа Т — типа, который будет определён позже в программе.
Шаблон функции бесполезен до тех пор, пока он не преобразуется в реальную функцию, когда С++ заменяет Т ( для обозначения неизвестного типа могут использоваться любые идентификаторы, а не только Т ) реальным типом. В приведённой программе функция main( ) неявно заставляет С++ создать две версии функции maximum( ).
«Создание функции из шаблона называется его настройкой, или инстанцированием.»
[Советы]
Первый вызов maximum( 1 , 2 ) заставляет С++ создать версию функции, в которой Т заменяется на int. Второй вызов создаёт отдельную функцию maximum( double , double ). В результате вывод программы имеет следующий вид.
Максимум из 1 и 2 равен 2
Максимум из 1.5 и 2.5 равен 2.5
Press any key to continue...
«Будьте внимательны с терминологией. Шаблон функции не является функцией. Прототип шаблона функции — maximum< T >( Т , Т ), а функция, которую создаёт данный шаблон при замене Т на int, — maximum ( int , int ) ( это уже функция, а не шаблон ).»
[Атас!]
Заметим, что следующий код всё равно оказывается неработоспособным:
double d = maximum( 1 , 2.0 ) ;
_________________
310 стр. Часть 5. Полезные особенности
Проблема в том, что типы первого и второго аргументов различны, а при инстанцировании типы аргументов должны точно соответствовать объявлению функции. Приведённое же выражение может соответствовать шаблону maximum< T1 , Т2 > ( Т1 , Т2 ) ( тогда С++ заменит Т1 на int , а Т2 на double ), но не использовавшемуся ранее шаблону с одним аргументом типа.
Вы можете заставить С++ инстанцировать шаблон, использовав в программе объявление требуемой функции:
float maximum( float , float ) ; /* Заставляет С++ */
/* инстанцировать шаблон функции */
/* maximum< T >( Т , Т ) для Т = float */
«С++ даже не пытается компилировать шаблон функции до тех пор, пока шаблон не будет преобразован в реальную функцию. Если ваш шаблон содержит ошибки, вероятно, вы не узнаете о них до тех пор, пока не инстанцируете его.»
[Атас!]
С++ позволяет программисту определять шаблоны классов. Шаблон класса следует тем же принципам, что и использование обычного класса, с заменой фиктивного неизвестного типа известным на этапе компиляции. Например, в приведённой далее программе создаётся вектор некоторого пользовательского класса ( вектор — это контейнер, в котором объекты хранятся в линейном порядке, так что массив является классическим примером вектора ).
/* TemplateVector — реализация шаблона вектора */
#include
#include
#include
#include
#include
using namespace std ;
/* TemplateVector — простой шаблон массива */
template < class T >
class TemplateVector
{
public :
TemplateVector( int nArraySize )
{
/* Количество элементов массива */
nSize = nArraySize ;
array = new T[ nArraySize ] ;
reset( ) ;
}
int size( ) { return nWriteIndex ; }
void reset( ) { nWriteIndex = 0 ; nReadIndex = 0 ; }
void add( T object )
{
if ( nWriteIndex < nSize )
{
array[ nWriteIndex++ ] = object ;
}
}
T get( )
{
_________________
311 стр. Глава 27. Шаблоны С++
return array[ nReadIndex++ ] ;
}
protected :
int nSize ;
int nWriteIndex ;
int nReadIndex ;
T* array ;
} ;
/* Работа с двумя векторами — целых чисел и имён */
void intFn( ) ;
void nameFn( ) ;
int main( int argc , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
intFn( ) ;
nameFn( ) ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
/* Работа с целыми числами */
void intFn( )
{
/* Создание вектора */
TemplateVector< int > integers( 10 ) ;
/* Добавляем значения в вектор */
cout << "Введите последовательность целых чисел\n"
"для внесения в вектор ( отрицательное\n"
"число завершает ввод последовательности )"
<< endl ;
for ( ; ; )
{
int n ;
cin >> n ;
if ( n < 0 ) { break ; }
integers.add( n ) ;
}
cout << "\nВы ввели следующие числа" << endl ;
for ( int i = 0 ; i < integers.size( ) ; i++ )
{
cout << i << ":" << integers.get( ) << endl ;
}
}
/* Работа с именами */
class Name
{
public :
Name( char* n = " " ) : name( n ) { }
_________________
312 стр. Часть 5. Полезные особенности
string display( ) { return name ; }
protected :
string name ;
} ;
void nameFn( )
{
/* Создание вектора */
TemplateVector< Name > names( 10 ) ;
/* Добавление значений в вектор */
cout << "Введите имена\n"
<< "('х' для завершения ):" << endl ;
for ( ; ; )
{
char buffer[ 80 ] ;
do
{
cin.getline( buffer , 80 ) ;
} while ( strlen( buffer ) == 0 ) ;
if ( stricmp( buffer , "x" ) == 0 )
{
break ;
}
names.add( Name( buffer ) ) ;
}
cout << "\nВы ввели имена" << endl ;
for ( int i = 0 ; i < names.size( ) ; i++ )
{
Name name = names.get( ) ;
cout << i << ":" << name.display( ) << endl ;
}
}
Шаблон класса TemplateVector< T > содержит массив объектов класса Т. Шаблон класса имеет две функции-члена: add( ) и get( ). Первая из них добавляет объект типа Т в очередное пустое место массива, а вторая — возвращает следующий элемент из массива.
Приведённая программа инстанцирует этот шаблон сначала для типа int. а затем для пользовательского класса Name.
Функция intFn( ) создаёт вектор целых чисел с 10 элементами, после чего считывает вводимые пользователем числа в вектор, а потом выводит их на экран, используя функции, предоставляемые шаблоном TemplateVector.
Вторая функция, nameFn( ), создаёт вектор объектов типа Name. Функция так же размещает пользовательский ввод в векторе, а потом выводит его элементы на экран.
Обратите внимание, как шаблон TemplateVector позволяет с одинаковой простотой работать как со встроенным типом, так и с пользовательским классом. Вот как выглядит пример работы данной программы.
Введите последовательность целых чисел
для внесения в вектор ( отрицательное число
завершает ввод последовательности )
5
10
15
-1
Вы ввели следующие числа
0:5
1:10
2:15
Введите имена
('х' для завершения ):
Igor
Ira
Anton
x
Вы ввели имена
0: Igor
1: Ira
2 : Anton
Press any key to continue...
_________________
313 стр. Глава 27. Шаблоны С++
"Неужели я не могу просто создать класс Array? — скажете вы. — Зачем мне возиться с шаблонами?"
Конечно, можете. Если заранее знаете, объекты какого типа будут храниться в этом массиве. Например, если вам нужен только массив целых чисел, то нет смысла ломать голову над шаблоном Vector< T > — проще создать класс IntArray и использовать его.
По сути единственной альтернативой шаблонам является использование void*, указателя, который может указывать на объекты любого типа. Этот способ использован в следующей программе.
/* VoidVector — реализация вектора с использованием */
/* void* для хранения элементов */
#include
#include
#include
using namespace std ;
typedef void* VoidPtr ;
class VoidVector
{
public:
VoidVector( int nArraySize )
{
/* Количество элементов */
/* Количество элементов */
nSize = nArraySize ;
ptr = new VoidPtr[ nArraySize ] ;
reset( ) ;
}
int size( ) { return nWriteIndex ; }
void reset( ) { nWriteIndex = 0 ; nReadIndex = 0 ; }
void add( void* pValue )
{
if ( nWriteIndex < nSize )
{
ptr[ nWriteIndex++ ] = pValue ;
_________________
314 стр. Часть 5. Полезные особенности
}
}
VoidPtr get( ){ return ptr[ nReadIndex++ ] ; }
protected :
int nSize ;
int nWriteIndex ;
int nReadIndex ;
VoidPtr* ptr ;
} ;
int main( int argc , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */
/* Создание вектора */
VoidVector vv( 10 ) ;
/* Добавление значений к вектору */
cout << "Введите последовательность целых чисел\n"
"для внесения в вектор ( отрицательное\n"
"число завершает ввод последовательности )"
<< endl ;
for( ; ; )
{
int* p = new int ;
cin >> *p ;
if ( *p < 0 )
{
delete p ;
break ;
}
vv.add( ( void* ) p ) ;
}
cout << "\nВы ввели следующие числа" << endl ;
for ( int i = 0 ; i < vv.size( ) ; i++ )
{
int* p = ( int* )vv.get( ) ;
cout << i << ":" << *p << endl ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
В этой программе тип VoidPtr определён как синоним void*.
«Ключевое слово typedef создаёт новое имя для существующего типа. Вы можете везде, где видите VoidPtr, в уме вставлять void*. Использование таких замен делает текст более удобочитаемым, а также упрощает синтаксис выражений. Иногда оказывается невозможным заставить работать существующий шаблон класса с указателем, и тогда использование typedef для замены составного типа наподобие указателя может решить проблему.»
[Советы]
_________________
315 стр. Глава 27. Шаблоны С++
Класс VoidVector предоставляет те же функции-члены add( ) и get( ), что и TemplateVector из предыдущей программы.
Это решение имеет ( как минимум ) три проблемы. Во-первых, оно неудобно в использовании, как видно из текста функции main( ) — вы не в состоянии сохранить значение, и должны использовать только указатели на объекты. Это означает, что вы должны выделить для значения память в куче и поместить в вектор её адрес.
Во-вторых, если вдруг вы попытаетесь добавлять целые значения в вектор следующим образом:
int n ;
сin >> n ;
vv.add( ( void* ) &n ) ;
то у вас ничего не получится. Переменная n имеет локальную область видимости, так что при выходе из цикла for он просто потеряет всякий смысл.
«На самом деле всё ещё хуже — адрес n остаётся неизменным во всех итерациях цикла for.»
[Советы]
В-третьих, самая серьёзная проблема в том, что при получении значений из VoidVector вы должны знать их тип. С++ не может проверить тип объекта, чтобы убедиться, что ваши предположения верны. Допустим, вы решили, что в векторе хранятся не целые, а действительные числа, и использовали следующий код:
double dValue = *( double* )get( ) ;
Такая программа не будет работать корректно, поскольку в dValue в результате окажется какой-то мусор. Однако компиляция этой программы пройдёт без ошибок. Приведение типа к void* сводит на нет преимущества строгой типизации С++.
Вы должны знать некоторые особенности использования шаблонов. Во-первых, шаблон не генерирует никакого кода ( код генерируется только после преобразования в конкретный класс или функцию ). Именно по этой причине шаблоны практически никогда не размещают в .срр-файлах. Обычно полное определение шаблона класса, включая все функции-члены, располагается в заголовочном файле с тем, чтобы быть доступным компилятору в процессе его работы.
Во-вторых, шаблон класса не потребляет память. Следовательно, наличие шаблона класса никак не скажется на программе, если этот шаблон не будет инстанцирован. С другой стороны, шаблон класса использует память при каждом инстанцировании, поэтому несмотря на то, что, например, класс Array< int > уже существует, классу Array< Student > также потребуется память.
И наконец, шаблон класса не компилируется и не проверяется на наличие ошибок до тех пор, пока не будет преобразован в реальный класс. Таким образом, программа, содержащая Аггау< Т >, может нормально компилироваться, несмотря на наличие в шаблоне очевидных синтаксических ошибок. Эти ошибки никак не проявят себя до тех пор, пока не будут созданы реальные классы наподобие Array< int > или Array< Student >.
_________________
316 стр. Часть 5. Полезные особенности
В этой главе...
►Контейнер list 320
►Итераторы 321
►Использование контейнера map 324
Некоторые программы сразу же пересылают получаемые данные, однако большинству программ приходится сначала сохранять информацию. Структуры, которые используются для хранения данных, называются контейнерами или коллекциями ( в моей книге это взаимозаменяемые понятия ). Пока что мы с вами в основном для хранения данных использовали массивы. Массив в качестве контейнера обладает рядом привлекательных свойств, в частности, высокой скоростью сохранения и выборки данных. Кроме того, можно объявить массив для хранения данных любого типа. Тем не менее и у массива есть свои существенные недостатки.
Во-первых, вы должны заранее знать размер массива. В общем случае это требование невыполнимо, хотя иногда вы знаете, что количество элементов не может превысить некоторое число. Однако те же вирусы успешно используют такие предположения программиста о количестве элементов массива, делая их ошибочными и заставляя программу выполнить запись за пределами массива. Не имеется также никакого иного способа увеличить массив, кроме как объявить новый массив и перенести в него содержимое старого массива меньшего размера.
Во-вторых, вставка элементов в произвольное место массива влечёт за собой копирование элементов внутри массива. Это достаточно дорогостоящая операция как с точки зрения используемой памяти, так и процессорного времени. Сортировка же элементов в пределах массива ещё более дорогостояща.
В настоящее время в состав С++ входит стандартная библиотека шаблонов ( Standard Template Library, STL ), включающая множество различных типов контейнеров, каждый из которых обладает своими достоинствами ( и, само собой, недостатками ).
«STL — весьма объёмная библиотека с массой сложно реализованных контейнеров. Весь приведённый здесь материал следует рассматривать как беглое знакомство лишь с некоторыми возможностями STL.»
[Советы]
Наиболее распространённым типом массива, по-видимому, является нуль-завершённая строка, используемая для вывода текста. В ней наиболее ярко проявляются как достоинства, так и недостатки массивов. Взгляните, насколько просто выглядит следующее выражение:
cout << "Это обычная строка" ;
_________________
317 стр. Глава 28. Стандартная библиотека шаблонов
А вот как выглядит конкатенация двух строк:
char* concatString( char* s1 , char* s2 )
{
int length = strlen( s1 ) + strlen( s2 ) + 1 ;
char* s = new char[ length ] ;
strcpy( s , s1 ) ;
strcat( s , s2 ) ;
return s ;
}
Для работы со строками STL предоставляет контейнер string. Этот класс предоставляет программисту массу операций ( включая перегруженные операторы ), которые упрощают работу со строками символов. Та же конкатенация строк с использованием класса string выглядит гораздо проще:
string concat( string s1 , string s2 )
{
return s1 + s2 ;
}
«До сих пор в программах я старался избегать использования класса string, поскольку вы ещё с ним не знакомы. Однако большинство программистов используют этот класс гораздо чаще, чем массивы символов с завершающим нулевым элементом.»
[Помни!]
Приведённая далее программа демонстрирует несколько возможностей класса string.
/* STLString — демонстрация простейших */
/* возможностей класса string из STL */
#include
#include
#include
using namespace std ;
/* concat — конкатенация двух строк */
string concat( string s1 , string s2 )
{
return s1 + s2 ;
}
/* removeSpaces — удаление всех пробелов из строки */
string removeSpaces( string s )
{
/* Находим смещение первого пробела; продолжаем поиск до тех пор, пока не сможем найти больше ни одного пробела */
size_t offset ;
while ( ( offset = s.find( " " ) ) != -1 )
{
/* Удаляем найденный пробел */
s.erase( offset , 1 ) ;
}
return s ;
}
/* insertPhrase — вставка фразы в том месте, где находится метка
string insertPhrase( string source )
{
_________________
318 стр. Часть 5. Полезные особенности
size_t offset = source.find( "
if ( offset != -1 )
{
source.erase( offset , 4 ) ;
source.insert( offset , "Randall" ) ;
}
return source ;
}
int main( int argc , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
/* Создаём строку, которая представляет собой конкатенацию двух меньших строк */
cout << "string1 + string2 = "
<< concat( "string1 " , "string2" )
<< endl ;
/* Создаём тестовую строку и удаляем в ней все пробелы */
string s2( "The phrase" ) ;
cout << "<" << s2 << "> минус пробелы = <"
<< removeSpaces( s2 ) << ">" << endl ;
/* Вставляем фразу в средину существующей строки */
string s3 = "Stephen
cout << s3 + " -> " + insertPhrase( s3 ) << endl ;
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Оператор operator+( ) выполняет конкатенацию строк, которая раньше осуществлялась при помощи функции concatCharacterString( ).
Функция removeSpaces( ) удаляет все найденные пробелы из строки при помощи операции string::find( ), которая возвращает смещение первого найденного пробела. После того, как положение пробела в строке определено, функция-член erase( ) удаляет его из строки. Метод find( ) возвращает смещение найденного пробела от начала строки, или -1, если он не найден.
«Тип size_t определён в заголовочных файлах STL как целое число, которое в состоянии работать с массивом максимально допустимого на вашем компьютере размера. Обычно это тип long. Использование типа size_t связано с вопросами переносимости исходного кода между различными программно-аппаратными платформами. Visual С++ .NET сгенерирует предупреждение, если вместо size_t вы используете int.»
[Советы]
Функция insertPhrase( ) использует метод find( ) для поиска точки вставки в строку, после чего метод erase( ) удаляет метку вставки
Вот как выглядит вывод данной программы:
string1 + string2 = string1 string2
Stephen
Press any key to continue...
_________________
319 стр. Глава 28. Стандартная библиотека шаблонов
STL предоставляет программисту массу контейнеров — гораздо больше, чем я могу описать в одной главе. Здесь я попытаюсь хотя бы вкратце познакомить вас с двумя из них.
Контейнер STL list хранит объекты связанными наподобие блоков детского конструктора. Объекты могут быть связаны в любом порядке, что делает данный контейнер идеальным для вставки, сортировки, объединения списков и прочих операций над объектами. Приведённая далее программа демонстрирует использование list для сортировки набора имён .
/* STLList — использование контейнера list для */
/* ввода и сортировки строк */
#include
#include
#include
#include
#include
/* Объявление списка строк */
using namespace std ;
list
int main( int argc , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
/* Ввод строк имён */
cout << "Введите имя ( или х для завершения )"
<< endl ;
while ( true )
{
string name ;
cin >> name ;
if ( ( name.compare( "x" ) == 0 ) ||
( name.compare( "X" ) == 0 ) )
{
break ;
}
names.push_back( name ) ;
}
/* Сортируем список */
names.sort( ) ;
/* Выводим отсортированный список */
/* Выводим имена, пока список не опустеет */
cout << "\nОтсортированный список:" << endl ;
while ( !names.empty( ) )
{
/* Первое имя в списке */
string name = names.front( ) ;
cout << name << endl ;
/* Удаляем это имя из списка */
names.pop_front( ) ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
_________________
320 стр. Часть 5. Полезные особенности
В этом примере определена переменная names, являющаяся списком объектов string. Программа начинает работу с чтения вводимого пользователем списка имён . Каждое введённое имя добавляется к концу списка с помощью метода push_back( ). Цикл завершается, когда пользователь вводит имя "х". Затем список имён сортируется при помощи метода sort( ) .
Программа выводит отсортированный список имён , удаляя объекты с начала списка до тех пор, пока он не станет пустым.
Вот пример вывода данной программы.
Введите имя ( или х для завершения )
Igor
Ira
Anton
х
Отсортированный список:
Anton
Igor
Ira
Press any key to continue...
Контейнер list предоставляет программисту массу различных возможностей, простейшие из которых — insert, swap и erase. Контейнер также позволяет программисту осуществлять итерации по списку с выполнением пользовательской функции над каждым элементом списка.
Однако список не в состоянии обеспечить произвольный доступ к своим элементам. Поскольку объекты могут быть связаны в произвольном порядке, не существует быстрого способа обратиться к n-ому элементу.
В представленной в предыдущем разделе программе для прохода по списку использован деструктивный метод: метод pop_front( ) позволяет пользователю пройти по всему списку, удаляя всякий раз первый объект в списке.
Проход по массиву обычно осуществляется программистом с использованием индекса массива — но такой способ в случае списка неприменим. Можно представить решение, основанное на использовании методов типа getFirst( ) и getNext( ), однако разработчики STL хотели обеспечить обобщённый метод прохода по элементам контейнера, который работал бы для любого вида контейнера. Этой цели служат итераторы STL.
Итератор представляет собой объект, который указывает на объекты, содержащиеся в контейнере. В общем случае итераторы поддерживают следующие функции.
■■■
■ Класс может вернуть итератор, который указывает на первый член коллекции.
■ Итератор можно переместить от одного элемента к следующему.
■ Программа может обратиться к элементу, на который указывает итератор.
■■■
Код, требуемый для обхода списка list, отличается от кода обхода вектора vector. Однако итераторы скрывают эти детали, унифицируя обход любого контейнера с точки зрения программиста.
_________________
321 стр. Глава 28. Стандартная библиотека шаблонов
Приведённая далее программа использует итератор для обхода списка STL недеструктивным образом.
/* STLListUserClass — использование списка STL для */
/* хранения и сортировки объектов */
/* пользовательского класса */
#include
#include
#include
#include
#include
using namespace std ;
/* Student — пример пользовательского класса */
class Student
{
public :
Student( char* pszName , int id )
{
name = new string( pszName ) ;
ssID = id ;
}
string* name ;
int ssID ;
} ;
/* Данная функция требуется для поддержки сортировки */
bool operator<( Student& s1 , Student& s2 )
{
return s1.ssID < s2.ssID ;
}
/* Определение коллекции студентов */
list
int main( int argc , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
/* Добавление нескольких студентов в список */
students.push_back( *new Student( "Семенякин Сергей" , 10 ) ) ;
students.push_back( *new Student( "Редчук Александр", 5 ) ) ;
students.push_back( *new Student( "Шапран Павел" , 25 ) ) ;
students.push_back( *new Student( "Чистяков Александр" , 20 ) ) ;
students.push_back( *new Student( "Снежко Ирина" , 15 ) ) ;
/* Сортировка списка */
students.sort( ) ;
/* обход списка: */
/* 1 ) получаем итератор, который указывает на первый элемент списка */
list
/* 2 ) цикл выполняется до тех пор, пока итератор не будет указывать на конец списка */
while ( iter != students.end( ) )
{
_________________
322 стр. Часть 5. Полезные особенности
/* 3 ) Получение студента, на которого указывает итератор */
Student& s = *iter ;
cout << s.ssID << " — " << *s.name << endl ;
/* 4 ) итератор переходит к следующему элементу списка */
iter++ ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Программа определяет список пользовательских объектов Student ( вместо простых имён ). Вызовы push_back( ) добавляют элементы в список ( "зашивание" этих вызовов в программу, а не, например, ввод с клавиатуры делает эту программу короче ). Вызов sort( ) сортирует список так же, как и в предыдущей программе.
«Функция sort( ) в STL требует от пользователя переопределения оператора "меньше чем". ( Это одно из тех редких мест, где требуется определение пользовательского оператора, отличного от присвоения. ) Оператор operator<( Student&, Student& ) вызывается при вычислении выражения s1 < s2, где s1 и s2 — объекты типа Student.»
[Атас!]
Программа использует итератор iter для прохода по списку. Взгляните внимательно на объявление итератора: list
5 — Редчук Александр
10 — Семенякин Сергей
15 — Снежко Ирина
20 — Чистяков Александр
25 — Шапран Павел
Press any key to continue...
Как сортирует функция sort( )
Я должен разъяснить один интересный момент — откуда метод sort( ) знает, какой из двух элементов списка "больше"? Другими словами, как определяется порядок сортировки? Для ряда типов С++ определяет порядок сортировки самостоятельно. Так, например, С++ не надо пояснять, какой из двух int больше. Кроме того, STL сортирует коллекцию строк по тем же правилам, что используются в словаре.
Таким образом, программе, сортирующей имена в списке, не надо было ничего пояснять, поскольку С++ известно, как сортировать объекты типа string. Однако С++ не знает, какой из объектов student больше. Для этой цели служит глобальная функция ::operator<( Student&, Student& ). Метод sort( ) использует эту функцию для определения корректного порядка сортировки. В качестве эксперимента измените смысл оператора operator<( ) на обратный:
return s1.ssID > s2.ssID ;
При этом вы должны получить тот же список, что и ранее, но выведенный в обратном порядке:
25 — Шапран Павел
20 — Чистяков Александр
15 — Снежко Ирина
10 — Семенякин Сергей
5 — Редчук Александр
Press any key to continue...
_________________
323 стр. Глава 28. Стандартная библиотека шаблонов
Ассоциативный массив map представляет собой ещё один класс-контейнер. Имеется множество ассоциативных массивов, но все они обладают одним общим свойством — обеспечивают быстрое сохранение и выборку в соответствии с некоторым ключом или индексом. Приведённая ниже программа демонстрирует этот принцип на практике.
Например, в институте студенты могут быть зарегистрированы при помощи уникальных идентификационных номеров. Этот идентификационный номер используется во всех случаях студенческой жизни: для получения информации о студенте, при выдаче книг в библиотеке, записи в ведомость об оценках. Очень важно, чтобы любая программа могла получить информацию о студенте по его номеру быстро и эффективно.
Следующая программа демонстрирует использование ассоциативного массива студентов с идентификатором в качестве ключа.
/* STLMap — использование ассоциативного массива */
/* для коллекции студентов, упорядоченной */
/* по их идентификаторам */
#include
#include
#include
#include
#include
#include
using namespace std ;
/* SC — Функция сравнения студентов, */
/* определяющая порядок их сортировки */
struct SC
{
bool operator( )( const int id1 , const int id2 ) const
{
return id1 < id2 ;
}
} ;
/* Ассоциативный массив в действительности содержит пары, первый элемент которых является ключом, а второй — данными ( в нашем случае — классом Student ) */
class Student ;
typedef Student* SP ;
typedef pair< const int , Student* > Pair ;
typedef map< int , SP , SC > Map ;
typedef map< int , SP , SC >::iterator MapIterator ;
/* Коллекция студентов */
Map students ;
_________________
324 стр. Часть 5. Полезные особенности
/* Student — определяет важные свойства студентов, в первую очередь — ключ, используемый для выборки информации о студенте */
class Student
{
public :
Student( char* pszName , int id )
: studentIDKey( id ) , name( pszName ) { }
/* getKey — ключ, используемый в качестве индекса в ассоциативном массиве */
const int getKey( ) { return studentIDKey ; }
/* display — вывод информации на экран */
string display( )
{
ostringstream out ;
out << studentIDKey << " — " << name ;
return out.str( ) ;
}
protected :
/* Ключевое поле — идентификатор студента */
const int studentIDKey ;
/* Имя студента ( а также прочие данные ) */
string name ;
} ;
int main( int argc , char* pArgs[ ] )
{
setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */
/* Добавляем несколько студентов в коллекцию */
Student* pS ;
pS = new Student( "Алла" , 3456 ) ;
Pair* ptr = new Pair( pS -> getKey( ) , pS ) ;
students.insert( *ptr ) ;
/* Ассоциативный массив перегружает оператор индексирования для создания пары и вставки её в массив */
students[ 1234 ] = new Student( "Лариса" ,
1234 ) ;
students[ 5678 ] = new Student( "Марианна" ,
5678 ) ;
/* Проход по списку студентов. Ассоциативный массив всегда хранит элементы упорядоченными по ключу */
cout << "Отсортированный список студентов:" << endl ;
MapIterator iter = students.begin( ) ;
while ( iter != students.end( ) )
{
Pair p = *iter ;
Student* s = p.second ;
cout << s -> display( ) << endl ;
iter++ ;
}
_________________
325 стр. Глава 28. Стандартная библиотека шаблонов
/* Операторы инкремента и декремента могут использоваться для поиска предыдущего и последующего элемента */
cout << "\nИщем студента 3456" << endl ;
MapIterator p = students.find( 3456 ) ;
cout << "Найден: " << p -> second -> display( ) << endl ;
MapIterator p1 = p ;
MapIterator prior = --p1 ;
cout << "Предшественник = "
<< prior -> second -> display( ) << endl ;
MapIterator p2 = p ;
MapIterator successor = ++p2 ;
cout << "Следующий = "
<< successor -> second -> display( ) << endl ;
/* Функция find( ) возвращает итератор end( ), если искомый элемент не найден; operator[ ] возвращает NULL */
if ( students.find( 0123 ) == students.end( ) )
{
cout << "Вызов students.find( 0123 ) возвратил\n"
<< "students.end( ), т.к. студента 0123 нет"
<< endl ;
}
/* Вывод с использованием индекса */
cout << "Проверка индекса: students[ 3456 ] = "
<< students[ 3456 ] -> display( ) << endl ;
if ( students[ 0123 ] == NULL )
{
cout << "students[ 0123 ] возвращает NULL"
<< endl ;
}
/* Пауза для того, чтобы посмотреть на результат работы программы */
system( "PAUSE" ) ; return 0 ;
}
Ключевым моментом программы являются три оператора typedef. Контейнер map содержит множество объектов Pair, каждый из которых содержит по два элемента. Первый элемент — ключ ( в нашем случае — идентификатор студента ), а второй — сам объект Student. В аргументы шаблона Map добавлен класс SC, который содержит единственный метод, сравнивающий два ключа ( это немного сложнее, чем глобальная функция, использованная в контейнере list, но эффект абсолютно тот же ).
Программа начинает работу с создания трёх объектов Pair и вносит их в список. Затем проход по контейнеру показывает, что он хранит элементы упорядоченными по ключу, так что вызов метода sort( ) нам не нужен.
Во второй части программы выполняется поиск с использованием метода find( ), а также выборка предыдущего и последующего элементов контейнера при помощи операторов инкремента и декремента.
_________________
326 стр. Часть 5. Полезные особенности
Вывод программы выглядит следующим образом:
Отсортированный список студентов:
1234 — Лариса
3456 — Алла
5678 — Марианна
Ищем студента 3456
Найден: 3456 — Алла
Предшественник = 1234 — Лариса
Следующий = 5678 — Марианна
Вызов students.find( 0123 ) возвратил
students.end( ), т.к. студента 0123 нет
Проверка индекса: students[ 3456 ] = 3456 — Алла
students[ 0123 ] возвращает NULL
Press any key to continue...
_________________
327 стр. Глава 28. Стандартная библиотека шаблонов