Директива связывания extern "C" A
Если программист хочет использовать функцию, написанную на другом языке, в частности на С, то компилятору нужно указать, что при вызове требуются несколько иные условия. Скажем, имя функции или порядок передачи аргументов различаются в зависимости от языка программирования.
Показать, что функция написана на другом языке, можно с помощью директивы связывания в форме простой либо составной инструкции:
// директива связывания в форме простой инструкции extern "C" void exit(int); // директива связывания в форме составной инструкции extern "C" { int printf( const char* ... ); int scanf( const char* ... ); } // директива связывания в форме составной инструкции extern "C" { #include <cmath> |
}
Первая форма такой директивы состоит из ключевого слова extern, за которым следует строковый литерал, а за ним – “обычное” объявление функции. Хотя функция написана на другом языке, проверка типов вызова выполняется полностью. Несколько объявлений функций могут быть помещены в фигурные скобки составной инструкции директивы связывания – второй формы этой директивы. Скобки отмечают те объявления, к которым она относится, не ограничивая их видимости, как в случае обычной составной инструкции. Составная инструкция extern "C" в предыдущем примере говорит только о том, что функции printf() и scanf()
написаны на языке С. Во всех остальных отношениях эти объявления работают точно так же, как если бы они были расположены вне инструкции.
Если в фигурные скобки составной директивы связывания помещается директива препроцессора #include, все объявленные во включаемом заголовочном файле функции рассматриваются как написанные на языке, указанном в этой директиве. В предыдущем примере все функции из заголовочного файла cmath написаны на языке С.
Директива связывания не может появиться внутри тела функции. Следующий фрагмент кода вызывает ошибку компиляции:
int main() { // ошибка: директива связывания не может появиться // внутри тела функции extern "C" double sqrt( double ); double getValue(); //правильно double result = sqrt ( getValue() ); //... return 0; |
}
Если мы переместим директиву так, чтобы она оказалась вне тела main(), программа откомпилируется правильно:
extern "C" double sqrt( double ); int main() { double getValue(); //правильно double result = sqrt ( getValue() ); //... return 0; |
Однако более подходящее место для директивы связывания – заголовочный файл, где находится объявление функции, описывающее ее интерфейс.
Как сделать С++ функцию доступной для программы на С? Директива extern "C"
поможет и в этом:
// функция calc() может быть вызвана из программы на C |
Если в одном файле имеется несколько объявлений функции, то директива связывания может быть указана при каждом из них или только при первом – в этом случае она распространяется и на все последующие объявления. Например:
// ---- myMath.h ---- extern "C" double calc( double ); // ---- myMath.C ---- // объявление calc() в myMath.h #include "myMath.h" // определение функции extern "C" calc() // функция calc() может быть вызвана из программы на C |
В данном разделе мы видели примеры директивы связывания extern "C" только для языка С. Это единственный внешний язык, поддержку которого гарантирует стандарт С++. Конкретная реализация может поддерживать связь и с другими языками. Например, extern "Ada" для функций, написанных на языке Ada; extern "FORTRAN" для языка FORTRAN и т.д. Мы описали один из случаев использования ключевого слова extern в С++. В разделе 8.2 мы покажем, что это слово имеет и другое назначение в объявлениях функций и объектов.
Упражнение 7.14
exit(), printf(), malloc(), strcpy() и strlen() являются функциями из библиотеки С. Модифицируйте приведенную ниже С-программу так, чтобы она компилировалась и связывалась в С++.
const char *str = "hello"; void *malloc( int ); char *strcpy( char *, const char * ); int printf( const char *, ... ); int exit( int ); int strlen( const char * ); int main() { /* программа на языке С */ char* s = malloc( strlen(str)+l ); strcpy( s, str ); printf( "%s, world\n", s ); exit( 0 ); |
Директива typedef
Директива typedef
позволяет задать синоним для встроенного либо пользовательского типа данных. Например:
typedef double wages; typedef vector<int> vec_int; typedef vec_int test_scores; typedef bool in_attendance; |
typedef int *Pint;
Имена, определенные с помощью директивы typedef, можно использовать точно так же, как спецификаторы типов:
// double hourly, weekly; wages hourly, weekly; // vector<int> vecl( 10 ); vec_int vecl( 10 ); // vector<int> test0( c1ass_size ); const int c1ass_size = 34; test_scores test0( c1ass_size ); // vector< bool > attendance; vector< in_attendance > attendance( c1ass_size ); // int *table[ 10 ]; |
Pint table [ 10 ];
Эта директива начинается с ключевого слова typedef, за которым идет спецификатор типа, и заканчивается идентификатором, который становится синонимом для указанного типа.
Для чего используются имена, определенные с помощью директивы typedef? Применяя мнемонические имена для типов данных, можно сделать программу более легкой для восприятия. Кроме того, принято употреблять такие имена для сложных составных типов, в противном случае воспринимаемых с трудом (см. пример в разделе 3.14), для объявления указателей на функции и функции-члены класса (см. раздел 13.6).
Ниже приводится пример вопроса, на который почти все дают неверный ответ. Ошибка вызвана непониманием директивы typedef как простой текстовой макроподстановки. Дано определение:
typedef char *cstring;
Каков тип переменной cstr в следующем объявлении:
extern const cstring cstr;
Ответ, который кажется очевидным:
const char *cstr
Однако это неверно. Спецификатор const
относится к cstr, поэтому правильный ответ – константный указатель на char:
char *const
cstr;
Директивы препроцессора
Заголовочные файлы включаются в текст программы с помощью директивы препроцессора #include. Директивы препроцессора начинаются со знака “диез” (#), который должен быть самым первым символом строки. Программа, которая обрабатывает эти директивы, называется препроцессором
(в современных компиляторах препроцессор обычно является частью самого компилятора).
Директива #include
включает в программу содержимое указанного файла. Имя файла может быть указано двумя способами:
#include <some_file.h> |
#include "my_file.h"
Если имя файла заключено в угловые скобки (<>), считается, что нам нужен некий стандартный заголовочный файл, и компилятор ищет этот файл в предопределенных местах. (Способ определения этих мест сильно различается для разных платформ и реализаций.) Двойные кавычки означают, что заголовочный файл – пользовательский, и его поиск начинается с того каталога, где находится исходный текст программы.
Заголовочный файл также может содержать директивы #include. Поэтому иногда трудно понять, какие же конкретно заголовочные файлы включены в данный исходный текст, и некоторые заголовочные файлы могут оказаться включенными несколько раз. Избежать этого позволяют условные директивы препроцессора. Рассмотрим пример:
#ifndef BOOKSTORE_H
#define BOOKSTORE_H /* содержимое файла bookstore.h */ |
#endif
Условная директива
#ifndef проверяет, не было ли значение BOOKSTORE_H
определено ранее. (BOOKSTORE_H – это константа препроцессора; такие константы принято писать заглавными буквами.) Препроцессор обрабатывает следующие строки вплоть до директивы #endif. В противном случае он пропускает строки от #ifndef до # endif.
Директива
#define BOOKSTORE_H
определяет константу препроцессора BOOKSTORE_H. Поместив эту директиву непосредственно после директивы #ifndef, мы можем гарантировать, что содержательная часть заголовочного файла bookstore.h
будет включена в исходный текст только один раз, сколько бы раз ни включался в текст сам этот файл.
Другим распространенным примером применения условных директив препроцессора является включение в текст программы отладочной информации. Например:
int main() { #ifdef DEBUG cout << "Начало выполнения main()\n"; #endif string word; vector<string> text; while ( cin >> word ) { #ifdef DEBUG cout << "Прочитано слово: " << word << "\n"; #endif text.push_back(word); } // ... |
Если константа DEBUG не определена, результирующий текст программы будет выглядеть так:
int main() |
string word;
vector<string> text;
while ( cin >> word )
{
text.push_back(word);
}
// ...
}
В противном случае мы получим:
int main() |
cout << "Начало выполнения main()\n";
string word;
vector<string> text;
while ( cin >> word )
{
cout << "Прочитано слово: " << word << "\n";
text.push_back(word);
}
// ...
}
Константа препроцессора может быть определена в командной строке при вызове компилятора с помощью опции -D (в различных реализациях эта опция может называться по-разному). Для UNIX-систем вызов компилятора с определением препроцессорной константы DEBUG
выглядит следующим образом:
$ CC -DDEBUG main.C
Есть константы, которые автоматически определяются компилятором. Например, мы можем узнать, компилируем ли мы С++ или С программу. Для С++ программы автоматически определяется константа __cplusplus
(два подчеркивания). Для стандартного С определяется __STDC__. Естественно, обе константы не могут быть определены одновременно. Пример:
#idfef __cplusplus // компиляция С++ программы extern "C"; // extern "C" объясняется в главе 7 #endif |
Другими полезными предопределенными константами (в данном случае лучше сказать переменными) препроцессора являются __LINE__ и __FILE__. Переменная __LINE__
содержит номер текущей компилируемой строки, а __FILE__ – имя компилируемого файла. Вот пример их использования:
if ( element_count == 0 ) |
<< " Строка: " << __LINE__
<< "element_count не может быть 0";
Две константы __DATE__ и __TIME__
содержат дату и время компиляции.
Стандартная библиотека С предоставляет полезный макрос assert(), который проверяет некоторое условие и в случае, если оно не выполняется, выдает диагностическое сообщение и аварийно завершает программу. Мы будем часто пользоваться этим полезным макросом в последующих примерах программ. Для его применения следует включить в программу директиву
#include <assert.h>
assert.h – это заголовочный файл стандартной библиотеки С. Программа на C++ может ссылаться на заголовочный файл как по его имени, принятому в C, так и по имени, принятому в C++. В стандартной библиотеке С++ этот файл носит имя cassert. Имя заголовочного файла в библиотеке С++ отличается от имени соответствующего файла для С отсутствием расширения .h и подставленной спереди буквой c
(выше уже упоминалось, что в заголовочных файлах для C++ расширения не употребляются, поскольку они могут зависеть от реализации).
Эффект от использования директивы препроцессора #include зависит от типа заголовочного файла. Инструкция
#include <cassert>
включает в текст программы содержимое файла cassert. Но поскольку все имена, используемые в стандартной библиотеке С++, определены в пространстве std, имя assert()
будет невидимо до тех пор, пока мы явно не сделаем его видимым с помощью следующей using-директивы:
using namespace std;
Если же мы включаем в программу заголовочный файл для библиотеки С
#include <assert.h>
то надобность в using-директиве отпадает: имя assert() будет видно и так[2]. (Пространства имен используются разработчиками библиотек для предотвращения засорения глобального пространства имен. В разделе 8.5 эта тема рассматривается более подробно.)
Дополнительные операции со строками
Вторая форма функции-члена erase()
принимает в качестве параметров два итератора, ограничивающих удаляемую подстроку. Например, превратим
string name( "AnnaLiviaPlurabelle" );
в строку "Annabelle":
typedef string::size_type size_type; size_type startPos = name.find( 'L' ) size_type endPos = name.find_1ast_of( 'b' ); name.erase( name.begin()+startPos, |
name.begin()+endPos );
Символ, на который указывает второй итератор, не входит в удаляемую подстроку.
Для третьей формы параметром является только один итератор; эта форма удаляет все символы, начиная с указанной позиции до конца строки. Например:
name.erase( name. begin()+4 );
оставляет строку "Anna".
Функция-член insert()
позволяет вставить в заданную позицию строки другую строку или символ. Общая форма выглядит так:
string_object.insert( position, new_string );
position
обозначает позицию, перед которой производится вставка. new_string
может быть объектом класса string, C-строкой или символом:
string string_object( "Missisippi" ); string::size_type pos = string_object.find( "isi" ); |
string_object.insert( pos+1, 's' );
Можно выделить для вставки подстроку из new_string:
string new_string ( "AnnaBelle Lee" ); string_object += ' '; // добавим пробел // найдем начальную и конечную позицию в new_string pos = new_string.find( 'B' ); string::size_type posEnd = new_string.find( ' ' ); string_object.insert( string_object.size(), // позиция вставки new_string, pos, // начало подстроки в new_string posEnd // конец подстроки new_string |
)
string_object
получает значение "Mississippi Belle". Если мы хотим вставить все символы new_string, начиная с pos, последний параметр нужно опустить.
Пусть есть две строки:
string sl( "Mississippi" ); |
string s2( "Annabelle" );
Как получить третью строку со значением "Miss Anna"?
Можно использовать функции-члены assign() и append():
string s3; // скопируем первые 4 символа s1 |
s3 теперь содержит значение "Miss".
// добавим пробел |
Теперь s3 содержит "Miss ".
// добавим 4 первых символа s2 |
s3 получила значение "Miss Anna". То же самое можно сделать короче:
s3.assign(s1,4).append(' ').append(s2,4);
Другая форма функции-члена assign()
имеет три параметра: второй обозначает позицию начала, а третий – длину. Позиции нумеруются с 0. Вот как, скажем, извлечь "belle" из "Annabelle":
string beauty; // присвоим beauty значение "belle" |
Вместо этих параметров мы можем использовать пару итераторов:
// присвоим beauty значение "belle" |
В следующем примере две строки содержат названия текущего проекта и проекта, находящегося в отложенном состоянии. Они должны периодически обмениваться значениями, поскольку работа идет то над одним, то над другим. Например:
string current_project( "C++ Primer, 3rd Edition" ); |
Функция-член swap()
позволяет обменять значения двух строк с помощью вызова
current_project.swap( pending_project );
Для строки
string first_novel( "V" );
операция взятия индекса
char ch = first_novel[ 1 ];
возвратит неопределенное значение: длина строки first_novel равна 1, и единственное правильное значение индекса – 0. Такая операция взятия индекса не обеспечивает проверку правильности параметра, но мы всегда можем сделать это сами с помощью функции-члена size():
int elem_count( const string &word, char elem ) { int occurs = 0; // не надо больше проверять ix for ( int ix=0; ix < word.size(); ++-ix ) if ( word[ ix ] == elem ) ++occurs; return occurs; |
}
Там, где это невозможно или нежелательно, например:
void mumble( const string &st, int index ) { // возможна ошибка char ch = st[ index ]; // ... |
следует воспользоваться функцией at(), которая делает то же, что и операция взятия индекса, но с проверкой. Если индекс выходит за границу, возбуждается исключение out_of_range:
void mumble( const string &st, int index ) { try { char ch = st.at( index ); // ... } catch ( std::out_of_range ){...} // ... |
Строки можно сравнивать лексикографически. Например:
string cobol_program_crash( "abend" ); |
Строка cobol_program_crash
лексикографически меньше, чем cplus_program_crash: сопоставление производится по первому отличающемуся символу, а буква e в латинском алфавите идет раньше, чем o. Операция сравнения выполняется функцией-членом compare(). Вызов
sl.compare( s2 );
возвращает одно из трех значений:
· если s1
больше, чем s2, то положительное;
· если s1
меньше, чем s2, то отрицательное;
· если s1
равно s2, то 0.
Например,
cobol_program_crash.compare( cplus_program_crash );
вернет отрицательное значение, а
cplus_program_crash.compare( cobol_program_crash );
положительное. Перегруженные операции сравнения (<, >, !=, ==, <=, >=) являются более компактной записью функции compare().
Шесть вариантов функции-члена compare() позволяют выделить сравниваемые подстроки в одном или обоих операндах. (Примеры вызовов приводились в предыдущем разделе.)
Функция-член replace()
дает десять способов заменить одну подстроку на другую (их длины не обязаны совпадать). В двух основных формах replace() первые два аргумента задают заменяемую подстроку: в первом варианте в виде начальной позиции и длины, во втором – в виде пары итераторов на ее начало и конец. Вот пример первого варианта:
string sentence( " An ADT provides both interface and implementation." ); string::size_type position = sentence.find_1ast_of( 'A' ); string::size_type length = 3; // заменяем ADT на Abstract Data Type |
position
представляет собой начальную позицию, а length – длину заменяемой подстроки. Третий аргумент является подставляемой строкой. Его можно задать несколькими способами. Допустим, как объект string:
string new_str( "Abstract Data Type" ); |
Следующий пример иллюстрирует выделение подстроки в new_str:
#include <string> typedef string::size_type size_type; // найдем позицию трех букв size_type posA = new_str.find( 'A' ); size_type posD = new_str.find( 'D' ); size_type posT = new_str.find( 'T' ); // нашли: заменим T на "Type" sentence.replace( position+2, 1, new_str, posT, 4 ); // нашли: заменим D на "Data " sentence.replace( position+1, 1, new_str, posD, 5 ); // нашли: заменим A на "Abstract " |
Еще один вариант позволяет заменить подстроку на один символ, повторенный заданное количество раз:
string hmm( "Some celebrate Java as the successor to C++." ); string:: size_type position = hmm.find( 'J' ); // заменим Java на xxxx |
В данном примере используется указатель на символьный массив и длина вставляемой подстроки:
const char *lang = "EiffelAda95JavaModula3"; int index[] = { 0, 6, 11, 15, 22 }; string ahhem( "C++ is the language for today's power programmers." ); |
А здесь мы используем пару итераторов:
string sentence( "An ADT provides both interface and implementation." ); // указывает на 'A' в ADT string: iterator start = sentence. begin()+3; // заменяем ADT на Abstract Data Type |
sentence.repiace( start, start+3, "Abstract Data Type" );
Оставшиеся четыре варианта допускают задание заменяющей строки как объекта типа string, символа, повторяющегося N раз, пары итераторов и C-строки.
Вот и все, что мы хотели сказать об операциях со строками. Для более полной информации обращайтесь к определению стандарта С++ [ISO-C++97].
Упражнение 6.18
Напишите программу, которая с помощью функций-членов assign() и append() из строк
string quote1( "When lilacs last in the dooryard bloom'd" ); |
составит предложение
"The child is in the dooryard"
Упражнение 6.19
Напишите функцию:
string generate_salutation( string generic1, string lastname, string generic2, string::size_type pos, |
которая в строке
string generic1( "Dear Ms Daisy:" );
заменяет Daisy и Ms
(миссис). Вместо Daisy
подставляется параметр lastname, а вместо Ms
подстрока
string generic2( "MrsMsMissPeople" );
длины length, начинающаяся с pos.
Например, вызов
string lastName( "AnnaP" ); string greetings = |
вернет строку:
Dear Miss AnnaP:
Дополнительные операторы ввода/вывода
Иногда необходимо прочитать из входного потока последовательность не интерпретируемых байтов, а типов данных, таких, как char, int, string и т.д. Функция-член get() класса istream
читает по одному байту, а функция getline() читает строку, завершающуюся либо символом перехода на новую строку, либо каким-то иным символом, определяемым пользователем. У функции-члена get() есть три формы:
·
get(char& ch) читает из входного потока один символ (в том числе и пустой) и помещает его в ch. Она возвращает объект iostream, для которого была вызвана. Например, следующая программа собирает статистику о входном потоке, а затем копирует входной поток в выходной:
#include <iostream> int main() { char ch; int tab_cnt = 0, nl_cnt = 0, space_cnt = 0, period_cnt = 0, comma_cnt = 0; while ( cin.get(ch)) { switch( ch ) { case ' ': space_cnt++; break; case '\t': tab_cnt++; break; case '\n': nl_cnt++; break; case '.': period_cnt++; break; case ',': comma_cnt++; break; } cout.put(ch); } cout << "\nнаша статистика:\n\t" << "пробелов: " << space_cnt << '\t' << "символов новой строки: " << nl_cnt << '\t' << "табуляций: " << tab_cnt << "\n\t" << "точек: " << period_cnt << '\t' << "запятых: " << comma_cnt << endl; |
}
Функция-член put()
класса ostream
дает альтернативный метод вывода символа в выходной поток: put()
принимает аргумент типа char и возвращает объект класса ostream, для которого была вызвана.
После компиляции и запуска программа печатает следующий результат:
Alice Emma has long flowing red hair. Her Daddy says
when the wind blows through her hair, it looks almost alive,
like a fiery bird in flight. A beautiful fiery bird, he tells her,
magical but untamed. "Daddy, shush, there is no such creature,"
she tells him, at the same time wanting him to tell her more.
Shyly, she asks, "I mean, Daddy, is there?"
наша статистика:
пробелов: 59 символов новой строки: 6 табуляций: 0
точек: 4 запятых: 12
· вторая форма get()
также читает из входного потока по одному символу, но возвращает не поток istream, а значение прочитанного символа. Тип возвращаемого значения равен int, а не char, поскольку необходимо возвращать еще и признак конца файла, который обычно равен -1, чтобы отличаться от кодов реальных символов. Для проверки на конец файла мы сравниваем полученное значение с константой EOF, определенной в заголовочном файле iostream. Переменная, в которой сохраняется значение, возвращенное get(), должна быть объявлена как int, чтобы в ней можно было представить не только код любого символа, но и EOF:
#include <iostream> int main() { int ch; // альтернатива: // while ( ch = cin.get() && ch != EOF ) while (( ch = cin.get()) != EOF ) cout.put( ch ); return 0; |
При использовании любой из этих форм get() для чтения данной последовательности нужно семь итераций:
a b c
d
Читаются следующие символы: ('a', пробел, 'b', пробел, 'c', символ новой строки, 'd'). На восьмой итерации читается EOF. Оператор ввода (>>) по умолчанию пропускает пустые символы, поэтому на ту же последовательность потребуется четыре итерации, на которых возвращаются символы: 'a', 'b', 'c', 'd'. А вот следующая форма get()
может прочесть всю последовательность всего за две итерации;
· сигнатура третьей формы get()
такова:
get(char *sink, streamsize size, char delimiter='\n')
sink – это массив, в который помещаются символы. size – это максимальное число символов, читаемых из потока istream. delimiter – это символ-ограничитель, при обнаружении которого чтение прекращается. Сам ограничитель не читается, а оставляется в потоке и будет прочитан следующим. Программисты часто забывают удалить его из потока перед вторым обращением к get(). Чтобы избежать этой ошибки, в показанной ниже программе мы воспользовались функцией-членом ignore()
класса istream. По умолчанию ограничителем является символ новой строки.
Символы читаются из потока, пока одно из следующих условий не окажется истинным. Как только это случится, в очередную позицию массива помещается двоичный нуль.
· прочитано size-1
символов;
· встретился конец файла;
· встретился символ-ограничитель (еще раз напомним, что он остается в потоке и будет считан следующим).
Эта форма get()
возвращает объект istream, для которого была вызвана (функция-член gcount() позволяет узнать количество прочитанных символов). Вот простой пример ее применения:
#include <iostream> int main() { const int max_line = 1024; char line[ max_line ]; while ( cin.get( line, max_line )) { // читается не больше max_line - 1 символов, // чтобы оставить место для нуля int get_count = cin.gcount(); cout << "фактически прочитано символов: " << get_count << endl; // что-то сделать со строкой // если встретился символ новой строки, // удалить его, прежде чем приступать к чтению следующей if ( get_count < max_line-1 ) cin.ignore(); } |
Если на вход этой программы подать текст о юной Алисе Эмме, то результат будет выглядеть так:
фактически прочитано символов: 52
фактически прочитано символов: 60
фактически прочитано символов: 66
фактически прочитано символов: 63
фактически прочитано символов: 61
фактически прочитано символов: 43
Чтобы еще раз протестировать поведение программы, мы создали строку, содержащую больше max_line
символов, и поместили ее в начало текста. Получили:
фактически прочитано символов: 1023
фактически прочитано символов: 528
фактически прочитано символов: 52
фактически прочитано символов: 60
фактически прочитано символов: 66
фактически прочитано символов: 63
фактически прочитано символов: 61
фактически прочитано символов: 43
По умолчанию ignore()
читает и удаляет один символ из потока, для которого вызвана, но можно и явно задать ограничитель и количество пропускаемых символов. В общем виде ее сигнатура такова:
ignore( streamsize length = 1, int delim = traits::eof )
ignore()
читает и отбрасывает length
символов из потока или все символы до ограничителя включительно или до конца файла и возвращает объект istream, для которого вызвана.
Мы рекомендуем пользоваться функцией getline(), а не get(), поскольку она автоматически удаляет ограничитель из потока. Сигнатура getline()
такая же, как у get() с тремя аргументами (и возвращает она тоже объект istream, для которого вызвана):
getline(char *sink, streamsize size, char delimiter='\n')
Поскольку и getline(), и get() с тремя аргументами могут читать size символов или меньше, то часто нужно “спросить” у объекта istream, сколько символов было фактически прочитано. Это позволяет сделать функция-член gcount(): она возвращает число символов, прочитанных при последнем обращении к get() или getline().
Функция-член write()
класса ostream
дает альтернативный метод вывода массива символов. Вместо того чтобы выводить символы до завершающего нуля, она выводит указанное число символов, включая и внутренние нули, если таковые имеются. Вот ее сигнатура:
write( const char *sink, streamsize length )
Здесь length
определяет, сколько символов выводить. write()
возвращает объект класса ostream, для которого она вызвана.
Парной для функции write() из класса ostream
является функция read() из класса istream с такой сигнатурой:
read( char* addr, streamsize size )
read()
читает size
соседних байт из входного потока и помещает их, начиная с адреса addr. Функция gcount()
возвращает число байт, прочитанных при последнем обращении к read(). В свою очередь read() возвращает объект класса istream, для которого она вызвана. Вот пример использования getline(), gcount() и write():
#include <iostream> int main() { const int lineSize = 1024; int lcnt = 0; // сколько строк прочитано int max = -1; // длина самой длинной строки char inBuf[ lineSize ]; // читается до конца строки, но не более 1024 символов while (cin.getline( inBuf, lineSize )) { // сколько символов фактически прочитано int readin = cin.gcount(); // статистика: счетчик строк, самая длинная строка ++lcnt; if ( readin > max ) max = readin; cout << "Строка #" << lcnt << "\tПрочитано символов: " << readin << endl; cout.write( inBuf, readin).put('\n').put('\n'); } cout << "Всего прочитано строк: " << lcnt << endl; cout << "Самая длинная строка: " << max << endl; |
Когда на вход было подано несколько фраз из романа Германа Мелвилла “Моби Дик”, программа напечатала следующее:
Строка #1 Прочитано символов: 45
Call me Ishmael. Some years ago, never mind
Строка #2 Прочитано символов: 46
how long precisely, having little or no money
Строка #3 Прочитано символов: 48
in my purse, and nothing particular to interest
Строка #4 Прочитано символов: 51
me on shore, I thought I would sail about a little
Строка #5 Прочитано символов: 47
and see the watery part of the world. It is a
Строка #6 Прочитано символов: 43
way I have of driving off the spleen, and
Строка #7 Прочитано символов: 28
regulating the circulation.
Всего прочитано строк: 7
Самая длинная строка: 51
Функция-член getline()
класса istream
поддерживает только ввод в массив символов. Однако в стандартной библиотеке есть обычная функция getline(), которая помещает символы в объект класса string:
getline( istream &is, string str, char delimiter );
Эта функция читает не более str::max_size()-1
символов. Если входная последовательность длиннее, то операция завершается неудачно и объект переводится в ошибочное состояние. В противном случае ввод прекращается, когда прочитан ограничитель (он удаляется из потока, но в строку не помещается) либо достигнут конец файла.
Вот еще три необходимые нам функции-члена класса istream:
// возвращает символ в поток putback( char class ); // устанавливает "указатель на следующий символ потока istream на один символ назад unget(); // возвращает следующий символ (или EOF), // но не извлекает его из потока |
Следующий фрагмент иллюстрирует использование некоторых из них:
char ch, next, lookahead; while ( cin.get( ch )) { switch (ch) { case '/': // это комментарий? посмотрим с помощью peek() // если да, пропустить остаток строки next = cin.peek(); if ( next == '/' ) cin.ignore( lineSize, '\n' ); break; case '>': // проверка на лексему >>= next = cin.peek(); if ( next == '>' ) { lookahead = cin.get(); next = cin.peek(); if ( next != '=' ) cin.putback( lookahead ); } // ... |
Упражнение 20.4
Прочитайте из стандартного ввода следующую последовательность символов, включая все пустые, и скопируйте каждый символ на стандартный вывод (эхо-копирование):
a b c
d e
f
Упражнение 20.5
Прочитайте фразу “riverrun, from bend of bay to swerve of shore” сначала как последовательность из девяти строк, а затем как одну строку.
Упражнение 20.6
С помощью функций getline() и gcount()
прочитайте последовательность строк из стандартного ввода и найдите самую длинную (не забудьте, что строку, прочитанную за несколько обращений к getline(), нужно считать одной).
Доступ к членам
Часто бывает так, что внутреннее представление типа класса изменяется в последующих версиях программы. Допустим, опрос пользователей нашего класса Screen
показал, что для его объектов всегда задается размер экрана 80 ´ 24. В таком случае было бы желательно заменить внутреннее представление экрана менее гибким, но более эффективным:
class Screen { public: // функции-члены private: // инициализация статических членов (см. 13.5) static const int _height = 24; static const int _width = 80; string _screen; string::size_type _cursor; |
};
Прежняя реализация функций-членов (то, как они манипулируют данными-членами класса) больше не годится, ее нужно переписать. Но это не означает, что должен измениться и интерфейс функций-членов (список формальных параметров и тип возвращаемого значения).
Если бы данные-члены класса Screen
были открыты и доступны любой функции внутри программы, как отразилось бы на пользователях изменение внутреннего представления этого класса?
·
все функции, которые напрямую обращались к данным-членам старого представления, перестали бы работать. Следовательно, пришлось бы отыскивать и изменять соответствующие части кода;
· так как интерфейс не изменился, то коды, манипулировавшие объектами класса Screen только через функции-члены, не пришлось бы модифицировать. Но поскольку сами функции-члены все же изменились, программу пришлось бы откомпилировать заново.
Сокрытие информации – это формальный механизм, предотвращающий прямой доступ к внутреннему представлению типа класса из функций программы. Ограничение доступа к членам задается с помощью секций тела класса, помеченных ключевыми словами public, private и protected – спецификаторами доступа. Члены, объявленные в секции public, называются открытыми, а объявленные в секциях private и protected
соответственно закрытыми или защищенными.
· открытый член
доступен из любого места программы. Класс, скрывающий информацию, оставляет открытыми только функции-члены, определяющие операции, с помощью которых внешняя программа может манипулировать его объектами;
· закрытый член
доступен только функциям-членам и друзьям
класса. Класс, который хочет скрыть информацию, объявляет свои данные-члены закрытыми;
· защищенный член
ведет себя как открытый по отношению к производному классу и как закрытый по отношению к остальной части программы. (В главе 2 мы видели пример использования защищенных членов в классе IntArray. Детально они рассматриваются в главе 17, где вводится понятие наследования.)
В следующем определении класса Screen
указаны секции public и private:
class Screen { public: void home() { _cursor = 0; } char get() { return _screen[_cursor]; } char get( int, int ); void move( int, int ); // ... private: string _screen; string::size_type _cursor; short _height, _width; |
Согласно принятому соглашению, сначала объявляются открытые члены класса. (Обсуждение того, почему в старых программах C++ сначала шли закрытые члены и почему этот стиль еще кое-где сохранился, см. в книге [LIPPMAN96a].) В теле класса может быть несколько секций public, protected и private. Каждая секция продолжается либо до метки следующей секции, либо до закрывающей фигурной скобки. Если спецификатор доступа не указан, то секция, непосредственно следующая за открывающей скобкой, по умолчанию считается private.
Доступ к членам базового класса
Объект производного класса фактически построен из нескольких частей. Каждый базовый класс вносит свою долю в виде подобъекта, составленного из нестатических данных-членов этого класса. Объект производного класса построен из подобъектов, соответствующих каждому из его базовых, а также из части, включающей нестатические члены самого производного класса. Так, наш объект NameQuery состоит из подобъекта Query, содержащего члены _loc и _solution, и части, принадлежащей NameQuery,– она содержит только член _name.
Внутри производного класса к членам, унаследованным из базового, можно обращаться напрямую, как к его собственным. (Глубина цепочки наследования не увеличивает затраты времени и не лимитирует доступ к ним.) Например:
void NameQuery:: display_partial_solution( ostream &os ) { os << _name << " is found in " << (_solution ? _solution->size() : 0) << " lines of text\n"; |
}
Это касается и доступа к унаследованным функциям-членам базового класса: мы вызываем их так, как если бы они были членами производного – либо через его объект:
NameQuery nq( "Frost" ); // вызывается NameQuery::eval() nq.eval(); // вызывается Query::display() |
nq.display();
либо непосредственно из тела другой (или той же самой) функции-члена:
void NameQuery:: match_count() { if ( ! _solution ) // вызывается Query::_vec2set() _solution = _vec2set( &_loc ); return _solution->size(); |
}
Однако прямой доступ из производного класса к членам базового запрещен, если имя последнего скрыто в производном классе:
class Diffident { public: // ... protected: int _mumble; // ... }; class Shy : public Diffident { public: // ... protected: // имя Diffident::_mumble скрыто string _mumble; // ... |
};
В области видимости Shy
употребление неквалифицированного имени _mumble разрешается в пользу члена _mumble
класса Shy
(объекта string), даже если такое использование в данном контексте недопустимо:
void Shy:: turn_eyes_down() { // ... _mumble = "excuse me"; // правильно // ошибка: int Diffident::_mumble скрыто _mumble = -1; |
Некоторые компиляторы помечают это как ошибку типизации. Для доступа к члену базового класса, имя которого скрыто в производном, необходимо квалифицировать имя члена базового класса именем самого этого класса с помощью оператора разрешения области видимости. Так выглядит правильная реализация функции-члена turn_eyes_down():
void Shy:: turn_eyes_down() { // ... _mumble = "excuse me"; // правильно // правильно: имя члена базового класса квалифицировано Diffident::_mumble = -1; |
Функции-члены базового и производного классов не составляют множество перегруженных функций:
class Diffident { public: void mumble( int softness ); // ... }; class Shy : public Diffident { public: // скрывает видимость функции-члена Diffident::_mumble, // а не перегружает ее void mumble( string whatYaSay ); void print( int soft, string words ); // ... |
Вызов функции-члена базового класса из производного в этом случае приводит к ошибке компиляции:
Shy simon; // правильно: Shy::mumble( string ) simon.mumble( "pardon me" ); // ошибка: ожидался первый аргумент типа string // Diffident::mumble( int ) невидима |
Хотя к членам базового класса можно обращаться напрямую, они сохраняют область видимости класса, в котором определены. А чтобы функции перегружали друг друга, они должны находиться в одной и той же области видимости. Если бы это было не так, следующие два экземпляра невиртуальной функции-члена turn_aside()
class Diffident { public: void turn_aside( ); // ... }; class Shy : public Diffident { public: // скрывает видимость // Diffident::turn_aside() void turn_aside(); // ... |
};
привели бы к ошибке повторного определения, так как их сигнатуры одинаковы. Однако запись правильна, поскольку каждая функция находится в области видимости того класса, в котором определена.
А если нам действительно нужен набор перегруженных функций-членов базового и производного классов? Написать в производном классе небольшую встроенную заглушку для вызова экземпляра из базового? Это возможно:
class Shy : public Diffident { public: // один из способов реализовать множество перегруженных // членов базового и производного классов void mumble( string whatYaSay ); void mumble( int softness ) { Diffident::mumble( softness ); } // ... |
Но в стандартном C++ тот же результат достигается посредством using-объявления:
class Shy : public Diffident { public: // в стандартном C++ using-объявление // создает множество перегруженных // членов базового и производного классов void mumble( string whatYaSay ); using Diffident::mumble; // ... |
По сути дела, using-объявление вводит каждый именованный член базового класса в область видимости производного. Поэтому такой член теперь входит в множество перегруженных функций, ассоциированных с именем функции-члена производного класса. (В ее using-объявлении нельзя указать список параметров, только имя. Это означает, что если некоторая функция уже перегружена в базовом классе, то в область видимости производного класса попадут все перегруженные экземпляры и, следовательно, добавить только одну из них невозможно.)
Обратим внимание на степень доступности защищенных членов базового класса. Когда мы пишем:
class Query { public: const vector<location>* locations() { return &_loc; } // ... protected: vector<location> _loc; // ... |
то имеем в виду, что класс, производный от Query, может напрямую обратиться к члену _loc, тогда как во всей остальной программе для этого необходимо пользоваться открытой функцией доступа. Однако объект производного класса имеет доступ только к защищенному члену _loc входящего в него
подобъекта, относящегося к базовому классу. Объект производного класса неспособен обратиться к защищенным членам другого независимого объекта базового класса:
bool NameQuery:: compare( const Query *pquery ) { // правильно: защищенный член подобъекта Query int myMatches = _loc.size(); // ошибка: нет прав доступа к защищенному члену // независимого объекта Query int itsMatches = pquery->_loc.size(); return myMatches == itsMatches; |
У объекта NameQuery
есть доступ к защищенным членам только одного объекта Query – подобъекта самого себя. Прямое обращение к ним из производного класса осуществляется через неявный указатель this (см. раздел 13.4). Первая реакция на ошибку компиляции – переписать функцию compare() с использованием открытой функции-члена location():
bool NameQuery:: compare( const Query *pquery ) { // правильно: защищенный член подобъекта Query int myMatches = _loc.size(); // правильно: используется открытый метод доступа int itsMatches = pquery->locations()->size(); return myMatches == itsMatches; |
Однако проблема заключается в неправильном проектировании. Поскольку _loc – это член базового класса Query, то место compare()
среди членов базового, а не производного класса. Во многих случаях подобные проблемы могут быть решены путем переноса некоторой операции в тот класс, где находится недоступный член, как в приведенном примере.
Этот вид ограничения доступа не распространяется на доступ изнутри класса к другим объектам того же класса:
bool NameQuery:: compare( const NameQuery *pname ) { int myMatches = _loc.size(); // правильно int itsMatches = name->_loc.size(); // тоже правильно return myMatches == itsMatches; |
Производный класс может напрямую обращаться к защищенным членам базового в других объектах того же класса, что и он сам, равно как и к защищенным и закрытым членам других объектов своего класса.
Рассмотрим инициализацию указателя на базовый Query адресом объекта производного NameQuery:
Query *pb = new NameQuery( "sprite" );
При вызове виртуальной функции, определенной в базовом классе Query, например:
pb->eval(); // вызывается NameQuery::eval()
вызывается функция из NameQuery. За исключением вызова виртуальной функции, объявленной в Query и переопределенной в NameQuery, другого способа напрямую добраться до членов класса NameQuery
через указатель pb не существует:
(a) если в Query и NameQuery
объявлены некоторые невиртуальные функции-члены с одинаковым именем, то через pb
всегда вызывается экземпляр из Query;
(b) если в Query и NameQuery
объявлены одноименные члены, то через pb обращение происходит к члену класса Query;
(c) если в NameQuery
имеется виртуальная функция, отсутствующая в Query, скажем suffix(), то попытка вызвать ее через pb приводит к ошибке компиляции:
// ошибка: suffix() - не член класса Query |
· Обращение к члену или невиртуальной функции-члену класса NameQuery
через pb
тоже вызывает ошибку компиляции:
// ошибка: _name - не член класса Query |
Квалификация имени члена в этом случае не помогает:
// ошибка: у класса Query нет базового класса NameQuery |
В C++ с помощью указателя на базовый класс можно работать только с данными и функциями-членами, включая виртуальные, которые объявлены (или унаследованы) в самом этом классе, независимо от того, какой фактический объект адресуется указателем. Объявление функции-члена виртуальной откладывает решение вопроса о том, какой экземпляр функции вызвать, до выяснения (во время выполнения программы) фактического типа объекта, адресуемого pb.
Такой подход может показаться недостаточно гибким, но у него есть два весомых преимущества:
· поиск виртуальной функции-члена во время выполнения никогда не закончится неудачно из-за того, что фактический тип класса не существует. В таком случае программа просто не смогла бы откомпилироваться;
· механизм виртуализации можно оптимизировать. Часто вызов такой функции оказывается не дороже, чем косвенный вызов функции по указателю (детально этот вопрос рассмотрен в [LIPPMAN96a]).
В базовом классе Query
определен статический член _text_file:
static vector<string> *_text_file;
Создается ли при порождении класса NameQuery
второй экземпляр _text_file, уникальный именно для него? Нет. Все объекты производного класса ссылаются на тот же самый, единственный разделяемый статический член. Сколько бы ни было производных классов, существует лишь один экземпляр _text_file. Можно обратиться к нему через объект производного класса с помощью синтаксиса доступа:
nameQueryObject._text_file; // правильно
Наконец, если производный класс хочет получить доступ к закрытым членам своего базового класса напрямую, то он должен быть объявлен другом базового:
class Query { friend class NameQuery; public: // ... |
Теперь объект NameQuery
может обращаться не только к закрытым членам своего подобъекта, соответствующего базовому классу, но и к закрытым и защищенным членам любых объектов Query.
А если мы произведем от NameQuery
класс StringQuery? Он будет поддерживать сокращенную форму запроса AndQuery, и вместо
beautiful && fiery && bird
можно будет написать:
"beautiful fiery bird"
Унаследует ли StringQuery от класса NameQuery
дружественные отношения с Query? Нет. Отношение дружественности не наследуется. Производный класс не становится другом класса, который объявил своим другом один из базовых. Если производному классу требуется стать другом одного или более классов, то эти классы должны предоставить ему соответствующие права явно. Например, у класса StringQuery нет никаких специальных прав доступа по отношению к Query. Если расширенный доступ необходим, то Query должен разрешить его явно.
Упражнение 17.6
Даны следующие определения базового и производных классов:
class Base { public: foo( int ); // ... protected: int _bar; double _foo_bar; }; class Derived : public Base { public: foo( string ); bool bar( Base *pb ); void foobar(); // ... protected: string _bar; |
Исправьте ошибки в каждом из следующих фрагментов кода:
Derived d; d.foo( 1024 ); |
(c) bool Derived::bar( Base *pb ) |
Доступ к членам класса
Говорят, что определение функции-члена принадлежит области видимости класса независимо от того, находится ли оно вне или внутри его тела. Отсюда следуют два вывода:
· в определении функции-члена могут быть обращения к любым членам класса, открытым или закрытым, и это не нарушает ограничений доступа;
· когда функция-член обращается к членам класса, операторы доступа “точка” и “стрелка” не необходимы.
Например:
#include <string> void Screen::copy( const Screen &sobj ) { // если этот объект и объект sobj - одно и то же, // копирование излишне // мы анализируем указатель this (см. раздел 13.4) if ( this != &sobj ) { _height = sobj._height; _width = sobj._width; _cursor = 0; // создаем новую строку; // ее содержимое такое же, как sobj._screen _screen = sobj._screen; } |
}
Хотя _screen, _height, _width и _cursor
являются закрытыми членами класса Screen, функция-член copy()
работает с ними напрямую. Если при обращении к члену отсутствует оператор доступа, то считается, что речь идет о члене того класса, для которого функция-член вызвана. Если вызвать copy() следующим образом:
#include "Screen.h" int main() { Screen s1; // Установить s1 Screen s2; s2.copy(s1); // ... |
}
то параметр sobj
внутри определения copy()
соотносится с объектом s1 из функции main(). Функция-член copy() вызвана для объекта s2, стоящего перед оператором “точка”. Для такого вызова члены _screen, _height, _width и _cursor, при обращении к которым внутри определения этой функции нет оператора доступа, – это члены объекта s2. В следующем разделе мы рассмотрим доступ к членам класса внутри определения функции-члена более подробно и, в частности, покажем, как для поддержки такого доступа применяется указатель this.
Друзья
Иногда удобно разрешить некоторым функциям доступ к закрытым членам класса. Механизм друзей
позволяет классу разрешать доступ к своим неоткрытым членам.
Объявление друга начинается с ключевого слова friend и может встречаться только внутри определения класса. Так как друзья не являются членами класса, то не имеет значения, в какой секции они объявлены. В примере ниже мы сгруппировали все подобные объявления сразу после заголовка класса:
class Screen { friend istream& operator>>( istream&, Screen& ); friend ostream& operator<<( ostream&, const Screen& ); public: // ... оставшаяся часть класса Screen |
};
Операторы ввода и вывода теперь могут напрямую обращаться к закрытым членам класса Screen. Простая реализация оператора вывода выглядит следующим образом:
#include <iostream> ostream& operator<<( ostream& os, const Screen& s ) { // правильно: можно обращаться к _height, _width и _screen os << "<" << s._height << "," << s._width << ">"; os << s._screen; return os; |
}
Другом может быть функция из пространства имен, функция-член другого класса или даже целый класс. В последнем случае всем его функциям-членам предоставляется доступ к неоткрытым членам класса, объявляющего дружественные отношения. (В разделе 15.2 друзья обсуждаются более подробно.)
Рассмотрим еще раз перегруженные операторы равенства для класса String, определенные в области видимости пространства имен. Оператор равенства для двух объектов String
выглядит следующим образом:
bool operator==( const String &str1, const String &str2 ) { if ( str1.size() != str2.size() ) return false; return strcmp( str1.c_str(), str2.c_str() ) ? false : true; |
}
Сравните это определение с определением того же оператора как функции-члена:
bool String::operator==( const String &rhs ) const { if ( _size != rhs._size ) return false; return strcmp( _string, rhs._string ) ? false : true; |
}
Нам пришлось модифицировать способ обращения к закрытым членам класса String. Поскольку новый оператор равенства– это глобальная функция, а не функция-член, у него нет доступа к закрытым членам класса String. Для получения размера объекта String и лежащей в его основе C-строки символов используются функции-члены size() и c_str().
Альтернативной реализацией является объявление глобальных операторов равенства друзьями класса String. Если функция или оператор объявлены таким образом, им предоставляется доступ к неоткрытым членам.
Объявление друга (оно начинается с ключевого слова friend) встречается только внутри определения класса. Поскольку друзья не являются членами класса, объявляющего дружественные отношения, то безразлично, в какой из секций – public, private или protected – они объявлены. В примере ниже мы решили поместить все подобные объявления сразу после заголовка класса:
class String { friend bool operator==( const String &, const String & ); friend bool operator==( const char *, const String & ); friend bool operator==( const String &, const char * ); public: // ... остальная часть класса String |
};
В этих трех строчках три перегруженных оператора сравнения, принадлежащие глобальной области видимости, объявляются друзьями класса String, а следовательно, в их определениях можно напрямую обращаться к закрытым членам данного класса:
Еще раз о разрешении перегрузки функций
В главе 9 подробно описывалось, как разрешается вызов перегруженной функции. Если фактические аргументы при вызове имеют тип класса, указателя на тип класса или указателя на члены класса, то на роль возможных кандидатов претендует большее число функций. Следовательно, наличие таких аргументов оказывает влияние на первый шаг процедуры разрешения перегрузки – отбор множества функций-кандидатов.
На третьем шаге этой процедуры выбирается наилучшее соответствие. При этом ранжируются преобразования типов фактических аргументов в типы формальных параметров функции. Если аргументы и параметры имеют тип класса, то в множество возможных преобразований следует включать и последовательности определенных пользователем преобразований, также подвергая их ранжированию.
В этом разделе мы детально рассмотрим, как фактические аргументы и формальные параметры типа класса влияют на отбор функций-кандидатов и как последовательности определенных пользователем преобразований сказываются на выборе наилучшей из устоявших функции.
Еще раз об итераторах
Следующая реализация шаблона функции не компилируется. Можете ли вы сказать, почему?
// в таком виде это не компилируется template < typename type > int count( const vector< type > &vec, type value ) { int count = 0; vector< type >::iterator iter = vec.begin(); while ( iter != vec.end() ) if ( *iter == value ) ++count; return count; |
}
Проблема в том, что у ссылки vec
есть спецификатор const, а мы пытаемся связать с ней итератор без такого спецификатора. Если бы это было разрешено, то ничто не помешало бы нам модифицировать с помощью этого итератора элементы вектора. Для предотвращения подобной ситуации язык требует, чтобы итератор, связанный с const-вектором, был константным. Мы можем сделать это следующим образом:
// правильно: это компилируется без ошибок |
vector< type>::const_iterator iter = vec.begin();
Требование, чтобы с const-контейнером был связан только константный итератор, аналогично требованию о том, чтобы const-массив адресовался только константным указателем. В обоих случаях это вызвано необходимостью гарантировать, что содержимое const-контейнера не будет изменено.
Операции begin() и end()
перегружены и возвращают константный или неконстантный итератор в зависимости от наличия спецификатора const в объявлении контейнера. Если дана такая пара объявлений:
vector< int > vec0; |
const vector< int > vec1;
то при обращениях к begin() и end() для vec0
будет возвращен неконстантный, а для vec1 – константный итератор:
vector< int >::iterator iter0 = vec0.begin(); |
vector< int >::const_iterator iter1 = vec1.begin();
Разумеется, присваивание константному итератору неконстантного разрешено всегда. Например:
// правильно: инициализация константного итератора неконстантным |
vector< int >::const_iterator iter2 = vec0.begin();
Файловый ввод/вывод
Библиотека iostream
поддерживает и файловый ввод/вывод. Все операции, применимые в стандартному вводу и выводу, могут быть также применены к файлам. Чтобы использовать файл для ввода или вывода, мы должны включить еще один заголовочный файл:
#include <fstream>
Перед тем как открыть файл для вывода, необходимо объявить объект типа ofstream:
ofstream outfile("name-of-file");
Проверить, удалось ли нам
открыть файл, можно следующим образом:
if ( ! outfile ) // false, если файл не открыт |
cerr << "Ошибка открытия файла.\n"
Так же открывается файл и для ввода, только он имеет тип ifstream:
ifstream infile("name-of-file"); |
if ( ! infile ) // false, если файл не открыт
cerr << "Ошибка открытия файла.\n"
Ниже приводится текст простой программы, которая читает файл с именем in_file и выводит все прочитанные из этого файла слова, разделяя их пробелом, в другой файл, названный out_file.
#include <iostream> #include <fstream> #include <string> int main() { ifstream infile("in_file"); ofstream outfile("out_file"); if ( ! infile ) { cerr << "Ошибка открытия входного файла.\n" return -1; } if ( ! outfile ) { cerr << "Ошибка открытия выходного файла.\n" return -2; } |
string word;
while ( infile >> word )
outfile << word << ' ';
return 0;
}
В главе 20 библиотека ввода/вывода будет рассмотрена подробно. А в следующих разделах мы увидим, как можно создавать новые типы данных, используя механизм классов и шаблонов.
Функции
Мы рассмотрели, как объявлять переменные (глава 3), как писать выражения (глава 4) и инструкции (глава 5). Здесь мы покажем, как группировать эти компоненты в определения функций, чтобы облегчить их многократное использование внутри программы. Мы увидим, как объявлять и определять функции и как вызывать их, рассмотрим различные виды передаваемых параметров и обсудим особенности использования каждого вида. Мы расскажем также о различных видах значений, которые может вернуть функция. Будут представлены четыре специальных случая применения функций: встроенные (inline), рекурсивные, написанные на других языках и объявленные директивами связывания, а также функция main(). В завершение главы мы разберем более сложное понятие – указатель на функцию.
Функции-члены
Пользователям, по-видимому, понадобится широкий набор операций над объектами типа Screen: возможность перемещать курсор, проверять и устанавливать области экрана и рассчитывать его реальные размеры во время выполнения, а также копировать один объект в другой. Все эти операции можно реализовать с помощью функций-членов.
Функции-члены класса объявляются в его теле. Это объявление выглядит точно так же, как объявление функции в области видимости пространства имен. (Напомним, что глобальная область видимости – это тоже область видимости пространства имен. Глобальные функции рассматривались в разделе 8.2, а пространства имен – в разделе 8.5.) Например:
class Screen { public: void home(); void move( int, int ); char get(); char get( int, int ); void checkRange( int, int ); // ... |
};
Определение функции-члена также можно поместить внутрь тела класса:
class Screen { public: // определения функций home() и get() void home() { _cursor = 0; } char get() { return _screen[_cursor]; } // ... |
};
home() перемещает курсор в левый верхний угол экрана; get()
возвращает символ, находящийся в текущей позиции курсора.
Функции-члены отличаются от обычных функций следующим:
· функция-член объявлена в области видимости своего класса, следовательно, ее имя не видно за пределами этой области. К функции-члену можно обратиться с помощью одного из операторов доступа к членам – точки (.) или стрелки (->):
ptrScreen->home(); |
myScreen.home();
(в разделе 13.9 область видимости класса обсуждается более детально);
· функции-члены имеют право доступа как к открытым, так и к закрытым членам класса, тогда как обычным функциям доступны лишь открытые. Конечно, функции-члены одного класса, как правило, не имеют доступа к данным-членам другого класса.
Функция-член может быть перегруженной (перегруженные функции рассматриваются в главе 9). Однако она способна перегружать лишь другую функцию-член своего класса. По отношению к функциям, объявленным в других классах или пространствах имен, функция-член находится в отдельной области видимости и, следовательно, не может перегружать их. Например, объявление get(int, int) перегружает лишь get() из того же класса Screen:
class Screen { public: // объявления перегруженных функций-членов get() char get() { return _screen[_cursor]; } char get( int, int ); // ... |
};
(Подробнее мы остановимся на функциях-членах класса в разделе 13.3.)
Функции-члены класса
Функции-члены реализуют набор операций, применимых к объектам класса. Например, для Screen
такой набор состоит из следующих объявленных в нем функций-членов:
class Screen { public: void home() { _cursor = 0; } char get() { return _screen[_cursor]; } char get( int, int ); void move( int, int ); bool checkRange( int, int ); int height() { return _height; } int width() { return _width; } // ... |
};
Хотя у любого объекта класса есть собственная копия всех данных-членов, каждая функция-член существует в единственном экземпляре:
Screen myScreen, groupScreen; myScreen.home(); |
groupScreen.home();
При вызове функции home() для объекта myScreen
происходит обращение к его члену _cursor. Когда же эта функция вызывается для объекта groupScreen, то она обращается к члену _cursor именно этого объекта, причем сама функция home() одна и та же. Как же может одна функция-член обращаться к данным-членам разных объектов? Для этого применяется указатель this, рассматриваемый в следующем разделе.
Функции-члены шаблонов классов
Как и для обычных классов, функция-член шаблона класса может быть определена либо внутри определения шаблона (и тогда называется встроенной), либо вне его. Мы уже встречались со встроенными функциями-членами при рассмотрении шаблона Queue. Например, конструктор Queue
является встроенным, так как определен внутри определения шаблона класса:
template <class Type> class Queue { // ... public: // встроенный конструктор Queue() : front( 0 ), back( 0 ) { } // ... |
};
При определении функции-члена шаблона вне определения самого шаблона следует применять специальный синтаксис для обозначения того, членом какого именно шаблона является функция. Определению функции-члена должно предшествовать ключевое слово template, за которым следуют параметры шаблона. Так, конструктор Queue
можно определить следующим образом:
template <class Type> class Queue { public: Queue(); private: // ... }; template <class Type> inline Queue<Type>:: |
Queue( ) { front = back = 0; }
За первым вхождением Queue
(перед оператором ::) следует список параметров, показывающий, какому шаблону принадлежит данная функция-член. Второе вхождение Queue в определение конструктора (после оператора ::) содержит имя функции-члена, за которым может следовать список параметров шаблона, хотя это и необязательно. После имени функции идет ее определение;. в нем могут быть ссылки на параметр шаблона Type всюду, где в определении обычной функции использовалось бы имя типа.
Функция-член шаблона класса сама является шаблоном. Стандарт C++ требует, чтобы она конкретизировалась только при вызове либо при взятии ее адреса. (Некоторые более старые компиляторы конкретизируют такие функции одновременно с конкретизацией самого шаблона класса.) При конкретизации функции-члена используется тип того объекта, для которого функция вызвана:
Queue<string> qs;
Объект qs
имеет тип Queue<string>. При инициализации объекта этого класса вызывается конструктор Queue<string>. В данном случае аргументом, которым конкретизируется функция-член (конструктор), будет string.
Функция-член шаблона конкретизируется только при реальном использовании в программе (т.е. при вызове или взятии ее адреса). От того, в какой именно момент конкретизируется функция-член, зависит разрешение имен в ее определении (см. раздел 16.11) и объявление ее специализации (см. раздел 16.9).
Функции-члены шаблонов Queue и QueueItem
Чтобы понять, как определяются и используются функции-члены шаблонов классов, продолжим изучение шаблонов Queue и QueueItem:
template <class Type> class Queue { public: Queue() : front( 0 ), back ( 0 ) { } ~Queue(); Type& remove(); void add( const Type & ); bool is_empty() const { return front == 0; } private: QueueItem<Type> *front; QueueItem<Type> *back; |
};
Деструктор, а также функции-члены remove() и add()
определены не в теле шаблона, а вне его. Деструктор Queue
опустошает очередь:
template <class Type> Queue<Type>::~Queue() { while (! is_empty() ) remove(); |
}
Функция-член Queue<Type>::add()
помещает новый элемент в конец очереди:
template <class Type> void Queue<Type>::add( const Type &val ) { // создать новый объект QueueItem QueueItem<Type> *pt = new QueueItem<Type>( val ); if ( is_empty() ) front = back = pt; else { back->next = pt; back = pt; } |
}
Функция-член Queue<Type>::remove()
возвращает значение элемента, находящегося в начале очереди, и удаляет сам элемент.
#include <iostream> #include <cstdlib> template <class Type> Type Queue<Type>::remove() { if ( is_empty() ) { cerr << "remove() вызвана для пустой очереди\n"; exit( -1 ); } QueueItem<Type> *pt = front; front = front->next; Type retval = pt->item; delete pt; return retval; |
}
Мы поместили определения функций-членов в заголовочный файл Queue.h, включив его в каждый файл, где возможны конкретизации функций. (Обоснование этого решения, а также рассмотрение более общих вопросов, касающихся модели компиляции шаблонов, мы отложим до раздела 16.8.)
В следующей программе иллюстрируется использование и конкретизация функции-члена шаблона Queue:
#include <iostream> #include "Queue.h" int main() { // конкретизируется класс Queue<int> // оператор new требует, чтобы Queue<int> был определен Queue<int> *p_qi = new Queue<int>; int ival; for ( ival = 0; ival < 10; ++ival ) // конкретизируется функция-член add() p_qi->add( ival ); int err_cnt = 0; for ( ival = 0; ival < 10; ++ival ) { // конкретизируется функция-член remove() int qval = p_qi->remove(); if ( ival != qval ) err_cnt++; } if ( !err_cnt ) cout << "!! queue executed ok\n"; else cerr << "?? queue errors: " << err_cnt << endl; return 0; |
}
После компиляции и запуска программа выводит следующую строку:
!! queue executed ok
Упражнение 16.5
Используя шаблон класса Screen, определенный в разделе 16.2, реализуйте функции-члены Screen
(см. разделы 13.3, 13.4 и 13.6) в виде функций-членов шаблона.
Функции-члены со спецификаторами const и volatile
Любая попытка модифицировать константный объект из программы обычно помечается компилятором как ошибка. Например:
const char blank = ' '; |
blank = '\n'; // ошибка
Однако объект класса, как правило, не модифицируется программой напрямую. Вместо этого вызывается та или иная открытая функция-член. Чтобы не было “покушений” на константность объекта, компилятор должен различать безопасные (те, которые не изменяют объект) и небезопасные (те, которые пытаются это сделать) функции-члены:
const Screen blankScreen; blankScreen.display(); // читает объект класса |
blankScreen.set( '*' ); // ошибка: модифицирует объект класса
Проектировщик класса может указать, какие функции-члены не модифицируют объект, объявив их константными с помощью спецификатора const:
class Screen { public: char get() const { return _screen[_cursor]; } // ... |
};
Для класса, объявленного как const, могут быть вызваны только те функции-члены, которые также объявлены со спецификатором const. Ключевое слово const
помещается между списком параметров и телом функции-члена. Для константной функции-члена, определенной вне тела класса, это слово должно присутствовать как в объявлении, так и в определении:
class Screen { public: bool isEqual( char ch ) const; // ... private: string::size_type _cursor; string _screen; // ... }; bool Screen::isEqual( char ch ) const { return ch == _screen[_cursor]; |
}
Запрещено объявлять константную функцию-член, которая модифицирует члены класса. Например, в следующем упрощенном определении:
class Screen { public: int ok() const { return _cursor; } void error( int ival ) const { _cursor = ival; } // ... private: string::size_type _cursor; // ... |
};
определение функции-члена ok()
корректно, так как она не изменяет значения _cursor. В определении же error()
значение _cursor
изменяется, поэтому такая функция-член не может быть объявлена константной и компилятор выдает сообщение об ошибке:
error: cannot modify a data member within a const member function
ошибка: не могу модифицировать данные-члены внутри константной функции-члена
Если класс будет интенсивно использоваться, лучше объявить его функции-члены, не модифицирующие данных, константными. Однако наличие спецификатора const в объявлении функции-члена не предотвращает все возможные изменения. Такое объявление гарантирует лишь, что функции-члены не смогут изменять данные-члены, но если класс содержит указатели, то адресуемые ими объекты могут быть модифицированы константной функцией, не вызывая ошибки компиляции. Это часто приводит в недоумение начинающих программистов. Например:
#include <cstring> class Text { public: void bad( const string &parm ) const; private: char *_text; }; void Text::bad( const string &parm ) const { _text = parm.c_str(); // ошибка: нельзя модифицировать _text for ( int ix = 0; ix < parm.size(); ++ix ) _text[ix] = parm[ix]; // плохой стиль, но не ошибка |
Модифицировать _text
нельзя, но это объект типа char*, и символы, на которые он указывает, можно изменить внутри константной функции-члена класса Text. Функция-член bad()
демонстрирует плохой стиль программирования. Константность функции-члена не гарантирует, что объекты внутри класса останутся неизменными после ее вызова, причем компилятор не поможет обнаружить такую ситуацию.
Константную функцию-член можно перегружать неконстантной функцией с тем же списком параметров:
class Screen { public: char get(int x, int y); char get(int x, int y) const; // ... |
В этом случае наличие спецификатора const у объекта класса определяет, какая из двух функций будет вызвана:
int main() { const Screen cs; Screen s; char ch = cs.get(0,0); // вызывает константную функцию-член ch = s.get(0,0); // вызывает неконстантную функцию-член |
Хотя конструкторы и деструкторы не являются константными функциями-членами, они все же могут вызываться для константных объектов. Объект становится константным после того, как конструктор проинициализирует его, и перестает быть таковым, как только вызывается деструктор. Таким образом, объект со спецификатором const
трактуется как константный с момента завершения работы конструктора и до вызова деструктора.
Функцию-член можно также объявить со спецификатором volatile (он был введен в разделе 3.13). Объект класса объявляется как volatile, если его значение изменяется способом, который не обнаруживается компилятором (например, если это структура данных, представляющая порт ввода/вывода). Для таких объектов вызываются только функции-члены с тем же спецификатором, конструкторы и деструкторы:
class Screen { public: char poll() volatile; // ... }; |
Функции-кандидаты
Функцией-кандидатом называется функция, имеющая то же имя, что и вызванная. Кандидаты отыскиваются двумя способами:
·
объявление функции видимо в точке вызова. В следующем примере
void f(); void f( int ); void f( double, double = 3.4 ); void f( char*, char* ); int main() { f( 5.6 ); // для разрешения этого вызова есть четыре кандидата return 0; |
}
все четыре функции f()
удовлетворяют этому условию. Поэтому множество кандидатов содержит четыре элемента;
· если тип фактического аргумента объявлен внутри некоторого пространства имен, то функции-члены этого пространства, имеющие то же имя, что и вызванная функция, добавляются в множество кандидатов:
namespace NS { class C { /* ... */ }; void takeC( C& ); } // тип cobj - это класс C, объявленный в пространстве имен NS NS::C obj; int main() { // в точке вызова не видна ни одна из функций takeC() takeC( cobj); // правильно: вызывается NS::takeC( C& ), // потому что аргумент имеет тип NS::C, следовательно, // принимается во внимание функция takeC(), // объявленная в пространстве имен NS return 0; |
}
Таким образом, совокупность кандидатов является объединением множества функций, видимых в точке вызова, и множества функций, объявленных в том же пространстве имен, к которому принадлежат типы фактических аргументов.
При идентификации множества перегруженных функций, видимых в точке вызова, применимы уже рассмотренные ранее правила.
Функция, объявленная во вложенной области видимости, скрывает, а не перегружает одноименную функцию во внешней области. В такой ситуации кандидатами будут только функции из во вложенной области, т.е. такие, которые не скрыты при вызове. В следующем примере функциями-кандидатами, видимыми в точке вызова, являются format(double) и format(char*):
Функцией-кандидатом называется функция с тем же именем, что и вызванная. Предположим, что имеется такой вызов:
SmallInt si(15); |
add( si, 566 );
Функция-кандидат должна иметь имя add. Какие из объявлений add()
принимаются во внимание? Те, которые видимы в точке вызова.
Например, обе функции add(), объявленные в глобальной области видимости, будут кандидатами для следующего вызова:
const matrix& add( const matrix &, int ); double add( double, double ); int main() { SmallInt si(15); add( si, 566 ); // ... |
}
Рассмотрение функций, чьи объявления видны в точке вызова, производится не только для вызовов с аргументами типа класса. Однако для них поиск объявлений проводится еще в двух областях видимости:
· если фактический аргумент – это объект типа класса, указатель или ссылка на тип класса либо указатель на член класса и этот тип объявлен в пользовательском пространстве имен, то к множеству функций-кандидатов добавляются функции, объявленные в этом же пространстве и имеющие то же имя, что и вызванная:
namespace NS { class SmallInt { /* ... */ }; class String { /* ... */ }; String add( const String &, const String & ); } int main() { // si имеет тип class SmallInt: // класс объявлен в пространстве имен NS NS::SmallInt si(15); add( si, 566 ); // NS::add() - функция-кандидат return 0; |
}
Аргумент si
имеет тип SmallInt, т.е. тип класса, объявленного в пространстве имен NS. Поэтому к множеству функций-кандидатов добавляется add(const String &, const String &), объявленная в этом пространстве имен;
· если фактический аргумент – это объект типа класса, указатель или ссылка на класс либо указатель на член класса и у этого класса есть друзья, имеющие то же имя, что и вызванная функция, то они добавляются к множеству функций-кандидатов:
namespace NS { class SmallInt { friend SmallInt add( SmallInt, int ) { /* ... */ } }; } int main() { NS::SmallInt si(15); add( si, 566 ); // функция-друг add() - кандидат return 0; |
Наследование влияет на первый шаг процедуры разрешения перегрузки функции – формирование множества кандидатов для данного вызова, причем это влияние может быть различным в зависимости от того, рассматривается ли вызов обычной функции вида
func( args );
или функции-члена с помощью операторов доступа “точка” или “стрелка”:
object.memfunc( args ); |
pointer->memfunc( args );
В данном разделе мы изучим оба случая.
Если аргумент обычной функции имеет тип класса, ссылки или указателя на тип класса, и класс определен в пространстве имен, то кандидатами будут все одноименные функции, объявленные в этом пространстве, даже если они невидимы в точке вызова (подробнее об этом говорилось в разделе 15.10). Если аргумент при наследовании имеет тип класса, ссылки или указателя на тип класса, и у этого класса есть базовые, то в множество кандидатов добавляются также функции, объявленные в тех пространствах имен, где определены базовые классы. Например:
namespace NS { class ZooAnimal { /* ... */ }; void display( const ZooAnimal& ); } // базовый класс Bear объявлен в пространстве имен NS class Bear : public NS::ZooAnimal { }; int main() { Bear baloo; display( baloo ); return 0; |
}
Аргумент baloo
имеет тип класса Bear. Кандидатами для вызова display() будут не только функции, объявления которых видимы в точке ее вызова, но также и те, что объявлены в пространствах имен, в которых объявлены класс Bear и его базовый класс ZooAnimal. Поэтому в множество кандидатов добавляется функция display(const ZooAnimal&), объявленная в пространстве имен NS.
Если аргумент имеет тип класса и в определении этого класса объявлены функции-друзья с тем же именем, что и вызванная функция, то эти друзья также будут кандидатами, даже если их объявления не видны в точке вызова (см. раздел 15.10). Если аргумент при наследовании имеет тип класса, у которого есть базовые, то в множество кандидатов добавляются одноименные функции-друзья каждого из них. Предположим, что в предыдущем примере display() объявлена как функция-друг ZooAnimal:
Рассмотрим два вида вызовов функции-члена:
mc.mf( arg ); |
pmc->mf( arg );
где mc – выражение типа myClass, а pmc – выражение типа “указатель на тип myClass”. Множество кандидатов для обоих вызовов составлено из функций, найденных в области видимости класса myClass при поиске объявления mf().
Аналогично для вызова функции вида
myClass::mf( arg );
множество кандидатов также состоит из функций, найденных в области видимости класса myClass при поиске объявления mf(). Например:
class myClass { public: void mf( double ); void mf( char, char = '\n' ); static void mf( int* ); // ... }; int main() { myClass mc; int iobj; mc.mf( iobj ); |
}
Кандидатами для вызова функции в main()
являются все три функции-члена mf(), объявленные в myClass:
void mf( double ); void mf( char, char = '\n' ); |
static void mf( int* );
Если бы в myClass не было объявлено ни одной функции-члена с именем mf(), то множество кандидатов оказалось бы пустым. (На самом деле рассматривались бы также и функции из базовых классов. О том, как они попадают в это множество, мы поговорим в разделе 19.3.) Если для вызова функции не оказывается кандидатов, компилятор выдает сообщение об ошибке.
Функции-кандидаты для вызова функции в области видимости класса
Когда вызов функции вида
calc(t)
встречается в области видимости класса (например, внутри функции-члена), то первая часть множества кандидатов, описанного в предыдущем подразделе (т.е. множество, включающее объявления функций, видимых в точке вызова), может содержать не только функции-члены класса. Для построения такого множества применяется разрешение имени. (Эта тема детально разбиралась в разделах 13.9 – 13.12.)
Рассмотрим пример:
namespace NS { struct myClass { void k( int ); static void k( char* ); void mf(); }; int k( double ); }; void h(char); void NS::myClass::mf() { h('a'); // вызывается глобальная h( char ) k(4); // вызывается myClass::k( int ) |
}
Как отмечалось в разделе 13.11, квалификаторы NS::myClass:: просматриваются в обратном порядке: сначала поиск видимого объявления для имени, использованного в определении функции-члена mf(), ведется в классе myClass, а затем – в пространстве имен NS. Рассмотрим первый вызов:
h( 'a' );
При разрешении имени h() в определении функции-члена mf() сначала просматриваются функции-члены myClass. Поскольку функции-члена с таким именем в области видимости этого класса нет, то далее поиск идет в пространстве имен NS. Функции h()нет и там, поэтому мы переходим в глобальную область видимости. Результат – глобальная функция h(char), единственная функция-кандидат, видимая в точке вызова.
Как только найдено подходящее объявление, поиск прекращается. Следовательно, множество содержит только те функции, объявления которых находятся в областях видимости, где разрешение имени завершилось успешно. Это можно наблюдать на примере построения множества кандидатов для вызова
k( 4 );
Сначала поиск ведется в области видимости класса myClass. При этом найдены две функции-члена k(int) и k(char*). Поскольку множество кандидатов содержит лишь функции, объявленные в той области, где разрешение успешно завершилось, то пространство имен NS не просматривается и функция k(double) в данное множество не включается.
Если обнаруживается, что вызов неоднозначен, поскольку в множестве нет наиболее подходящей функции, то компилятор выдает сообщение об ошибке. Поиск кандидатов, лучше соответствующих фактическим аргументам, в объемлющих областях видимости не производится.
Функция main(): разбор параметров командной строки
При запуске программы мы, как правило, передаем ей информацию в командной строке. Например, можно написать
prog -d -o of lie dataO
Фактические параметры являются аргументами функции main() и могут быть получены из массива C-строк с именем argv; мы покажем, как их использовать.
Во всех предыдущих примерах определение main()
содержало пустой список:
int main() { ... }
Развернутая сигнатура main()
позволяет получить доступ к параметрам, которые были заданы пользователем в командной строке:
int main( int argc, char *argv[] ){...}
argc
содержит их количество, а argv – C-строки, представляющие собой отдельные значения (в командной строке они разделяются пробелами). Скажем, при запуске команды
prog -d -o ofile data0
argc
получает значение 5, а argv
включает следующие строки:
argv[ 0 ] = "prog";
argv[ 1 ] = "-d";
argv[ 2 ] = "-o";
argv[ 3 ] = "ofile";
argv[ 4 ] = "dataO";
В argv[0]
всегда входит имя команды (программы). Элементы с индексами от 1 до argc-1
служат параметрами.
Посмотрим, как можно извлечь и использовать значения, помещенные в argv. Пусть программа из нашего примера вызывается таким образом:
prog [-d] [-h] [-v] [-o output_file] [-l limit_value] file_name |
[ file_name [file_name [ ... ]]]
Параметры в квадратных скобках являются необязательными. Вот, например, запуск программы с их минимальным количеством – одним лишь именем файла:
prog chap1.doc
Но можно запускать и так:
prog -l 1024 -o chap1-2.out chapl.doc chap2.doc prog d chap3.doc |
prog -l 512 -d chap4.doc
При разборе параметров командной строки выполняются следующие основные шаги:
1. По очереди извлечь каждый параметр из argv. Мы используем для этого цикл for с начальным индексом 1
(пропуская, таким образом, имя программы):
for ( int ix = 1; ix < argc; ++ix ) { char *pchar = argv[ ix ]; // ... |
}
2. Определить тип параметра. Если строка начинается с дефиса (-), это одна из опций { h, d, v, l, o}. В противном случае это может быть либо значение, ассоциированное с опцией (максимальный размер для -l, имя выходного файла для -o), либо имя входного файла. Чтобы определить, начинается ли строка с дефиса, используем инструкцию switch:
switch ( pchar[ 0 ] ) { case '-': { // -h, -d, -v, -l, -o } default: { // обработаем максимальный размер для опции -1 // имя выходного файла для -o // имена входных файлов ... } |
Реализуем обработку двух случаев пункта 2.
Если строка начинается с дефиса, мы используем switch по следующему символу для определения конкретной опции. Вот общая схема этой части программы:
case '-': { switch( pchar[ 1 ] ) { case 'd': // обработка опции debug break; case 'v': // обработка опции version break; case 'h': // обработка опции help break; case 'o': // приготовимся обработать выходной файл break; case 'l': // приготовимся обработать макс.размер break; default: // неопознанная опция: // сообщить об ошибке и завершить выполнение } |
Опция -d
задает необходимость отладки. Ее обработка заключается в присваивании переменной с объявлением
bool debug_on = false;
значения true:
case 'd': debug_on = true; |
В нашу программу может входить код следующего вида:
if ( debug_on ) |
Опция -v
выводит номер версии программы и завершает исполнение:
case 'v': cout << program_name << "::" << program_version << endl; |
Опция -h
запрашивает информацию о синтаксисе запуска и завершает исполнение. Вывод сообщения и выход из программы выполняется функцией usage():
case 'h': // break не нужен: usage() вызывает exit() |
Опция -o
сигнализирует о том, что следующая строка содержит имя выходного файла. Аналогично опция -l
говорит, что за ней указан максимальный размер. Как нам обработать эти ситуации?
Если в строке параметра нет дефиса, возможны три варианта: параметр содержит имя выходного файла, максимальный размер или имя входного файла. Чтобы различать эти случаи, присвоим true
переменным, отражающим внутреннее состояние:
// если ofi1e_on==true, // следующий параметр - имя выходного файла bool ofi1e_on = false; // если ofi1e_on==true, // следующий параметр - максимальный размер |
Вот обработка опций -l и -o в нашей инструкции switch:
case 'l': limit_on = true; break; case 'o': ofile_on = true; |
Встретив строку, не начинающуюся с дефиса, мы с помощью переменных состояния можем узнать ее содержание:
// обработаем максимальный размер для опции -1 // имя выходного файла для -o // имена входных файлов ... default: { // ofile_on включена, если -o встречалась if ( ofile_on ) { // обработаем имя выходного файла // выключим ofile_on } else if ( limit_on ) { // если -l встречалась // обработаем максимальный размер // выключим limit_on } else { // обработаем имя входного файла } |
Если аргумент является именем выходного файла, сохраним это имя и выключим ofile_on:
if ( ofile_on ) { ofile_on = false; ofile = pchar; |
Если аргумент задает максимальный размер, мы должны преобразовать строку встроенного типа в представляемое ею число. Сделаем это с помощью стандартной функции atoi(), которая принимает строку в качестве аргумента и возвращает int
(также существует функция atof(), возвращающая double). Для использования atoi()
включим заголовочный файл ctype.h. Нужно проверить, что значение максимального размера неотрицательно и выключить limit_on:
// int limit; else if ( limit_on ) { limit_on = false; limit = atoi( pchar ); if ( limit < 0 ) { cerr << program_name << "::" << program_version << " : error: " << "negative value for limit.\n\n"; usage( -2 ); } |
Если обе переменных состояния равны false, у нас есть имя входного файла. Сохраним его в векторе строк:
else |
file_names.push_back( string( pchar ));
При обработке параметров командной строки важен способ реакции на неверные опции. Мы решили, что задание отрицательной величины в качестве максимального размера будет фатальной ошибкой. Это приемлемо или нет в зависимости от ситуации. Также можно распознать эту ситуацию как ошибочную, выдать предупреждение и использовать ноль или какое-либо другое значение по умолчанию.
Слабость нашей реализации становится понятной, если пользователь небрежно относится к пробелам, разделяющим параметры. Скажем, ни одна из следующих двух строк не будет обработана:
prog - d dataOl |
(Оба случая мы оставим для упражнений в конце раздела.)
Вот полный текст нашей программы. (Мы добавили инструкции печати для трассировки выполнения.)
#include <iostream> #include <string> #include <vector> #include <ctype.h> const char *const program_name = "comline"; const char *const program_version = "version 0.01 (08/07/97)"; inline void usage( int exit_value = 0 ) { // печатает отформатированное сообщение о порядке вызова // и завершает программу с кодом exit_value ... cerr << "порядок вызова:\n" << program_name << " " << "[-d] [-h] [-v] \n\t" << "[-o output_file] [-l limit] \n\t" << "file_name\n\t[file_name [file_name [ ... ]]]\n\n" << "где [] указывает на необязательность опции:\n\n\t" << "-h: справка.\n\t\t" << "печать этого сообщения и выход\n\n\t" << "-v: версия.\n\t\t" << "печать информации о версии программы и выход\n\n\t" << "-d: отладка.\n\t\t включает отладочную печать\n\n\t" << "-l limit\n\t\t" << "limit должен быть неотрицательным целым числом\n\n\t" << "-o ofile\n\t\t" << "файл, в который выводится результат\n\t\t" << "по умолчанию результат записывается на стандартный вывод\n\n" << "file_name\n\t\t" << "имя подлежащего обработке файла\n\t\t" << "должно быть задано хотя бы одно имя --\n\t\t" << "но максимальное число не ограничено\n\n" << "примеры:\n\t\t" << "$command chapter7.doc\n\t\t" << "$command -d -l 1024 -o test_7_8 " << "chapter7.doc chapter8.doc\n\n"; exit( exit_value ); } int main( int argc, char* argv[] ) { bool debug_on = false; bool ofile_on = false; bool limit_on = false; int limit = -1; string ofile; vector<string> file_names; cout << "демонстрация обработки параметров в командной строке:\n" << "argc: " << argc << endl; for ( int ix = 1; ix < argc; ++ix ) { cout << "argv[ " << ix << " ]: " << argv[ ix ] << endl; char *pchar = argv[ ix ]; switch ( pchar[ 0 ] ) { case '-': { cout << "встретился \'-\'\n"; switch( pchar[ 1 ] ) { case 'd': cout << "встретилась -d: " << "отладочная печать включена\n"; debug_on = true; break; case 'v': cout << "встретилась -v: " << "выводится информация о версии\n"; cout << program_name << " :: " << program_version << endl; return 0; case 'h': cout << "встретилась -h: " << "справка\n"; // break не нужен: usage() завершает программу usage(); case 'o': cout << "встретилась -o: выходной файл\n"; ofile_on = true; break; case 'l': cout << "встретилась -l: " << "ограничение ресурса\n"; limit_on = true; break; default: cerr << program_name << " : ошибка : " << "неопознанная опция: - " << pchar << "\n\n"; // break не нужен: usage() завершает программу usage( -1 ); } break; } default: // либо имя файла cout << "default: параметр без дефиса: " << pchar << endl; if ( ofile_on ) { ofile_on = false; ofile = pchar; } else if ( limit_on ) { limit_on = false; limit = atoi( pchar ); if ( limit < 0 ) { cerr << program_name << " : ошибка : " << "отрицательное значение limit.\n\n"; usage( -2 ); } } else file_names.push_back( string( pchar )); break; } } if ( file_names.empty() ) { cerr << program_name << " : ошибка : " << "не задан ни один входной файл.\n\n"; usage( -3 ); } if ( limit != -1 ) cout << "Заданное пользователем значение limit: " << limit << endl; if ( ! ofile.empty() ) cout << "Заданный пользователем выходной файл: " << ofile << endl; cout << (file_names.size() == 1 ? "Файл, " : "Файлы, ") << "подлежащий(е) обработке:\n"; for ( int inx = 0; inx < file_names.size(); ++inx ) cout << "\t" << file_names[ inx ] << endl; |
a.out -d -l 1024 -o test_7_8 chapter7.doc chapters.doc
Вот трассировка обработки параметров командной строки:
демонстрация обработки параметров в командной строке:
argc: 8
argv[ 1 ]: -d
встретился '-'
встретилась -d: отладочная печать включена
argv[ 2 ]: -l
встретился '-'
встретилась -l: ограничение ресурса
argv[ 3 ]: 1024
default: параметр без дефиса: 1024
argv[ 4 ]: -o
встретился '-'
встретилась -o: выходной файл
argv[ 5 ]: test_7_8
default: параметр без дефиса: test_7_8
argv[ 6 ]: chapter7.doc
default: параметр без дефиса: chapter7.doc
argv[ 7 ]: chapter8.doc
default: параметр без дефиса: chapter8.doc
Заданное пользователем значение limit: 1024
Заданный пользователем выходной файл: test_7_8
Файлы, подлежащий(е) обработке:
chapter7.doc
chapter8.doc
Глобальные объекты и функции
Объявление функции в глобальной области видимости вводит глобальную функцию, а объявление переменной – глобальный объект. Глобальный объект существует на протяжении всего времени выполнения программы. Время жизни глобального объекта начинается с момента запуска программы и заканчивается с ее завершением.
Для того чтобы глобальную функцию можно было вызвать или взять ее адрес, она должна иметь определение. Любой глобальный объект, используемый в программе, должен быть определен, причем только один раз. Встроенные функции могут определяться несколько раз, если только все определения совпадают. Такое требование единственности или точного совпадения получило название правила одного определения (ПОО). В этом разделе мы покажем, как следует вводить глобальные объекты и функции в программе, чтобы ПОО соблюдалось.
Готовим сцену
Прежде чем детально описывать множественное и виртуальное наследование, покажем, зачем оно нужно. Наш первый пример взят из области трехмерной компьютерной графики. Но сначала познакомимся с предметной областью.
В компьютере сцена представляется графом сцены, который содержит информацию о геометрии (трехмерные модели), один или более источников освещения (иначе сцена будет погружена во тьму), камеру (без нее мы не можем смотреть на сцену) и несколько трансформационных узлов, с помощью которых позиционируются элементы.
Процесс применения источников освещения и камеры к геометрической модели для получения двумерного изображения, отображаемого на дисплее, называется рендерингом. В алгоритме рендеринга учитываются два основных аспекта: природа источника освещения сцены и свойства материалов поверхностей объектов, такие, как цвет, шероховатость и прозрачность. Ясно, что перышки на белоснежных крыльях феи выглядят совершенно не так, как капающие из ее глаз слезы, хотя те и другие освещены одним и тем же серебристым светом.
Добавление объектов к сцене, их перемещение, игра с источниками освещения и геометрией– работа компьютерного художника. Наша задача – предоставить интерактивную поддержку для манипуляций с графом сцены на экране. Предположим, что в текущей версии своего инструмента мы решили воспользоваться каркасом приложений Open Inventor для C++ (см. [WERNECKE94]), но с помощью подтипизации расширили его, создав собственные абстракции нужных нам классов. Например, Open Inventor располагает тремя встроенными источниками освещения, производными от абстрактного базового класса SoLight:
class SoSpotLight : public SoLight { ... } class SoPointLight : public SoLight { ... } |
class SoDirectionalLight : public SoLight { ... }
Префикс So
служит для того, чтобы дать уникальные имена сущностям, которые в области компьютерной графики весьма распространены (данный каркас приложений проектировался еще до появления пространств имен). Точечный источник (point light) – это источник света, излучающий, как солнце, во всех направлениях. Направленный источник (directional light) – источник света, излучающий в одном направлении. Прожектор
(spotlight) – источник, испускающий узконаправленный конический пучок, как обычный театральный прожектор.
По умолчанию Open Inventor осуществляет рендеринг графа сцены на экране с помощью библиотеки OpenGL (см. [NEIDER93]). Для интерактивного отображения этого достаточно, но почти все изображения, сгенерированные для киноиндустрии, сделаны с помощью средства RenderMan (см. [UPSTILL90]). Чтобы добавить поддержку такого алгоритма рендеринга мы, в частности, должны реализовать собственные специальные подтипы источников освещения:
class RiSpotLight : public SoSpotLight { ... } class RiPointLight : public SoPointLight { ... } |
Новые подтипы содержат дополнительную информацию, необходимую для рендеринга с помощью RenderMan. При этом базовые классы Open Inventor по-прежнему позволяют выполнять рендеринг с помощью OpenGL. Неприятности начинаются, когда возникает необходимость расширить поддержку теней.
В RenderMan направленный источник и прожектор поддерживают отбрасывание тени (поэтому мы называем их источниками освещения, дающими тень, – SCLS), а точечный – нет. Общий алгоритм требует, чтобы мы обошли все источники освещения на сцене и составили карту теней для каждого включенного SCLS. Проблема в том, что источники освещения хранятся в графе сцены как полиморфные объекты класса SoLight. Хотя мы можем инкапсулировать общие данные и необходимые операции в класс SCLS, непонятно, как включить его в существующую иерархию классов Open Inventor.
В поддереве с корнем SoLight в иерархии Open Inventor нет такого класса, из которого можно было бы произвести с помощью одиночного наследования класс SCLS так, чтобы в дальнейшем уже от него произвести SdRiSpotLight и SdRiDirectionalLight. Если не пользоваться множественным наследованием, лучшее, что можно сделать, – это сравнить член класса SCLS с каждым возможным типом SCLS-источника и вызвать соответствующую операцию:
SoLight *plight = next_scene_light(); if ( RiDirectionalLight *pdilite = dynamic_cast<RiDirectionalLight*>( plight )) pdilite->scls.cast_shadow_map(); else if ( RiSpotLight *pslite = dynamic_cast<RiSpotLight*>( plight )) pslite->scls.cast_shadow_map(); |
// и так далее
(Оператор dynamic_cast – это часть механизма идентификации типов во время выполнения (RTTI). Он позволяет опросить тип объекта, адресованного полиморфным указателем или ссылкой. Подробно RTTI будет обсуждаться в главе 19.)
Пользуясь множественным наследованием, мы можем инкапсулировать подтипы SCLS, защитив наш код от изменений при добавлении или удалении источника освещения (см. рис. 18.1).
RPointLight RSpotLight RDirectionalLight
Рис. 18.1. Множественное наследование источников освещения
class RiDirectionalLight : public SoDirectionalLight, public SCLS { ... }; class RiSpotLight : public SoSpotLight, public SCLS { ... }; // ... SoLight *plight = next_scene_light(); if ( SCLS *pscls = dynamic_cast<SCLS*>(plight)) |
Это решение несовершенно. Если бы у нас был доступ к исходным текстам Open Inventor, то можно было бы избежать множественного наследования, добавив к SoLight
член-указатель на SCLS и поддержку операции cast_shadow_map():
class SoLight : public SoNode { public: void cast_shadow_map() { if ( _scls ) _scls->cast_shadow_map(); } // ... protected: SCLS *_scls; }; // ... SdSoLight *plight = next_scene_light(); |
Самое распространенное приложение, где используется множественное (и виртуальное) наследование, – это потоковая библиотека ввода/вывода в стандартном C++. Два основных видимых пользователю класса этой библиотеки – istream
(для ввода) и ostream
(для вывода). В число их общих атрибутов входят:
· информация о форматировании (представляется ли целое число в десятичной, восьмеричной или шестнадцатеричной системе счисления, число с плавающей точкой – в нотации с фиксированной точкой или в научной нотации и т.д.);
· информация о состоянии (находится ли потоковый объект в нормальном или ошибочном состоянии и т.д.);
· информация о параметрах локализации (отображается ли в начале даты день или месяц и т.д.);
· буфер, где хранятся данные, которые нужно прочитать или записать.
Эти общие атрибуты вынесены в абстрактный базовый класс ios, для которого istream и ostream
являются производными.
Класс iostream – наш второй пример множественного наследования. Он предоставляет поддержку для чтения и записи в один и тот же файл; его предками являются классы istream и ostream. К сожалению, по умолчанию он также унаследует два различных экземпляра базового класса ios, а нам это не нужно.
изображена на рис. 18.2.
Рис. 18.2. Иерархия виртуального наследования iostream (упрощенная)
Еще один реальный пример виртуального и множественного наследования дают распределенные объектные вычисления. Подробное рассмотрение этой темы см. в серии статей Дугласа Шмидта (Douglas Schmidt) и Стива Виноски (Steve Vinoski) в [LIPPMAN96b].
В данной главе мы рассмотрим использование и поведение механизмов виртуального и множественного наследования. В другой нашей книге, “Inside the C++ Object Model”, описаны более сложные вопросы производительности и дизайна этого аспекта языка.
Для последующего обсуждения мы выбрали иерархию животных в зоопарке. Наши животные существуют на разных уровнях абстракции. Есть, конечно, особи, имеющие свои имена: Линь-Линь, Маугли или Балу. Каждое животное принадлежит к какому-то виду; скажем, Линь-Линь – это гигантская панда.
Виды в свою очередь входят в семейства. Так, гигантская панда – член семейства медведей, хотя, как мы увидим в разделе 18.5, по этому поводу в зоологии долго велись бурные дискуссии. Каждое семейство – член животного мира, в нашем случае ограниченного территорией зоопарка.
На каждом уровне абстракции имеются данные и операции, необходимые для поддержки все более и более широкого круга пользователей. Например, абстрактный класс ZooAnimal
хранит информацию, общую для всех животных в зоопарке, и предоставляет открытый интерфейс для всех возможных запросов.
Помимо классов, описывающих животных, есть и вспомогательные классы, инкапсулирующие различные абстракции иного рода, например “животные, находящиеся под угрозой вымирания”. Наша реализация класса Panda множественно наследует от Bear
(медведь) и Endangered
(вымирающие).
Идентификация членов иерархии
В разделе 2.4 мы уже упоминали о том, что в объектном проектировании обычно есть один разработчик, который конструирует и реализует класс, и много пользователей, применяющих предоставленный открытый интерфейс. Это разделение ответственности отразилось в концепции открытого и закрытого доступа к членам класса.
Когда используется наследование, у класса оказывается множество разработчиков. Во-первых, тот, кто предоставил реализацию базового класса (и, возможно, некоторых производных от него), а во-вторых, те, кто разрабатывал производные классы на различных уровнях иерархии. Этот род деятельности тоже относится к проектированию. Разработчик подтипа часто (хотя и не всегда) должен иметь доступ к реализации базового класса. Чтобы разрешить такой вид доступа, но все же предотвратить неограниченный доступ к деталям реализации класса, вводится дополнительный уровень доступа– protected (защищенный). Данные и функции-члены, помещенные в секцию protected
некоторого класса, остаются недоступными вызывающей программе, но обращение к ним из производных классов разрешено. (Все находящееся в секции private
базового класса доступно только ему, но не производным.)
Критерии помещения того или иного члена в секцию public одинаковы как для объектного, так и для объектно-ориентированного проектирования. Меняется только точка зрения на то, следует ли объявлять член закрытым или защищенным. Член базового класса объявляется закрытым, если мы не хотим, чтобы производные классы имели к нему прямой доступ; и защищенным, если его семантика такова, что для эффективной реализации производного класса может потребоваться прямой доступ к нему. При проектировании класса, который предполагается использовать в качестве базового, надо также принимать во внимание особенности функций, зависящих от типа, – виртуальных функций в иерархии классов.
На следующем шаге проектирования иерархии классов Query следует ответить на такие вопросы:
(a) Какие операции следует предоставить в открытом интерфейсе иерархии классов Query?
(b) Какие из них следует объявить виртуальными?
(c) Какие дополнительные операции могут потребоваться производным классам?
(d) Какие данные-члены следует объявить в нашем абстрактном базовом классе Query?
(e) Какие данные-члены могут потребоваться производным классам?
К сожалению, однозначно ответить на эти вопросы невозможно. Как мы увидим, процесс объектно-ориентированного проектирования по своей природе итеративен, эволюционирующая иерархия классов требует и добавлений, и модификаций. В оставшейся части этого раздела мы будем постепенно уточнять иерархию классов Query.
Идентификация типов во время выполнения
RTTI позволяет программам, которые манипулируют объектами через указатели или ссылки на базовые классы, получить истинный производный тип адресуемого объекта. Для поддержки RTTI в языке C++ есть два оператора:
· оператор dynamic_cast
поддерживает преобразования типов во время выполнения, обеспечивая безопасную навигацию по иерархии классов. Он позволяет трансформировать указатель на базовый класс в указатель на производный от него, а также преобразовать l-значение, ссылающееся на базовый класс, в ссылку на производный, но только в том случае, если это завершится успешно;
· оператор typeid
позволяет получить фактический производный тип объекта, адресованного указателем или ссылкой.
Однако для получения информации о типе производного класса операнд любого из операторов dynamic_cast или typeid
должен иметь тип класса, в котором есть хотя бы одна виртуальная функция. Таким образом, операторы RTTI – это события времени выполнения для классов с виртуальными функциями и события времени компиляции для всех остальных типов. В данном разделе мы более подробно познакомимся с их возможностями.
Использование RTTI оказывается необходимым при реализации таких приложений, как отладчики или объектные базы данных, когда тип объектов, которыми манипулирует программа, становится известен только во время выполнения путем исследования RTTI-информации, хранящейся вместе с типами объектов. Однако лучше пользоваться статической системой типов C++, поскольку она безопаснее и эффективнее.
Иерархия классов исключений в стандартной библиотеке C++
В начале этого раздела мы определили иерархию классов исключений, с помощью которой наша программа сообщает об аномальных ситуациях. В стандартной библиотеке C++ есть аналогичная иерархия, предназначенная для извещения о проблемах при выполнении функций из самой стандартной библиотеки. Эти классы исключений вы можете использовать в своих программах непосредственно или создать производные от них классы для описания собственных специфических исключений.
Корневой класс исключения в стандартной иерархии называется exception. Он определен в стандартном заголовочном файле <exception> и является базовым для всех исключений, возбуждаемых функциями из стандартной библиотеки. Класс exception
имеет следующий интерфейс:
namespace std { class exception public: exception() throw(); exception( const exception & ) throw(); exception& operator=( const exception & ) throw(); virtual ~exception() throw(); virtual const char* what() const throw(); }; |
}
Как и всякий другой класс из стандартной библиотеки C++, exception помещен в пространство имен std, чтобы не засорять глобальное пространство имен программы.
Первые четыре функции-члена в определении класса – это конструктор по умолчанию, копирующий конструктор, копирующий оператор присваивания и деструктор. Поскольку все они открыты, любая программа может свободно создавать и копировать объекты-исключения, а также присваивать им значения. Деструктор объявлен виртуальным, чтобы сделать возможным дальнейшее наследование классу exception.
Самой интересной в этом списке является виртуальная функция what(), которая возвращает C-строку с текстовым описанием возбужденного исключения. Классы, производные от exception, могут заместить what()
собственной версией, которая лучше характеризует объект-исключение.
Отметим, что все функции в определении класса exception имеют пустую спецификацию throw(), т.е. не возбуждают никаких исключений. Программа может манипулировать объектами-исключениями (к примеру, внутри catch-обработчиков типа exception), не опасаясь, что функции создания, копирования и уничтожения этих объектов возбудят исключения.
Помимо корневого exception, в стандартной библиотеке есть и другие классы, которые допустимо использовать в программе для извещения об ошибках, обычно подразделяемых на две больших категории: логические ошибки и ошибки времени выполнения.
Логические ошибки обусловлены нарушением внутренней логики программы, например логических предусловий или инвариантов класса. Предполагается, что их можно найти и предотвратить еще до начала выполнения программы. В стандартной библиотеке определены следующие такие ошибки:
namespace std { class logic_error : public exception { // логическая ошибка public: explicit logic_error( const string &what_arg ); }; class invalid_argument : public logic_error { // неверный аргумент public: explicit invalid_argument( const string &what_arg ); }; class out_of_range : public logic_error { // вне диапазона public: explicit out_of_range( const string &what_arg ); }; class length_error : public logic_error { // неверная длина public: explicit length_error( const string &what_arg ); }; class domain_error : public logic_error { // вне допустимой области public: explicit domain_error( const string &what_arg ); }; |
Функция может возбудить исключение invalid_argument, если получит аргумент с некорректным значением; в конкретной ситуации, когда значение аргумента выходит за пределы допустимого диапазона, разрешается возбудить исключение out_of_range, а length_error
используется для оповещения о попытке создать объект, длина которого превышает максимально возможную.
Ошибки времени выполнения, напротив, вызваны событием, с самой программой не связанным. Предполагается, что их нельзя обнаружить, пока программа не начала работать. В стандартной библиотеке определены следующие такие ошибки:
namespace std { class runtime_error : public exception { // ошибка времени выполнения public: explicit runtime_error( const string &what_arg ); }; class range_error : public runtime_error { // ошибка диапазона public: explicit range_error( const string &what_arg ); }; class overflow_error : public runtime_error { // переполнение public: explicit overflow_error( const string &what_arg ); }; class underflow_error : public runtime_error { // потеря значимости public: explicit underflow_error( const string &what_arg ); }; |
Упражнение 19.5
Какие исключения могут возбуждать следующие функции:
#include <stdexcept> (a) void operate() throw( logic_error ); (b) int mathErr( int ) throw( underflow_error, overflow_error ); |
Упражнение 19.6
Объясните, как механизм обработки исключений в C++ поддерживает технику программирования “захват ресурса – это инициализация; освобождение ресурса – это уничтожение”.
Упражнение 19.7
Исправьте ошибку в списке catch-обработчиков для данного try-блока:
#include <stdexcept> int main() { try { // использование функций из стандартной библиотеки } catch( exception ) { } catch( runtime_error &re ) { } catch( overflow_error eobj ) { } |
Упражнение 19.8
Дана программа на C++:
int main() { // использование стандартной библиотеки |
Модифицируйте main()
так, чтобы она перехватывала все исключения, возбуждаемые функциями стандартной библиотеки. Обработчики должны печатать сообщение об ошибке, ассоциированное с исключением, а затем вызывать функцию abort() (она определена в заголовочном файле <cstdlib>) для завершения main().
}
Функция может возбудить исключение range_error, чтобы сообщить об ошибке во внутренних вычислениях. Исключение overflow_error
говорит об ошибке арифметического переполнения, а underflow_error – о потере значимости.
Класс exception
является базовым и для класса исключения bad_alloc, которое возбуждает оператор new(), когда ему не удается выделить запрошенный объем памяти (см. раздел 8.4), и для класса исключения bad_cast, возбуждаемого в ситуации, когда ссылочный вариант оператора dynamic_cast не может быть выполнен (см. раздел 19.1).
Переопределим оператор operator[] в шаблоне Array из раздела 16.12 так, чтобы он возбуждал исключение типа range_error, если индекс массива Array
выходит за границы:
#include <stdexcept> #include <string> template <class elemType> class Array { public: // ... elemType& operator[]( int ix ) const { if ( ix < 0 || ix >= _size ) { string eObj = "ошибка: вне диапазона в Array<elemType>::operator[]()"; throw out_of_range( eObj ); } return _ia[ix]; } // ... private: int _size; elemType *_ia; |
Для использования предопределенных классов исключений в программу необходимо включить заголовочный файл <stdexcept>. Описание возбужденного исключения содержится в объекте eObj
типа string. Эту информацию можно извлечь в обработчике с помощью функции-члена what():
int main() { try { // функция main() такая же, как в разделе 16.2 } catch ( const out_of_range &excep ) { // печатается: // ошибка: вне диапазона в Array<elemType>::operator[]() cerr << excep.what() << "\n"; return -1; } |
В данной реализации выход индекса за пределы массива в функции try_array() приводит к тому, что оператор взятия индекса operator[]()
класса Array
возбуждает исключение типа out_of_range, которое перехватывается в main().
Имена перегруженных операторов
Перегружать можно только предопределенные операторы языка C++ (см. табл. 15.1).
Таблица 15.1. Перегружаемые операторы
+ | - | * | / | % | ^ | & | | | ~ | |||||||||
! | , | = | < | > | <= | >= | ++ | -- | |||||||||
<< | >> | == | != | && | || | += | -= | /= | |||||||||
%= | ^= | &= | |= | *= | <<= | >>= | [] | () | |||||||||
-> | ->* | new | new[] | delete | delete[] |
Проектировщик класса не вправе объявить перегруженным оператор с другим именем. Так, при попытке объявить оператор ** для возведения в степень компилятор выдаст сообщение об ошибке.
Следующие четыре оператора языка C++ не могут быть перегружены:
// неперегружаемые операторы |
:: .* . ?:
Предопределенное назначение оператора нельзя изменить для встроенных типов. Например, не разрешается переопределить встроенный оператор сложения целых чисел так, чтобы он проверял результат на переполнение.
// ошибка: нельзя переопределить встроенный оператор сложения int |
int operator+( int, int );
Нельзя также определять дополнительные операторы для встроенных типов данных, например добавить к множеству встроенных операций operator+ для сложения двух массивов.
Перегруженный оператор определяется исключительно для операндов типа класса или перечисления и может быть объявлен только как член класса или пространства имен, принимая хотя бы один параметр типа класса или перечисления (переданный по значению или по ссылке).
Предопределенные приоритеты операторов (см. раздел 4.13) изменить нельзя. Независимо от типа класса и реализации оператора в инструкции
x == y + z;
всегда сначала выполняется operator+, а затем operator==; однако помощью скобок порядок можно изменить.
Предопределенная арность операторов также должна быть сохранена. К примеру, унарный логический оператор НЕ нельзя определить как бинарный оператор для двух объектов класса String. Следующая реализация некорректна и приведет к ошибке компиляции:
// некорректно: ! - это унарный оператор
bool operator!( const String &s1, const String &s2 ) { return ( strcmp( s1.c_str(), s2.c_str() ) != 0 ); |
}
Для встроенных типов четыре предопределенных оператора ("+", "-", "*" и "&") используются либо как унарные, либо как бинарные. В любом из этих качеств они могут быть перегружены.
Для всех перегруженных операторов, за исключением operator(), недопустимы аргументы по умолчанию.
Имя переменной
Имя переменной, или идентификатор, может состоять из латинских букв, цифр и символа подчеркивания. Прописные и строчные буквы в именах различаются. Язык С++ не ограничивает длину идентификатора, однако пользоваться слишком длинными именами типа gosh_this_is_an_impossibly_name_to_type
неудобно.
Некоторые слова являются ключевыми в С++ и не могут быть использованы в качестве идентификаторов; в таблице 3.1 приведен их полный список.
Таблица 3.1. Ключевые слова C++
asm | auto | bool | break | case | |||||
catch | char | class | const | const_cast | |||||
continue | default | delete | do | double | |||||
dynamic_cast | else | enum | explicit | export | |||||
extern | false | float | for | friend | |||||
goto | if | inline | int | long | |||||
mutable | namespace | new | operator | private | |||||
protected | public | register | reinterpret_cast | return | |||||
short | signed | sizeof | static | static_cast | |||||
struct | switch | template | this | throw | |||||
true | try | typedef | typeid | typename | |||||
union | unsigned | using | virtual | void | |||||
volatile | wchar_t | while |
Чтобы текст вашей программы был более понятным, мы рекомендуем придерживаться общепринятых соглашений об именах объектов:
· имя переменной обычно пишется строчными буквами, например index
(для сравнения: Index – это имя типа, а INDEX – константа, определенная с помощью директивы препроцессора #define);
· идентификатор должен нести какой-либо смысл, поясняя назначение объекта в программе, например: birth_date или salary;
если такое имя состоит из нескольких слов, как, например, birth_date, то принято либо разделять слова символом подчеркивания (birth_date), либо писать каждое следующее слово с большой буквы (birthDate). Замечено, что программисты, привыкшие к ОбъектноОриентированномуПодходу предпочитают выделять слова заглавными буквами, в то время как те_кто_много_писал_на_С используют символ подчеркивания. Какой из двух способов лучше – вопрос вкуса.
Инициализация члена, являющегося объектом класса
Что произойдет, если в объявлении _name
заменить C-строку на тип класса string? Как это повлияет на почленную инициализацию по умолчанию? Как надо будет изменить явный копирующий конструктор? Мы ответим на эти вопросы в данном подразделе.
При почленной инициализации по умолчанию исследуется каждый член. Если он принадлежит к встроенному или составному типу, то такая инициализация применяется непосредственно. Например, в первоначальном определении класса Account
член _name
инициализируется непосредственно, так как это указатель:
newAcct._name = oldAcct._name;
Члены, являющиеся объектами классов, обрабатываются по-другому. В инструкции
Account newAcct( oldAcct );
оба объекта распознаются как экземпляры Account. Если у этого класса есть явный копирующий конструктор, то он и применяется для задания начального значения, в противном случае выполняется почленная инициализация по умолчанию.
Таким образом, если обнаруживается член-объект класса, то описанный выше процесс применяется рекурсивно. У класса есть явный копирующий конструктор? Если да, вызвать его для задания начального значения члена-объекта класса. Иначе применить к этому члену почленную инициализацию по умолчанию. Если все члены этого класса принадлежат к встроенным или составным типам, то каждый инициализируется непосредственно и процесс на этом завершается. Если же некоторые члены сами являются объектами классов, то алгоритм применяется к ним рекурсивно, пока не останется ничего, кроме встроенных и составных типов.
В нашем примере у класса string
есть явный копирующий конструктор, поэтому _name инициализируется с помощью его вызова. Копирующий конструктор по умолчанию для класса Account
выглядит следующим образом (хотя явно он не определен):
inline Account:: Account( const Account &rhs ) { _acct_nmbr = rhs._acct_nmbr; _balance = rhs._balance; // Псевдокод на C++ // иллюстрирует вызов копирующего конструктора // для члена, являющегося объектом класса _name.string::string( rhs._name ); |
}
Теперь почленная инициализация по умолчанию для класса Account корректно обрабатывает выделение и освобождение памяти для _name, но все еще неверно копирует номер счета, поэтому приходится кодировать явный копирующий конструктор. Однако приведенный ниже фрагмент не совсем правилен. Можете ли вы сказать, почему?
// не совсем правильно... inline Account:: Account( const Account &rhs ) { _name = rhs._name; _balance = rhs._balance; _acct_nmbr = get_unique_acct_nmbr(); |
Эта реализация ошибочна, поскольку в ней не различаются инициализация и присваивание. В результате вместо вызова копирующего конструктора string мы вызываем конструктор string по умолчанию на фазе неявной инициализации и копирующий оператор присваивания string – в теле конструктора. Исправить это несложно:
inline Account:: Account( const Account &rhs ) : _name( rhs._name ) { _balance = rhs._balance; _acct_nmbr = get_unique_acct_nmbr(); |
Самое главное – понять, что такое исправление необходимо. (Обе реализации приводят к тому, что в _name
копируется значение из rhs._name, но в первой одна и та же работа выполняется дважды.) Общее эвристическое правило состоит в том, чтобы по возможности инициализировать все члены-объекты классов в списке инициализации членов.
Упражнение 14.13
Для какого определения класса скорее всего понадобится копирующий конструктор?
1. Представление Point3w, содержащее четыре числа с плавающей точкой.
2. Класс matrix, в котором память для хранения матрицы выделяется динамически в конструкторе и освобождается в деструкторе.
3. Класс payroll
(платежная ведомость), где каждому объекту приписывается уникальный идентификатор.
4. Класс word
(слово), содержащий объект класса string и вектор, в котором хранятся пары (номер строки, смещение в строке).
Упражнение 14.14
Реализуйте для каждого из данных классов копирующий конструктор, конструктор по умолчанию и деструктор.
(a) class BinStrTreeNode { public: // ... private: string _value; int _count; BinStrTreeNode *_leftchild; BinStrTreeNode *_rightchild; |
(b) class BinStrTree { public: // ... private: BinStrTreeNode *_root; |
(c) class iMatrix { public: // ... private: int _rows; int _cols; int *_matrix; |
(d) class theBigMix { public: // ... private: BinStrTree _bst; iMatrix _im; string _name; vectorMfloat> *_pvec; |
Упражнение 14.15
Нужен ли копирующий конструктор для того класса, который вы выбрали в упражнении 14.3 из раздела 14.2? Если нет, объясните почему. Если да, реализуйте его.
Упражнение 14.16
Идентифицируйте в следующем фрагменте программы все места, где происходит почленная инициализация:
Point global; Point foo_bar( Point arg ) { Point local = arg; Point *heap = new Point( global ); *heap = local; Point pa[ 4 ] = { local, *heap }; return *heap; |
Инициализация и присваивание
Вспомним, что имя массива без указания индекса элемента интерпретируется как адрес первого элемента. Аналогично имя функции без следующих за ним скобок интерпретируется как указатель на функцию. Например, при вычислении выражения
lexicoCompare;
получается указатель типа
int (*)( const string &, const string & );
Применение оператора взятия адреса к имени функции также дает указатель того же типа, например lexicoCompare и &lexicoCompare. Указатель на функцию инициализируется следующим образом:
int (*pfi)( const string &, const string & ) = lexicoCompare; |
int (*pfi2)( const string &, const string & ) = &lexicoCompare;
Ему можно присвоить значение:
pfi = lexicoCompare; |
pfi2 = pfi;
Инициализация и присваивание корректны только тогда, когда список параметров и тип значения, которое возвращает функция, адресованная указателем в левой части операции присваивания, в точности соответствуют списку параметров и типу значения, возвращаемого функцией или указателем в правой части. В противном случае выдается сообщение об ошибке компиляции. Никаких неявных преобразований типов для указателей на функции не производится. Например:
int calc( int, int ); int (*pfi2s)( const string &, const string & ) = 0; int (*pfi2i)( int, int ) = 0; int main() { pfi2i = calc; // правильно pri2s = calc; // ошибка: несовпадение типов pfi2s = pfi2i; // ошибка: несовпадение типов return 0; |
}
Такой указатель можно инициализировать нулем или присвоить ему нулевое значение, в этом случае он не адресует функцию.
Инициализация класса
Рассмотрим следующее определение класса:
class Data { public: int ival; char *ptr; |
};
Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival
содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival – его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival
содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival
инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.
Мнемонические имена класса и обоих его членов сделали бы, конечно, его назначение более понятным для читателя программы, но не дали бы никакой дополнительной информации компилятору. Чтобы компилятор понимал наши намерения, мы должны предоставить одну или несколько перегруженных функций инициализации – конструкторов. Подходящий конструктор выбирается в зависимости от множества начальных значений, указанных при определении объекта. Например, любая из приведенных ниже инструкций представляет корректную инициализацию объекта класса Data:
Data dat01( "Venus and the Graces", 107925 ); Data dat02( "about" ); Data dat03( 107925 ); |
Data dat04;
Бывают ситуации (как в случае с dat04), когда нам нужен объект класса, но его начальные значения мы еще не знаем. Возможно, они станут известны позже. Однако начальное значение задать необходимо, хотя бы такое, которое показывает, что разумное начальное значение еще не присвоено. Другими словами, инициализация объекта иногда сводится к тому, чтобы показать, что он еще не
инициализирован. Большинство классов предоставляют специальный конструктор по умолчанию, для которого не требуется задавать начальных значений. Как правило, он инициализирует объект таким образом, чтобы позже можно было понять, что реальной инициализации еще не проводилось.
Обязан ли наш класс Data
иметь конструктор? Нет, поскольку все его члены открыты. Унаследованный из языка C механизм поддерживает явную инициализацию, аналогичную используемой при инициализации массивов:
int main() { // local1.ival = 0; local1.ptr = 0 Data local1 = { 0, 0 }; // local2.ival = 1024; // local3.ptr = "Anna Livia Plurabelle" Data.local2 - { 1024, "Anna Livia Plurabelle" }; // ... |
Значения присваиваются позиционно, на основе порядка, в котором объявляются данные-члены. Следующий пример приводит к ошибке компиляции, так как ival
объявлен перед ptr:
// ошибка: ival = "Anna Livia Plurabelle"; // ptr = 1024 |
Явная инициализация имеет два основных недостатка. Во-первых, она может быть применена лишь для объектов классов, все члены которых открыты (т.е. эта инициализация не поддерживает инкапсуляции данных и абстрактных типов – их не было в языке C, откуда она заимствована). А во-вторых, такая форма требует вмешательства программиста, что увеличивает вероятность появления ошибок (забыл включить список инициализации или перепутал порядок следования инициализаторов в нем).
Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для некоторых приложений более эффективно использовать список для инициализации больших структур постоянными значениями. К примеру, мы можем таким образом построить палитру цветов или включить в текст программы фиксированные координаты вершин и значения в узлах сложной геометрической модели. В подобных случаях инициализация выполняется во время загрузки, что сокращает затраты времени на запуск конструктора, даже если он определен как встроенный. Это особенно удобно при работе с глобальными объектами1[O.A.4] .
Однако в общем случае предпочтительным методом инициализации является конструктор, который гарантированно будет вызван компилятором для каждого объекта до его первого использования. В следующем разделе мы познакомимся с конструкторами детально.
Инициализация массива, распределенного из хипа A
По умолчанию инициализация массива объектов, распределенного из хипа, проходит в два этапа: выделение памяти для массива, к каждому элементу которого применяется конструктор по умолчанию, если он определен, и последующее присваивание значения каждому элементу.
Чтобы свести инициализацию к одному шагу, программист должен вмешаться и поддержать следующую семантику: задать начальные значения для всех или некоторых элементов массива и гарантировать применение конструктора по умолчанию для тех элементов, начальные значения которых не заданы. Ниже приведено одно из возможных программных решений, где используется оператор размещения new:
#include <utility> #include <vector > #include <new> #include <cstddef> #include "Accounts.h" typedef pair<char*, double> value_pair; /* init_heap_array() * объявлена как статическая функция-член * обеспечивает выделение памяти из хипа и инициализацию * массива объектов * init_values: пары начальных значений элементов массива * elem_count: число элементов в массиве * если 0, то размером массива считается размер вектора * init_values */ Account* Account:: init_heap_array( vector<value_pair> &init_values, vector<value_pair>::size_type elem_count = 0 ) { vector<value_pair>::size_type vec_size = init_value.size(); if ( vec_size == 0 && elem_count == 0 ) return 0; // размер массива равен либо elem_count, // либо, если elem_count == 0, размеру вектора ... size_t elems = elem_count ? elem_count : vec_size(); // получить блок памяти для размещения массива char *p = new char[sizeof(Account)*elems]; // по отдельности инициализировать каждый элемент массива int offset = sizeof( Account ); for ( int ix = 0; ix < elems; ++ix ) { // смещение ix-ого элемента // если пара начальных значений задана, // передать ее конструктору; // в противном случае вызвать конструктор по умолчанию if ( ix < vec_size ) new( p+offset*ix ) Account( init_values[ix].first, init_values[ix].second ); else new( p+offset*ix ) Account; } // отлично: элементы распределены и инициализированы; // вернуть указатель на первый элемент return (Account*)p; |
}
Необходимо заранее выделить блок памяти, достаточный для хранения запрошенного массива, как массив байт, чтобы избежать применения к каждому элементу конструктора по умолчанию. Это делается в такой инструкции:
char *p = new char[sizeof(Account)*elems];
Далее программа в цикле обходит этот блок, присваивая на каждой итерации переменной p
адрес следующего элемента и вызывая либо конструктор с двумя параметрами, если задана пара начальных значений, либо конструктор по умолчанию:
for ( int ix = 0; ix < elems; ++ix ) { if ( ix < vec_size ) new( p+offset*ix ) Account( init_values[ix].first, init_values[ix].second ); else new( p+offset*ix ) Account; |
В разделе 14.3 говорилось, что оператор размещения new
позволяет применить конструктор класса к уже выделенной области памяти. В данном случае мы используем new для поочередного применения конструктора класса Account к каждому из выделенных элементов массива. Поскольку при создании инициализированного массива мы подменили стандартный механизм выделения памяти, то должны сами позаботиться о ее освобождении. Оператор delete
работать не будет:
delete [] ps;
Почему? Потому что ps (мы предполагаем, что эта переменная была инициализирована вызовом init_heap_array()) указывает на блок памяти, полученный не с помощью стандартного оператора new, поэтому число элементов в массиве компилятору неизвестно. Так что всю работу придется сделать самим:
void Account:: dealloc_heap_array( Account *ps, size_t elems ) { for ( int ix = 0; ix < elems; ++ix ) ps[ix].Account::~Account(); delete [] reinterpret_cast<char*>(ps); |
Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам:
new( p+offset*ix ) Account;
то здесь мы обращаемся к ним, задавая индекс в массиве ps:
ps[ix].Account::~Account();
Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p – как указатель на char. Индексирование p
дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с p
ассоциирован не тот тип, что нужно, арифметические операции над указателями приходится программировать самостоятельно.
Мы объявляем обе функции статическими членами класса:
typedef pair<char*, double> value_pair;
class Account { public: // ... static Account* init_heap_array( vector<value_pair> &init_values, vector<value_pair>::size_type elem_count = 0 ); static void dealloc_heap_array( Account*, size_t ); // ... |
Инициализация, присваивание и уничтожение класса
В этой главе мы детально изучим автоматическую инициализацию, присваивание и уничтожение объектов классов в программе. Для поддержки инициализации служит конструктор– определенная проектировщиком функция (возможно, перегруженная), которая автоматически применяется к каждому объекту класса перед его первым использованием. Парная по отношению к конструктору функция, деструктор, автоматически применяется к каждому объекту класса по окончании его использования и предназначена для освобождения ресурсов, захваченных либо в конструкторе класса, либо на протяжении его жизни.
По умолчанию как инициализация, так и присваивание одного объекта класса другому выполняются почленно, т.е. путем последовательного копирования всех членов. Хотя этого обычно достаточно, при некоторых обстоятельствах такая семантика оказывается неадекватной. Тогда проектировщик класса должен предоставить специальный копирующий конструктор и копирующий оператор присваивания. Самое сложное в поддержке этих функций-членов – понять, что они должны быть написаны.
Инструкции
Мельчайшей независимой частью С++ программы является инструкция. Она соответствует предложению естественного языка, но завершается точкой с запятой (;), а не точкой. Выражение С++ (например, ival + 5) становится простой инструкцией, если после него поставить точку с запятой. Составная инструкция– это последовательность простых, заключенная в фигурные скобки. По умолчанию инструкции выполняются в порядке записи. Как правило, последовательного выполнения недостаточно для решения реальных задач. Специальные управляющие конструкции позволяют менять порядок действий в зависимости от некоторых условий и повторять составную инструкцию определенное количество раз. Инструкции if, if-else и switch
обеспечивают условное выполнение. Повторение обеспечивается инструкциями цикла while, do-while и for.
Инструкции объявления
В С++ определение объекта, например
int ival;
рассматривается как инструкция объявления (хотя в данном случае более правильно было бы сказать определения). Ее можно использовать в любом месте программы, где разрешено употреблять инструкции. В следующем примере объявления помечены комментарием //#n, где n– порядковый номер.
#include <fstream> #include <string> #include <vector> int main() { string fileName; // #1 cout << "Введите имя файла: "; cin >> fileName; if ( fileName.empty() ) { // странный случай cerr << "Пустое имя файла. Завершение работы.\n"; return -1; } ifstream inFile( fileName.c_str() ); // #2 if ( ! inFile ) { cerr << "Невозможно открыть файл.\n"; return -2; } string inBuf; // #3 vector< string > text; // #4 while ( inFile >> inBuf ) { for ( int ix = 0; ix < inBuf .size(); ++ix ) // #5 // можно обойтись без ch, // но мы использовали его для иллюстрации if (( char ch = inBuf[ix] )=='.'){ // #6 ch = '_'; inBuf[ix] = ch; } text.push_back( inBuf ); } if ( text.empty() ) return 0; // одна инструкция объявления, // определяющая сразу два объекта vector<string>::iterator iter = text.begin(), // #7 iend = text.end(); while ( iter != -iend ) { cout << *iter << '\n'; ++iter; } return 0; |
}
Программа содержит семь инструкций объявления и восемь определений объектов. Объявления действуют локально; переменная объявляется непосредственно перед первым использованием объекта.
В 70-е годы философия программирования уделяла особое внимание тому, чтобы определения всех объектов находились в начале программы или блока, перед исполняемыми инструкциями. (В С, например, определение переменной не является инструкцией и обязано располагаться в начале блока.) В некотором смысле это была реакция на идиому использования переменных без предварительного объявления, чреватую ошибками. Такую идиому поддерживал, например, FORTRAN.
Поскольку в С++ объявление является обычной инструкцией, ему разрешено появляться в любом месте программы, где допустимо употребление инструкции, что дает возможность использовать локальные объявления.
Необходимо ли это? Для встроенных типов данных применение локальных объявлений является скорее вопросом вкуса. Язык их поощряет , разрешая объявлять переменные внутри условных частей инструкций if, if-else, switch, while, for. Те программисты, которые любят этот стиль, верят, что таким образом делают свои программы более понятными.
Локальные объявления становятся необходимостью, когда мы используем объекты классов, имеющие конструкторы и деструкторы. Если мы помещаем все объявления в начало блока или функции, происходят две неприятные вещи:
· конструкторы всех объектов вызываются перед исполнением первой инструкции блока. Применение локальных объявлений позволяет “размазать” расходы на инициализацию по всему блоку;
· что более важно, блок или функция могут завершиться до того, как будут действительно использованы все объявленные в начале объекты. Скажем, наша программа из предыдущего примера имеет два аварийных выхода: при вводе пользователем пустого имени файла и при
невозможности открыть файл с заданным именем. При этом последующие инструкции функции уже не выполняются. Если бы объекты inBuf и next были объявлены в начале блока, конструкторы и деструкторы этих объектов в случае ненормального завершения функции вызывались бы совершенно напрасно.
Инструкция объявления может состоять из одного или более определений. Например, в нашей программе мы определяем два итератора вектора в одной инструкции:
// одна инструкция объявления, // определяющая сразу два объекта vector<string>::iterator iter = text.begin(), |
Эквивалентная пара, определяющая по одному объекту, выглядит так:
vector<string>::iterator iter = text.begin(); |
Хотя определение одного или нескольких объектов в одном предложении является скорее вопросом вкуса, в некоторых случаях – например, при одновременном определении объектов, указателей и ссылок – это может спровоцировать появление ошибок. Скажем, в следующей инструкции не совсем ясно, действительно ли программист хотел определить указатель и объект или просто забыл поставить звездочку перед вторым идентификатором (используемые имена переменных наводят на второе предположение):
// то ли хотел определить программист? |
Эквивалентная пара инструкций не позволит допустить такую ошибку:
string *ptr1; |
В наших примерах мы обычно группируем определения объектов в инструкции по сходству употребления. Например, в следующей паре
int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0; |
первая инструкция объявляет пять очень похожих по назначению объектов – счетчиков пяти гласных латинского алфавита. Счетчики для подсчета символов и слов определяются во второй инструкции. Хотя такой подход нам кажется естественным и удобным, нет никаких причин считать его хоть чем-то лучше других.
Упражнение 5.1
Представьте себе, что вы являетесь руководителем программного проекта и хотите, чтобы применение инструкций объявления было унифицировано. Сформулируйте правила использования объявлений объектов для вашего проекта.
Упражнение 5.2
Представьте себе, что вы только что присоединились к проекту из предыдущего упражнения. Вы совершенно не согласны не только с конкретными правилами использования инструкций объявления, но и вообще с навязыванием каких-либо правил для этого. Объясните свою позицию.
Инструкция break
Инструкция break
останавливает циклы for, while, do while и блока switch. Выполнение программы продолжается с инструкции, следующей за закрывающей фигурной скобкой цикла или блока. Например, данная функция ищет в массиве целых чисел определенное значение. Если это значение найдено, функция сообщает его индекс, в противном случае она возвращает -1. Вот как выглядит реализация функции:
// возвращается индекс элемента или -1 int search( int *ia, int size, int value ) { // проверка что ia != 0 и size > 0 ... int loc = -1; for ( int ix = 0; ix < size; ++ix ) { if ( value == ia[ ix ] ) { // нашли! // запомним индекс и выйдем из цикла 1oc = ix; break; } } // конец цикла // сюда попадаем по break ... return 1oc; |
}
В этом примере break
прекращает выполнение цикла for и передает управление инструкции, следующей за этим циклом, – в нашем случае return. Заметим, что break
выводит из блока, относящегося к инструкции for, а не if, хотя является частью составной инструкции, соответствующей if. Использование break
внутри блока if, не входящего в цикл или в switch, является синтаксической ошибкой:
// ошибка: неверное использование break if ( ptr ) { if ( *ptr == "quit" ) break; // ... |
}
Если эта инструкция используется внутри вложенных циклов или инструкций switch, она завершает выполнение того внутреннего блока, в котором находится. Цикл или switch, включающий тот цикл или switch, из которого мы вышли с помощью break, продолжает выполняться. Например:
white ( cin >> inBuf ) { switch( inBuf[ 0 ] ) { case '-': for ( int ix = 1; ix < inBuf.size(); ++ix ) { if ( inBuf[ ix ] == ' ' ) break; // #1 // ... // ... } break; // #2 case '+': // ... } |
}
Инструкция break, помеченная // #1, завершает выполнение цикла for внутри ветви case '-'
блока switch, но не сам switch. Аналогично break // #2
завершает выполнение блока switch, но не цикла while, в который тот входит.
Инструкция цикла for
Как мы видели, выполнение программы часто состоит в повторении последовательности инструкций – до тех пор, пока некоторое условие остается истинным. Например, мы читаем и обрабатываем записи файла, пока не дойдем до его конца, перебираем элементы массива, пока индекс не станет равным размерности массива минус 1, и т.д. В С++ предусмотрено три инструкции для организации циклов, в частности for и while, которые начинаются проверкой условия. Такая проверка означает, что цикл может закончиться без выполнения связанной с ним простой или составной инструкции. Третий тип цикла, do while, гарантирует, что тело будет выполнено как минимум один раз: условие цикла проверяется по его завершении. (В этом разделе мы детально рассмотрим цикл for; в разделе 5.6 разберем while, а в разделе 5.7 – do while.)
Цикл for
обычно используется для обработки структур данных, имеющих фиксированную длину, таких, как массив или вектор:
#include <vector> int main() { int ia[ 10 ]; for ( int ix = 0; ix < 10; ++-ix ) ia[ ix ] = ix; vector<int> ivec( ia, ia+10 ); vector<int>::iterator iter = ivec.begin() ; for ( ; iter != ivec.end(); ++iter ) *iter *= 2; return 0; |
}
Синтаксис цикла for
следующий:
for (инструкция-инициализации; условие; выражение ) |
инструкция
инструкция-инициализации
может быть либо выражением, либо инструкцией объявления. Обычно она используется для инициализации переменной значением, которое увеличивается в ходе выполнения цикла. Если такая инициализация не нужна или выполняется где-то в другом месте, эту инструкцию можно заменить пустой (см. второй из приведенных ниже примеров). Вот примеры правильного использования инструкции-инициализации:
// index и iter определены в другом месте for ( index =0; ... for ( ; /* пустая инструкция */ ... for ( iter = ivec.begin(); ... for ( int 1o = 0,hi = max; ... |
for ( char *ptr = getStr(); ...
условие служит для управления циклом. Пока условие при вычислении дает true, инструкция
продолжает выполняться. Выполняемая в цикле инструкция
может быть как простой, так и составной. Если же самое первое вычисление условия дает false, инструкция не выполняется ни разу. Правильные условия
можно записать так:
(... index < arraySize; ... ) (... iter != ivec.end(); ... ) (... *stl++ = *st2++; ... ) |
Выражение вычисляется после выполнения инструкции на каждой итерации цикла. Обычно его используют для модификации переменной, инициализированной в инструкции-инициализации. Если самое первое вычисление условия дает false, выражение не выполняется ни разу. Правильные выражения выглядят таким образом:
( ... ...; ++-index ) ( ... ...; ptr = ptr->next ) ( ... ...; ++i, --j, ++cnt ) |
Для приведенного ниже цикла for
const int sz = 24; int ia[ sz ]; vector<int> ivec( sz ); for ( int ix = 0; ix < sz; ++ix ) { ivec[ ix ] = ix; ia[ ix ]= ix; |
порядок вычислений будет следующим:
1. инструкция-инициализации
выполняется один раз перед началом цикла. В данном примере объявляется переменная ix, которая инициализируется значением 0.
2. Вычисляется условие. Если оно равно true, выполняется составная инструкция тела цикла. В нашем примере, пока ix меньше sz, значение ix
присваивается элементам ivec[ix] и ia[ix]. Когда значением условия
станет false, выполнение цикла прекратится. Если самое первое вычисление условия
даст false, составная инструкция
выполняться не будет.
3. Вычисляется выражение. Как правило, его используют для модификации переменной, фигурирующей в инструкции-инициализации и проверяемой в условии. В нашем примере ix увеличивается на 1.
Эти три шага представляют собой полную итерацию цикла for. Теперь шаги 2 и 3 будут повторяться до тех пор, пока условие не станет равным false, т.е. ix
окажется равным или большим sz.
В инструкции-инициализации можно определить несколько объектов, однако все они должны быть одного типа, так как инструкция объявления допускается только одна:
for ( int ival = 0, *pi = &ia, &ri = val; ival < size; ++iva1, ++pi, ++ri ) |
Объявление объекта в условии гораздо труднее правильно использовать: такое объявление должно хотя бы раз дать значение false, иначе выполнение цикла никогда не прекратится. Вот пример, хотя и несколько надуманный:
#include <iostream> int main() { for ( int ix = 0; bool done = ix == 10; ++ix ) cout << "ix: " << ix << endl; |
Видимость всех объектов, определенных внутри круглых скобок инструкции for, ограничена телом цикла. Например, проверка iter после цикла вызовет ошибку компиляции[8]:[O.A.2]
int main() { string word; vector< string > text; // ... for ( vector< string >::iterator iter = text.begin(), iter_end = text.end(); iter != text.end(); ++iter ) { if ( *iter == word ) break; // ... } // ошибка: iter и iter_end невидимы if ( iter != iter_end ) |
Упражнение 5.8
Допущены ли ошибки в нижеследующих циклах for? Если да, то какие?
(a) for ( int *ptr = &ia, ix = 0; ix < size && ptr != ia+size; ++ix, ++ptr ) // ... |
(b) for ( ; ; ) { if ( some_condition ) break; // ... |
(c) for ( int ix = 0; ix < sz; ++ix ) // ... if ( ix != sz ) // ... (d) int ix; for ( ix < sz; ++ix ) // ... (e) for ( int ix = 0; ix < sz; ++ix, ++ sz ) |
Упражнение 5.9
Представьте, что вам поручено придумать общий стиль использования цикла for в вашем проекте. Объясните и проиллюстрируйте примерами правила использования каждой из трех частей цикла.
Упражнение 5.10
Дано объявление функции:
bool is_equa1( const vector<int> &vl, |
Напишите тело функции, определяющей равенство двух векторов. Для векторов разной длины сравнивайте только то количество элементов, которое соответствует меньшему из двух. Например, векторы (0,1,1,2) и (0,1,1,2,3,5,8) считаются равными. Длину векторов можно узнать с помощью функций v1.size() и v2.size().
Инструкция continue
Инструкция continue
завершает текущую итерацию цикла и передает управление на вычисление условия, после чего цикл может продолжиться. В отличие от инструкции break, завершающей выполнение всего цикла, инструкция continue
завершает выполнение только текущей итерации. Например, следующий фрагмент программы читает из входного потока по одному слову. Если слово начинается с символа подчеркивания, оно обрабатывается, в противном случае программа переходит к новому слову.
while ( cin >> inBuf ) { if ( inBuf[0] '= '_' ) continue; // завершение итерации // обработка слова ... |
}
Инструкция continue
может быть использована только внутри цикла.
Инструкция do while
Представим, что нам надо написать программу, переводящую мили в километры. Структура программы выглядит так:
int val; bool more = true; // фиктивное значение, нужное для // начала цикла while ( more ) { val = getValue(); val = convertValue(val); printValue(val); more = doMore(); |
}
Проблема заключается в том, что условие вычисляется в теле цикла. for и while требуют, чтобы значение условия равнялось true до первого вхождения в цикл, иначе тело не выполнится ни разу. Это означает, что мы должны обеспечить такое условие до начала работы цикла. Альтернативой может служить использование do while, гарантирующего выполнение тела цикла хотя бы один раз. Синтаксис цикла do while
таков:
do инструкция |
while ( условие );
инструкция
выполняется до первой проверки условия. Если вычисление условия
дает false, цикл останавливается. Вот как выглядит предыдущий пример с использованием цикла do while:
do { val = getValue(); val = convertValue(val); printValue(val); |
} while doMore();
В отличие от остальных инструкций циклов, do while не разрешает объявлять объекты в своей части условия. Мы не можем написать:
// ошибка: объявление переменной // в условии не разрешается do { // ... mumble( foo ); |
} while ( int foo = get_foo() ) // ошибка
потому что до условной части инструкции do while мы дойдем только после первого выполнения тела цикла.
Упражнение 5.14
Какие ошибки допущены в следующих циклах do while:
(a)
do string rsp; int vail, va12; cout << "Введите два числа: "; c-in >> vail >> va12; cout << "Сумма " << vail << " и " << va12 << " = " << vail + va12 << "\n\n" << "Продолжить? [да][нет] "; cin >> rsp; while ( rsp[0] != 'n' ); (b) do { // ... } while ( int iva1 = get_response() ); (c) do { int ival = get_response(); if ( iva1 == some_value() ) break; } while ( iva1 ); if ( !iva1 ) |
// ...
Упражнение 5.15
Напишите небольшую программу, которая запрашивает у пользователя две строки и печатает результат лексикографического сравнения этих строк (строка считается меньшей, если идет раньше при сортировке по алфавиту). Пусть она повторяет эти действия, пока пользователь не даст команду закончить. Используйте тип string, сравнение строк и цикл do while.
Инструкция goto
Инструкция goto
обеспечивает безусловный переход к другой инструкции внутри той же функции, поэтому современная практика программирования выступает против ее применения.
Синтаксис goto
следующий:
goto метка;
где метка– определенный пользователем идентификатор. Метка ставится перед инструкцией, на которую можно перейти с помощью goto, и должна заканчиваться двоеточием. Нельзя ставить метку непосредственно перед закрывающей фигурной скобкой. Если же это необходимо, их следует разделить пустой инструкцией:
end: ; // пустая инструкция |
}
Переход через инструкцию объявления в том же блоке с помощью goto
невозможен. Например, данная функция вызывает ошибку компиляции:
int oops_in_error() { // mumble ... goto end; // ошибка: переход через объявление int ix = 10; // ... код, использующий ix end: ; |
}
Правильная реализация функции помещает объявление ix и использующие его инструкции во вложенный блок:
int oops_in_error() { // mumble ... goto end; { // правильно: объявление во вложенном блоке int ix = 10; // ... код, использующий ix } end: ; |
}
Причина такого ограничения та же, что и для объявлений внутри блока switch: компилятор должен гарантировать, что для объявленного объекта конструктор и деструктор либо выполняются вместе, либо ни один из них не выполняется. Это и достигается заключением объявления во вложенный блок.
Переход назад через объявление, однако, не считается ошибкой. Почему? Перепрыгнуть через инициализацию объекта нельзя, но проинициализировать один и тот же объект несколько раз вполне допустимо, хотя это может привести к снижению эффективности. Например:
// переход назад через объявление не считается ошибкой. void mumble ( int max_size ) { begin: int sz = get_size(); if ( sz <= 0 ) { // выдать предупреждение ... goto end; } else if ( sz > max_size ) // получить новое значение sz goto begin; { // правильно: переход через целый блок int ia = new int[ sz ]; doit( ia, sz ) ; delete [] ia; } end: ; |
}
Использование инструкции goto
резко критикуется во всех современных языках программирования. Ее применение приводит к тому, что ход выполнения программы становится трудно понять и, следовательно, такую программу трудно модифицировать. В большинстве случаев goto
можно заменить на инструкции if или циклы. Если вы все-таки решили использовать goto, не перескакивайте через большой фрагмент кода, чтобы можно было легко найти начало и конец вашего перехода.
Инструкция if
Инструкция if
обеспечивает выполнение или пропуск инструкции или блока в зависимости от условия. Ее синтаксис таков:
if ( условие ) |
инструкция
условие
заключается в круглые скобки. Оно может быть выражением, как в этом примере:
if(a+b>c) { ... }
или инструкцией объявления с инициализацией:
if ( int ival = compute_value() ){...}
Область видимости объекта, объявленного в условной части, ограничивается ассоциированной с if
инструкцией или блоком. Например, такой код вызывает ошибку компиляции:
if ( int ival = compute_value() ) { // область видимости ival // ограничена этим блоком } // ошибка: ival невидим |
if ( ! ival ) ...
Попробуем для иллюстрации применения инструкции if реализовать функцию min(), возвращающую наименьший элемент вектора. Заодно наша функция будет подсчитывать число элементов, равных минимуму. Для каждого элемента вектора мы должны проделать следующее:
1. Сравнить элемент с текущим значением минимума.
2. Если элемент меньше, присвоить текущему минимуму значение элемента и сбросить счетчик в 1.
3. Если элемент равен текущему минимуму, увеличить счетчик на 1.
4. В противном случае ничего не делать.
5. После проверки последнего элемента вернуть значение минимума и счетчика.
Необходимо использовать две инструкции if:
if ( minVal > ivec[ i ] )...// новое значение minVal |
if ( minVal == ivec[ i ] )...// одинаковые значения
Довольно часто программист забывает использовать фигурные скобки, если нужно выполнить несколько инструкций в зависимости от условия:
if ( minVal > ivec[ i ] ) minVal = ivec[ i ]; |
occurs = 1; // не относится к if!
Такую ошибку трудно увидеть, поскольку отступы в записи подразумевают, что и minVal=ivec[i], и occurs=1
входят в одну инструкцию if. На самом же деле инструкция
occurs = 1;
не является частью if и выполняется безусловно, всегда сбрасывая occurs в 1. Вот как должна быть составлена правильная if-инструкция (точное положение открывающей фигурной скобки является поводом для бесконечных споров):
if ( minVal > ivec[ i ] ) { minVal = ivec[ i ]; occurs = 1; |
Вторая инструкция if
выглядит так:
if ( minVal == ivec [ i ] ) |
Заметим, что порядок следования инструкций в этом примере крайне важен. Если мы будем сравнивать minVal
именно в такой последовательности, наша функция всегда будет ошибаться на 1:
if ( minVal > ivec[ i ] ) { minVal = ivec[ i ]; occurs = 1; } // если minVal только что получила новое значение, // то occurs будет на единицу больше, чем нужно if ( minVal == ivec[ i ] ) |
Выполнение второго сравнения не обязательно: один и тот же элемент не может одновременно быть и меньше и равен minVal. Поэтому появляется необходимость выбора одного из двух блоков в зависимости от условия, что реализуется инструкцией if-else, второй формой if-инструкции. Ее синтаксис выглядит таким образом:
if ( условие ) инструкция1 else |
инструкция1
выполняется, если условие истинно, иначе переходим к инструкция2. Например:
if ( minVal == ivec[ i ] ) ++occurs; else if ( minVal > ivec[ i ] ) { minVal = ivec[ i ]; occurs = 1; |
Здесь инструкция2
сама является if-инструкцией. Если minVal
меньше ivec[i], никаких действий не производится.
В следующем примере выполняется одна из трех инструкций:
if ( minVal < ivec[ i ] ) {} // пустая инструкция else if ( minVal > ivec[ i ] ) { minVal = ivec[ i ]; occurs = 1; } else // minVal == ivec[ i ] |
Составные инструкции if-else
могут служить источником неоднозначного толкования, если частей else
больше, чем частей if. К какому из if
отнести данную часть else? (Эту проблему иногда называют проблемой висячего else). Например:
if ( minVal <= ivec[ i ] ) if ( minVal == ivec[ i ] ) ++occurs; else { minVal = ivec[ i ]; occurs = 1; |
Судя по отступам, программист предполагает, что else относится к самому первому, внешнему if. Однако в С++ неоднозначность висячих else разрешается соотнесением их с последним встретившимся if. Таким образом, в действительности предыдущий фрагмент означает следующее:
if ( minVal <= ivec[ i ] ) { if ( minVal == ivec[ i ] ) ++occurs; else { minVal = ivec[ i ]; occurs = 1; } |
Одним из способов разрешения данной проблемы является заключение внутреннего if в фигурные скобки:
if ( minVal <= ivec[ i ] ) { if ( minVal == ivec[ i ] ) ++occurs; } else { minVal = ivec[ i ]; occurs = 1; |
В некоторых стилях программирования рекомендуется всегда употреблять фигурные скобки при использовании инструкций if-else, чтобы не допустить возможности неправильной интерпретации кода.
Вот первый вариант функции min(). Второй аргумент функции будет возвращать количество вхождений минимального значения в вектор. Для перебора элементов массива используется цикл for. Но мы допустили ошибку в логике программы. Сможете ли вы заметить ее?
#include <vector> int min( const vector<int> &ivec, int &occurs ) { int minVal = 0; occurs = 0; int size = ivec.size(); for ( int ix = 0; ix < size; ++ix ) { if ( minVal == ivec[ ix ] ) ++occurs; else if ( minVal > ivec[ ix ] ) { minVal = ivec[ ix ]; occurs = 1; } } return minVal; |
Обычно функция возвращает только одно значение. Однако согласно нашей спецификации в точке вызова должно быть известно не только само минимальное значение, но и количество его вхождений в вектор. Для возврата второго значения мы использовали параметр типа
ссылка. (Параметры-ссылки рассматриваются в разделе 7.3.) Любое присваивание значения ссылке occurs
изменяет значение переменной, на которую она ссылается:
int main() { int occur_cnt = 0; vector< int > ivec; // occur_cnt получает значение occurs // из функции min() int minval = min( ivec, occur_cnt ); // ... |
Альтернативой использованию параметра-ссылки является применение объекта класса pair, представленного в разделе 3.14. Функция min() могла бы возвращать два значения в одной паре:
// альтернативная реализация // с помощью пары #include <uti1ity> #include <vector> typedef pair<int,int> min_va1_pair; min_va1_pair min( const vector<int> &ivec ) { int minVal = 0; int occurs = 0; // то же самое ... return make_pair( minVal, occurs ); |
К сожалению, и эта реализация содержит ошибку. Где же она? Правильно: мы инициализировали minVal
нулем, поэтому, если минимальный элемент вектора больше нуля, наша реализация вернет нулевое значение минимума и нулевое значение количества вхождений.
Программу можно изменить, инициализировав minVal первым элементом вектора:
int minVal = ivec[0];
Теперь функция работает правильно. Однако в ней выполняются некоторые лишние действия, снижающие ее эффективность.
// исправленная версия min() // оставляющая возможность для оптимизации ... int minVal = ivec[0]; occurs = 0; int size = ivec.size(); for ( int ix = 0; ix < size; ++ix ) { if ( minVal == ivec[ ix ] ) ++occurs; |
Поскольку ix
инициализируется нулем, на первой итерации цикла значение первого элемента сравнивается с самим собой. Можно инициализировать ix
единицей и избежать ненужного выполнения первой итерации. Однако при оптимизации кода мы допустили другую ошибку (наверное, стоило все оставить как было!). Сможете ли вы ее обнаружить?
// оптимизированная версия min(), // к сожалению, содержащая ошибку... int minVal = ivec[0]; occurs = 0; int size = ivec.size(); for ( int ix = 1; ix < size; ++ix ) { if ( minVal == ivec[ ix ] ) ++occurs; |
Если ivec[0]
окажется минимальным элементом, переменная occurs не получит значения 1. Конечно, исправить это очень просто, но сначала надо найти ошибку:
int minVal = ivec[0]; |
К сожалению, подобного рода недосмотры встречаются не так уж редко: программисты тоже люди и могут ошибаться. Важно понимать, что это неизбежно, и быть готовым тщательно тестировать и анализировать свои программы.
Вот окончательная версия функции min() и программа main(), проверяющая ее работу:
#include <iostream> #include <vector> int min( const vector< int > &ivec, int &occurs ) { int minVal = ivec[ 0 ]; occurs = 1; int size = ivec.size(); for ( int ix = 1; ix < size; ++ix ) { if ( minVal == ivec[ ix ] ) ++occurs; else if ( minVal > ivec[ ix ] ){ minVal = ivec[ ix ]; occurs = 1; } } return minVal; } int main() { int ia[] = { 9,1,7,1,4,8,1,3,7,2,6,1,5,1 }; vector<int> ivec( ia, ia+14 ); int occurs = 0; int minVal = min( ivec, occurs ); cout << "Минимальное значение: " << minVal << " встречается: " << occurs << " раз.\n"; return 0; |
Результат работы программы:
Минимальное значение: 1 встречается: 5 раз.
В некоторых случаях вместо инструкции if-else можно использовать более краткое и выразительное условное выражение. Например, следующую реализацию функции min():
template <class valueType> inline const valueType& min( valueType &vall, valueType &va12 ) { if ( vall < va12 ) return vall; return va12; |
можно переписать так:
template <class valueType> inline const valueType& min( valueType &vall, valueType &va12 ) { return ( vall < va12 ) ? vall : va12; |
Длинные цепочки инструкций if-else, подобные приведенной ниже, трудны для восприятия и, таким образом, являются потенциальным источником ошибок.
if ( ch == 'a' || ch == 'A' ) ++aCnt; else if ( ch == 'e' || ch == 'E' ) ++eCnt; else if ( ch == 'i' || ch == 'I' ) ++iCnt; else if ( ch == 'o' || ch == '0' ) ++oCnt; else if ( ch == 'u' || ch == 'U' ) |
В качестве альтернативы таким цепочкам С++ предоставляет инструкцию switch. Это тема следующего раздела.
Упражнение 5.3
Исправьте ошибки в примерах:
(a) if ( ivall != iva12 ) ivall = iva12 else ivall = iva12 = 0; (b) if ( ivat < minval ) minvat = ival; occurs = 1; (c) if ( int ival = get_value()) cout << "ival = " << ival << endl; if ( ! ival ) cout << "ival = 0\n"; (d) if ( ival = 0 ) ival = get_value(); (e) if ( iva1 == 0 ) |
Упражнение 5.4
Преобразуйте тип параметра occurs
функции min(), сделав его не ссылкой, а простым объектом. Запустите программу. Как изменилось ее поведение?
Инструкция switch
Длинные цепочки инструкций if-else, наподобие приведенной в конце предыдущего раздела, трудны для восприятия и потому являются потенциальным источником ошибок. Модифицируя такой код, легко сопоставить, например, разные else и if. Альтернативный метод выбора одного их взаимоисключающих условий предлагает инструкция switch.
Для иллюстрации инструкции switch
рассмотрим следующую задачу. Нам надо подсчитать, сколько раз встречается каждая из гласных букв в указанном отрывке текста. (Общеизвестно, что буква e– наиболее часто встречающаяся гласная в английском языке.) Вот алгоритм программы:
1. Считывать по одному символу из входного потока, пока они не кончатся.
2. Сравнить каждый символ с набором гласных.
3. Если символ равен одной из гласных, прибавить 1 к ее счетчику.
4. Напечатать результат.
Написанная программа была запущена, в качестве контрольного текста использовался раздел из оригинала данной книги. Результаты подтвердили, что буква e действительно самая частая:
aCnt: 394
eCnt: 721
iCnt: 461
oCnt: 349
uCnt: 186
Инструкция switch
состоит из следующих частей:
· ключевого слова switch, за которым в круглых скобках идет выражение, являющееся условием:
char ch; while ( cm >> ch ) |
switch( ch )
· набора меток case, состоящих из ключевого слова case и константного выражения, с которым сравнивается условие. В данном случае каждая метка представляет одну из гласных латинского алфавита:
case 'a': case 'e': case 'i': case 'o': |
case 'u':
· последовательности инструкций, соотносимых с метками case. В нашем примере с каждой меткой будет сопоставлена инструкция, увеличивающая значение соответствующего счетчика;
· необязательной метки default, которая является аналогом части else инструкции if-else. Инструкции, соответствующие этой метке, выполняются, если условие не отвечает ни одной из меток case. Например, мы можем подсчитать суммарное количество встретившихся символов, не являющихся гласными буквами:
default: // любой символ, не являющийся гласной |
Константное выражение в метке case
должно принадлежать к целому типу, поэтому следующие строки ошибочны:
// неверные значения меток case 3.14: // не целое |
Кроме того, две разные метки не могут иметь одинаковое значение.
Выражение условия в инструкции switch
может быть сколь угодно сложным, в том числе включать вызовы функций. Результат вычисления условия сравнивается с метками case, пока не будет найдено равное значение или не выяснится, что такого значения нет. Если метка обнаружена, выполнение будет продолжено с первой инструкции после нее, если же нет, то с первой инструкции после метки default (при ее наличии) или после всей составной инструкции switch.
В отличие от if-else
инструкции, следующие за найденной меткой, выполняются друг за другом, проходя все нижестоящие метки case и метку default. Об этом часто забывают. Например, данная реализация нашей программы выполняется совершенно не так, как хотелось бы:
#include <iostream> int main() { char ch; int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0; while ( cin >> ch ) // Внимание! неверная реализация! switch ( ch ) { case 'a': ++aCnt; case 'e': ++eCnt; case 'i': ++iCnt; case 'o': ++oCnt; case 'u': ++uCnt; } cout << "Встретилась a: \t" << aCnt << '\n' << "Встретилась e: \t" << eCnt << '\n' << "Встретилась i: \t" << iCnt << '\n' << "Встретилась o: \t" << oCnt << '\n' << "Встретилась u: \t" << uCnt << '\n'; |
}
Если значение ch
равно i, выполнение начинается с инструкции после case 'i' и iCnt
возрастет на 1. Однако следующие ниже инструкции, ++oCnt и ++uCnt, также выполняются, увеличивая значения и этих переменных. Если же переменная ch
равна a, изменятся все пять счетчиков.
Программист должен явно дать указание компьютеру прервать последовательное выполнение инструкций в определенном месте switch, вставив break. В абсолютном большинстве случаев за каждой метке case
должен следовать соответствующий break.
break
прерывает выполнение switch и передает управление инструкции, следующей за закрывающей фигурной скобкой, – в данном случае производится вывод. Вот как это должно выглядеть:
switch ( ch ) { case 'a': ++aCnt; break; case 'e': ++eCnt; break; case 'i': ++iCnt; break; case 'o': ++oCnt; break; case 'u': ++uCnt; break; |
Если почему-либо нужно, чтобы одна из секций не заканчивалась инструкцией break, то желательно написать в этом месте разумный комментарий. Программа создается не только для машин, но и для людей, и необходимо сделать ее как можно более понятной для читателя. Программист, изучающий чужой текст, не должен
сомневаться, было ли нестандартное использование языка намеренным или ошибочным.
При каком условии программист может отказаться от инструкции break и позволить программе провалиться сквозь несколько меток case? Одним из таких случаев является необходимость выполнить одни и те же действия для двух или более меток. Это может понадобиться потому, что с case
всегда связано только одно значение. Предположим, мы не хотим подсчитывать, сколько раз встретилась каждая гласная в отдельности, нас интересует только суммарное количество всех встретившихся гласных. Это можно сделать так:
int vowelCnt = 0; // ... switch ( ch ) { // любой из символов a,e,1,o,u // увеличит значение vowelCnt case 'a': case 'e': case 'i': case 'o': case 'u': ++vowe1Cnt; break; |
}
Некоторые программисты подчеркивают осознанность своих действий тем, что предпочитают в таком случае писать метки на одной строке:
switch ( ch ) { // допустимый синтаксис case 'a': case 'e': case 'i': case 'o': case 'u': ++vowe1Cnt; break; |
В данной реализации все еще осталась одна проблема: как будут восприняты слова типа
UNIX
Наша программа не понимает заглавных букв, поэтому заглавные U и I не будут отнесены к гласным. Исправить ситуацию можно следующим образом:
switch ( ch ) { case 'a': case 'A': ++aCnt; break; case 'e': case 'E': ++eCnt; break; case 'i': case 'I': ++iCnt; break; case 'o': case 'O': ++oCnt; break; case 'u': case 'U': ++uCnt; break; |
Метка default
является аналогом части else инструкции if-else. Инструкции, соответствующие default, выполняются, если условие не отвечает ни одной из меток case. Например, добавим к нашей программе подсчет суммарного количества согласных:
#include <iostream> #include <ctype.h> int main() { char ch; int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0, consonantCount=0; while ( cin >> ch ) switch ( ch ) { case 'a': case 'A': ++aCnt; break; case 'e': case 'E': ++eCnt; break; case 'i': case 'I': ++iCnt; break; case 'o': case 'O': ++oCnt; break; case 'u': case 'U': ++uCnt; break; default: if ( isa1pha( ch ) ) ++consonantCnt; break; } cout << "Встретилась a: \t" << aCnt << '\n' << "Встретилась e: \t" << eCnt << '\n' << "Встретилась i: \t" << iCnt << '\n' << "Встретилась o: \t" << oCnt << '\n' << "Встретилась u: \t" << uCnt << '\n' << "Встретилось согласных: \t" << consonantCnt << '\n'; |
}
isalpha() – функция стандартной библиотеки С; она возвращает true, если ее аргумент является буквой. isalpha() объявлена в заголовочном файле ctype.h. (Функции из ctype.h мы будем рассматривать в главе 6.)
Хотя оператор break
функционально не нужен после последней метки в инструкции switch, лучше его все-таки ставить. Причина проста: если мы впоследствии захотим добавить еще одну метку после case, то с большой вероятностью забудем вписать недостающий break.
Условная часть инструкции switch
может содержать объявление, как в следующем примере:
switch( int ival = get_response() )
ival
инициализируется значением, получаемым от get_response(), и это значение сравнивается со значениями меток case. Переменная ival видна внутри блока switch, но не вне его.
Помещать же инструкцию объявления внутри тела блока switch не разрешается. Данный фрагмент кода не будет пропущен компилятором:
case illegal_definition: // ошибка: объявление не может // употребляться в этом месте string file_name = get_file_name(); // ... |
Если бы разрешалось объявлять переменную таким образом, то ее было бы видно во всем блоке switch, однако инициализируется она только в том случае, если выполнение прошло через данную метку case.
Мы можем употребить в этом месте составную инструкцию, тогда объявление переменной file_name
будет синтаксически правильным. Использование блока гарантирует, что объявленная переменная видна только внутри него, а в этом контексте она заведомо инициализирована. Вот как выглядит правильный текст:
case ok: { // ок string file_name = get_file_name(); // ... |
}
Упражнение 5.5
Модифицируйте программу из данного раздела так, чтобы она подсчитывала не только буквы, но и встретившиеся пробелы, символы табуляции и новой строки.
Упражнение 5.6
Модифицируйте программу из данного раздела так, чтобы она подсчитывала также количество встретившихся двухсимвольных последовательностей ff, fl и fi.
Упражнение 5.7
Найдите и исправьте ошибки в следующих примерах:
(a)
switch ( ival ) { case 'a': aCnt++; case 'e': eCnt++; default: iouCnt++; |
(b)
switch ( ival ) { case 1: int ix = get_value(); ivec[ ix ] = ival; break; default: ix = ivec.sizeQ-1; ivec[ ix ] = ival; |
(c)
switch ( ival ) { case 1, 3, 5, 7, 9: oddcnt++; break; case 2, 4, 6, 8, 10: evencnt++; break; |
(d)
int iva1=512 jva1=1024, kva1=4096; int bufsize; // ... switch( swt ) { case ival: bufsize = ival * sizeof( int ); break; case jval: bufsize = jval * sizeof( int ); break; case kval: bufsize = kval * sizeof( int ); break; |
(e)
enum { illustrator = 1, photoshop, photostyler = 2 }; switch ( ival ) { case illustrator: --i11us_1icense; break; case photoshop: --pshop_1icense; break; case photostyler: --psty1er_license; |
}
Инструкция while
Синтаксис инструкции while
следующий:
while ( условие ) |
инструкция
Пока значением условия
является true, инструкция
выполняется в такой последовательности:
1.
Вычислить условие.
2. Выполнить инструкцию,
если условие
истинно.
3. Если самое первое вычисление условия
дает false, инструкция не выполняется.
Условием
может быть любое выражение:
bool quit = false; // ... while ( ! quit ) { // ... quit = do_something(); } string word; |
while ( cin >> word ){ ... }
или объявление с инициализацией:
while ( symbol *ptr = search( name )) { // что-то сделать |
}
В последнем случае ptr
видим только в блоке, соответствующем инструкции while, как это было и для инструкций for и switch.
Вот пример цикла while, обходящего множество элементов, адресуемых двумя указателями:
int sumit( int *parray_begin, int *parray_end ) { int sum = 0; if ( ! parray_begin || ! parray_end ) return sum; while ( parray_begin != parray_end ) // прибавить к sum // и увеличить указатель sum += *parray_begin++; return sum; } int ia[6] = { 0, 1, 2, 3, 4, 5 }; int main() { int sum = sumit( &ia[0], &ia[ 6 ] ); // ... |
}
Для того чтобы функция sumit()
выполнялась правильно, оба указателя должны адресовать элементы одного и того же массива (parray_end
может указывать на элемент, следующий за последним). В противном случае sumit() будет возвращать бессмысленную величину. Увы, С++ не гарантирует, что два указателя адресуют один и тот же массив. Как мы увидим в главе 12, стандартные универсальные алгоритмы реализованы подобным же образом, они принимают параметрами указатели на первый и последний элементы массива.
Упражнение 5.11
Какие ошибки допущены в следующих циклах while:
(a) string bufString, word; while ( cin >> bufString >> word ) |
// ...
(b) while ( vector<int>::iterator iter != ivec.end() ) // ... (c) while ( ptr = 0 ) ptr = find_a_value(); (d) while ( bool status = find( word )) { word = get_next_word(); if ( word.empty() ) break; // ... } if ( ! status ) |
Упражнение 5.12
while
обычно применяется для циклов, выполняющихся, пока некоторое условие истинно, например, читать следующее значение, пока не
будет достигнут конец файла. for обычно рассматривается как пошаговый цикл: индекс пробегает по определенному диапазону значений. Напишите по одному типичному примеру for и while, а затем измените их, используя цикл другого типа. Если бы вам нужно было выбрать для постоянной работы только один из этих типов, какой бы вы выбрали? Почему?
Упражнение 5.13
Напишите функцию, читающую последовательность строк из стандартного ввода до тех пор, пока одно и то же слово не встретится два раза подряд либо все слова не будут обработаны. Для чтения слов используйте while; при обнаружении повтора слова завершите цикл с помощью инструкции break. Если повторяющееся слово найдено, напечатайте его. В противном случае напечатайте сообщение о том, что слова не повторялись.
Исключения и наследование
Обработка исключений – это стандартное языковое средство для реакции на аномальное поведение программы во время выполнения. C++ поддерживает единообразный синтаксис и стиль обработки исключений, а также способы тонкой настройки этого механизма в специальных ситуациях. Основы его поддержки в языке C++ описаны в главе 11, где показано, как программа может возбудить исключение, передать управление его обработчику (если таковой существует) и как обработчики исключений ассоциируются с try-блоками.
Возможности механизма обработки исключений становятся больше, если в качестве исключений использовать иерархии классов. В этом разделе мы расскажем, как писать программы, которые умеют возбуждать и обрабатывать исключения, принадлежащие таким иерархиям.
Исключения и вопросы проектирования
С обработкой исключений в программах C++ связано несколько вопросов. Хотя поддержка такой обработки встроена в язык, не стоит использовать ее везде. Обычно она применяется для обмена информацией об ошибках между независимо разработанными частями программы. Например, автор некоторой библиотеки может с помощью исключений сообщать пользователям об ошибках. Если библиотечная функция обнаруживает аномальную ситуацию, которую не способна обработать самостоятельно, она может возбудить исключение для уведомления вызывающей программы.
В нашем примере в библиотеке определен класс iStack и его функции-члены. Разумно предположить, что программист, кодировавший main(), где используется эта библиотека, не разрабатывал ее. Функции-члены класса iStack могут обнаружить, что операция pop()
вызвана, когда стек пуст, или что операция push() вызвана, когда стек полон; однако разработчик библиотеки ничего не знал о программе, пользующейся его функциями, так что не мог разрешить проблему локально. Не сумев обработать ошибку внутри функций-членов, мы решили возбуждать исключения, чтобы известить вызывающую программу.
Хотя C++ поддерживает исключения, следует применять и другие методы обработки ошибок (например, возврат кода ошибки) – там, где это более уместно. Однозначного ответа на вопрос: “Когда ошибку следует трактовать как исключение?” не существует. Ответственность за решение о том, что считать исключительной ситуацией, возлагается на разработчика. Исключения – это часть интерфейса библиотеки, и решение о том, какие исключения она возбуждает, – важный аспект ее дизайна. Если библиотека предназначена для использования в программах, которые не должны аварийно завершаться ни при каких обстоятельствах, то она обязана разбираться с аномалиями сама либо извещать о них вызывающую программу, передавая ей управление. Решение о том, какие ошибки следует обрабатывать как исключения, – трудная часть работы по проектированию библиотеки.
В нашем примере с классом iStack
вопрос, должна ли функция push() возбуждать исключение, если стек полон, является спорным. Альтернативная и, по мнению многих, лучшая реализация push() – локальное решение проблемы: увеличение размера стека при его заполнении. В конце концов, единственное ограничение – это объем доступной программе памяти. Наше решение о возбуждении исключения при попытке поместить значение в полный стек, по-видимому, непродуманно. Можно переделать функцию-член push(), чтобы она в такой ситуации наращивала стек:
void iStack::push( int value ) { // если стек полон, увеличить размер вектора if ( full() ) _stack.resize( 2 * _stack.size() ); _stack[ _top++ ] = value; |
Аналогично следует ли функции pop()
возбуждать исключение при попытке извлечь значение из пустого стека? Интересно отметить, что класс stack из стандартной библиотеки C++ (он рассматривался в главе 6) не возбуждает исключения в такой ситуации. Вместо этого постулируется, что поведение программы при попытке выполнения подобной операции не определено. Разрешить программе продолжать работу при обнаружении некорректного состояния признали возможным. Мы уже упоминали, что в разных библиотеках определены разные исключения. Не существует пригодного для всех случаев ответа на вопрос, что такое исключение.
Не все программы должны беспокоиться по поводу исключений, возбуждаемых библиотечными функциями. Хотя есть системы, для которых простой недопустим и которые, следовательно, должны обрабатывать все исключительные ситуации, не к каждой программе предъявляются такие требования. Обработка исключений предназначена в первую очередь для реализации отказоустойчивых систем. В этом случае решение о том, должна ли программа обрабатывать все исключения, возбуждаемые библиотеками, или может закончить выполнение аварийно, – это трудная часть процесса проектирования.
Еще один аспект проектирования программ заключается в том, что обработка исключений обычно структурирована. Как правило, программа строится из компонентов, и каждый компонент решает сам, какие исключения обрабатывать локально, а какие передавать на верхние уровни. Что мы понимаем под компонентом? Например, система анализа текстовых запросов, рассмотренная в главе 6, может быть разбита на три компонента, или слоя. Первый слой – это стандартная библиотека C++, которая обеспечивает базовые операции над строками, отображениями и т.д. Второй слой – это сама система анализа текстовых запросов, где определены такие функции, как string_caps() и suffix_text(), манипулирующие текстами и использующие стандартную библиотеку как основу. Третий слой – это программа, которая применяет нашу систему. Каждый компонент строится независимо и должен принимать решения о том, какие исключительные ситуации обрабатывать локально, а какие передавать на более высокий уровень.
Не все функции должны уметь обрабатывать исключения. Обычно try-блоки и ассоциированные с ними catch-обработчики применяются в функциях, являющихся точками входа в компонент. Catch-обработчики проектируются так, чтобы перехватывать те исключения, которые не должны попасть на верхние уровни программы. Для этого также используются спецификации исключений (см. раздел 11.4).
Мы расскажем о других аспектах проектирования программ, использующих исключения, в главе 19, после знакомства с классами и иерархиями классов.
12
Исключения, определенные как иерархии классов
В главе 11 мы использовали два типа класса для описания исключений, возбуждаемых функциями-членами нашего класса iStack:
class popOnEmpty { ... }; |
class pushOnFull { ... };
В реальных программах на C++ типы классов, представляющих исключения, чаще всего организуются в группы, или иерархии. Как могла бы выглядеть вся иерархия для этих классов?
Мы можем определить базовый класс Excp, которому наследуют оба наши класса исключений. Он инкапсулирует данные и функции-члены, общие для обоих производных:
class Excp { ... }; class popOnEmpty : public Excp { ... }; |
class pushOnFull : public Excp { ... };
Одной из операцией, которые предоставляет базовый класс, является вывод сообщения об ошибке. Эта возможность используется обоими классами, стоящими ниже в иерархии:
class Excp { public: // напечатать сообщение об ошибке static void print( string msg ) { cerr << msg << endl; } |
};
Иерархию классов исключений разрешается развивать и дальше. От Excp
можно произвести другие классы для более точного описания исключений, обнаруживаемых программой:
class Excp { ... }; class stackExcp : public Excp { ... }; class popOnEmpty : public stackExcp { ... }; |
class pushOnFull : public stackExcp { ... };
class mathExcp : public Excp ( ... }; class zeroOp : public mathExcp { ... }; |
class divideByZero : public mathExcp { ... };
Последующие уточнения позволяют более детально идентифицировать аномальные ситуации в работе программы. Дополнительные классы исключений организуются как слои. По мере углубления иерархии каждый новый слой описывает все более специфичные исключения. Например, первый, самый общий слой в приведенной выше иерархии представлен классом Excp. Второй специализирует Excp, выделяя из него два подкласса: stackExcp (для исключений при работе с нашим iStack) и mathExcp
(для исключений, возбуждаемых функциями из математической библиотеки). Третий, самый специализированный слой данной иерархии уточняет классы исключений: popOnEmpty и pushOnFull
определяют два вида исключений работы со стеком, а ZeroOp и divideByZero – два вида исключений математических операций.
В последующих разделах мы рассмотрим, как возбуждаются и обрабатываются исключения, представленные классами в нашей иерархии.
Использование членов пространства имен А
Использование квалифицированных имен при каждом обращении к членам пространств может стать обременительным, особенно если имена пространств достаточно длинны. Если бы удалось сделать их короче, то такие имена проще было бы читать и набивать. Однако употребление коротких имен увеличивает риск их совпадения с другими, поэтому желательно, чтобы в библиотеках применялись пространства с длинными именами.
К счастью, существуют механизмы, облегчающие использование членов пространств имен в программах. Псевдонимы пространства имен, using-объявления и using-директивы помогают преодолеть неудобства работы с очень длинными именами.
Использование исключений
Исключениями называют аномальные ситуации, возникающие во время исполнения программы: невозможность открыть нужный файл или получить необходимое количество памяти, использование выходящего за границы индекса для какого-либо массива. Обработка такого рода исключений, как правило, плохо интегрируется в основной алгоритм программы, и программисты вынуждены изобретать разные способы корректной обработки исключения, стараясь в то же время не слишком усложнить программу добавлением всевозможных проверок и дополнительных ветвей алгоритма.
С++ предоставляет стандартный способ реакции на исключения. Благодаря вынесению в отдельную часть программы кода, ответственного за проверку и обработку ошибок, значительно облегчается восприятие текста программы и сокращается ее размер. Единый синтаксис и стиль обработки исключений можно, тем не менее, приспособить к самым разнообразным нуждам и запросам.
Механизм исключений делится на две основные части:
точка программы, в которой произошло исключение. Определение того факта, что при выполнении возникла какая-либо ошибка, влечет за собой возбуждение
исключения. Для этого в С++ предусмотрен специальный оператор throw. Возбуждение исключения в случае невозможности открыть некоторый файл выглядит следующим образом:
if ( !infile ) { string errMsg("Невозможно открыть файл: "); errMsg += fileName; throw errMsg; |
}
Место программы, в котором исключение обрабатывается. При возбуждении исключения нормальное выполнение программы приостанавливается и управление передается обработчику исключения. Поиск нужного обработчика часто включает в себя раскрутку так называемого стека вызовов программы. После обработки исключения выполнение программы возобновляется, но не с того места, где произошло исключение, а с точки, следующей за обработчиком. Для определения обработчика исключения в С++ используется ключевое слово catch. Вот как может выглядеть обработчик для примера из предыдущего абзаца:
catch (string exceptionMsg) {
log_message (exceptionMsg);
return false;
}
Каждый catch-обработчик ассоциирован с исключениями, возникающими в блоке операторов, который непосредственно предшествует обработчику и помечен ключевым словом try. Одному try-блоку могут соответствовать несколько catch-предложений, каждое из которых относится к определенному виду исключений. Приведем пример:
int* stats (const int *ia, int size) |
int *pstats = new int [4];
try {
pstats[0] = sum_it (ia,size);
pstats[1] = min_val (ia,size);
pstats[2] = max_val (ia,size);
}
catch (string exceptionMsg) {
// код обработчика
}
catch (const statsException &statsExcp) {
// код обработчика
}
pstats [3] = pstats[0] / size;
do_something (pstats);
return pstats;
}
В данном примере в теле функции stats() три оператора заключены в try-блок, а четыре – нет. Из этих четырех операторов два способны возбудить исключения.
1) int *pstats = new int [4];
Выполнение оператора new
может окончиться неудачей. Стандартная библиотека С++ предусматривает возбуждение исключения bad_alloc в случае невозможности выделить нужное количество памяти. Поскольку в примере не предусмотрен обработчик исключения bad_alloc, при его возбуждении выполнение программы закончится аварийно.
2) do_something (pstats);
Мы не знаем реализации функции do_something(). Любая инструкция этой функции, или функции, вызванной из этой функции, или функции, вызванной из функции, вызванной этой функцией, и так далее, потенциально может возбудить исключение. Если в реализации функции do_something и вызываемых из нее предусмотрен обработчик такого исключения, то выполнение stats()
продолжится обычным образом. Если же такого обработчика нет, выполнение программы аварийно завершится.
Необходимо заметить, что, хотя оператор
pstats [3] = pstats[0] / size;
может привести к делению на ноль, в стандартной библиотеке не предусмотрен такой тип исключения.
Обратимся теперь к инструкциям, объединенным в try-блок. Если в одной из вызываемых в этом блоке функций – sum_it(), min_val() или max_val() –произойдет исключение, управление будет передано на обработчик,
следующий за try-блоком и перехватывающий именно это исключение. Ни инструкция, возбудившая исключение, ни следующие за ней инструкции в try-блоке выполнены не будут. Представим себе, что при вызове функции sum_it() возбуждено исключение:
throw string ("Ошибка: adump27832");
Выполнение функции sum_it()
прервется, операторы, следующие в try-блоке за вызовом этой функции, также не будут выполнены, и pstats[0] не будет инициализирована. Вместо этого возбуждается исключительное состояние и исследуются два catch-обработчика. В нашем случае выполняется catch с параметром типа string:
catch (string exceptionMsg) { // код обработчика |
После выполнения управление будет передано инструкции, следующей за последним catch-обработчиком, относящимся к данному try-блоку. В нашем случае это
pstats [3] = pstats[0] / size;
(Конечно, обработчик сам может возбуждать исключения, в том числе – того же типа. В такой ситуации будет продолжено выполнение catch-предложений, определенных в программе, вызвавшей функцию stats().)
Вот пример:
catch (string exceptionMsg) { |
cerr << "stats(): исключение: "
<< exceptionMsg
<< endl;
delete [] pstats;
return 0;
}
В таком случае выполнение вернется в функцию, вызвавшую stats(). Будем считать, что разработчик программы предусмотрел проверку возвращаемого функцией stats()
значения и корректную реакцию на нулевое значение.
Функция stats()
умеет реагировать на два типа исключений: string и statsException. Исключение любого другого типа игнорируется, и управление передается в вызвавшую функцию, а если и в ней не найдется обработчика, – то в функцию более высокого уровня, и так до функции main().При отсутствии обработчика и там, программа аварийно завершится.
Возможно задание специального обработчика, который реагирует на любой тип исключения. Синтаксис его таков:
catch (...) { // обрабатывает любое исключение, // однако ему недоступен объект, переданный // в обработчик в инструкции throw |
}
( Детально обработка исключительных ситуаций рассматривается в главах 11 и 19.)
Упражнение 2.18
Какие ошибочные ситуации могут возникнуть во время выполнения следующей функции:
int *alloc_and_init (string file_name)
{
ifstream infile (file_name)
int elem_cnt;
infile >> elem_cnt;
int *pi = allocate_array(elem_cnt);
int elem;
int index=0;
while (cin >> elem)
pi[index++] = elem;
sort_array(pi,elem_cnt);
register_data(pi);
return pi;
}
Упражнение 2.19
В предыдущем примере вызываемые функции allocate_array(), sort_array() и register_data()
могут возбуждать исключения типов noMem, int и string соответственно. Перепишите функцию alloc_and_init(), вставив соответствующие блоки try и catch для обработки этих исключений. Пусть обработчики просто выводят в cerr сообщение об ошибке.
Упражнение 2.20
Усовершенствуйте функцию alloc_and_init()
так, чтобы она сама возбуждала исключение в случае возникновения всех возможных ошибок (это могут быть исключения, относящиеся к вызываемым функциям allocate_array(), sort_array() и register_data() и какими-то еще операторами внутри функции alloc_and_init()). Пусть это исключение имеет тип string и строка, передаваемая обработчику, содержит описание ошибки.
Использование обобщенных алгоритмов
Допустим, мы задумали написать книжку для детей и хотим понять, какой словарный состав наиболее подходит для такой цели. Чтобы ответить на этот вопрос, нужно прочитать несколько детских книг, сохранить текст в отдельных векторах строк (см. раздел 6.7) и подвергнуть его следующей обработке:
1.
Создать копию каждого вектора.
2. Слить все векторы в один.
3. Отсортировать его в алфавитном порядке.
4. Удалить все дубликаты.
5. Снова отсортировать, но уже по длине слов.
6. Подсчитать число слов, длина которых больше шести знаков (предполагается, что длина – это некоторая мера сложности, по крайней мере, в терминах словаря).
7. Удалить семантически нейтральные слова (например, союзы and (и), if (если), or (или), but (но) и т.д.).
8. Напечатать получившийся вектор.
На первый взгляд, задача на целую главу. Но с помощью обобщенных алгоритмов мы решим ее в рамках одного подраздела.
Аргументом нашей функции является вектор из векторов строк. Мы принимаем указатель на него, проверяя, не является ли он нулевым:
#include <vector> #include <string> typedef vector<string, allocator> textwords; void process_vocab( vector<textwords, allocator> *pvec ) { if ( ! pvec ) { // выдать предупредительное сообщение return; } // ... |
}
Нужно создать один вектор, включающий все элементы исходных векторов. Это делается с помощью обобщенного алгоритма copy()
(для его использования необходимо включить заголовочные файлы algorithm и iterator):
#include <algorithm> #include <iterator> void process_vocab( vector<textwords, allocator> *pvec ) { // ... vector< string > texts; vector<textwords, allocator>::iterator iter = pvec->begin(); for ( ; iter != pvec->end(); ++iter ) copy( (*iter).begin(), (*iter).end(), back_inserter( texts )); // ... |
}
Первыми двумя аргументами алгоритма copy() являются итераторы, ограничивающие диапазон подлежащих копированию элементов. Третий аргумент – это итератор, указывающий на место, куда надо копировать элементы. back_inserter
называется адаптером итератора; он позволяет вставлять элементы в конец вектора, переданного ему в качестве аргумента. (Подробнее мы рассмотрим адаптеры итераторов в разделе 12.4.).
Алгоритм unique()
удаляет из контейнера дубликаты, расположенные рядом. Если дана последовательность 01123211, то результатом будет 012321, а не 0123. Чтобы получить вторую последовательность, необходимо сначала отсортировать вектор с помощью алгоритма sort(); тогда из последовательности 01111223
получится 0123. (Хотя на самом деле получится 01231223.)
unique() не изменяет размер контейнера. Вместо этого каждый уникальный элемент помещается в очередную свободную позицию, начиная с первой. В нашем примере физический результат – это последовательность 01231223; остаток 1223 – это, так сказать, “отходы” алгоритма. unique() возвращает итератор, указывающий на начало этого остатка. Как правило, этот итератор затем передается алгоритму erase() для удаления ненужных элементов. (Поскольку встроенный массив не поддерживает операции erase(), то семейство алгоритмов unique() в меньшей степени подходит для работы с ним.) Вот соответствующий фрагмент функции:
void process_vocab( vector<textwords, allocator> *pvec ) { // ... // отсортировать вектор texts sort( texts.begin(), texts.end() ); // удалить дубликаты vector<string, allocator>::iterator it; it = unique( texts.begin(), texts.end() ); texts.erase( it, texts.end() ); // ... |
Ниже приведен результат печати вектора texts, объединяющего два небольших текстовых файла, после применения sort(), но до применения unique():
a a a a alice alive almost
alternately ancient and and and and and and
and as asks at at beautiful becomes bird
bird blows blue bounded but by calling coat
daddy daddy daddy dark darkened darkening distant each
either emma eternity falls fear fiery fiery flight
flowing for grow hair hair has he heaven,
held her her her her him him home
houses i immeasurable immensity in in in in
inexpressibly is is is it it it its
journeying lands leave leave life like long looks
magical mean more night, no not not not
now now of of on one one one
passion puts quite red rises row same says
she she shush shyly sight sky so so
star star still stone such tell tells tells
that that the the the the the the
the there there thing through time to to
to to trees unravel untamed wanting watch what
when wind with with you you you you
your your
После применения unique() и последующего вызова erase()
вектор texts
выглядит следующим образом:
a alice alive almost alternately ancient
and as asks at beautiful becomes bird blows
blue bounded but by calling coat daddy dark
darkened darkening distant each either emma eternity falls
fear fiery flight flowing for grow hair has
he heaven, held her him home houses i
immeasurable immensity in inexpressibly is it its journeying
lands leave life like long looks magical mean
more night, no not now of on one
passion puts quite red rises row same says
she shush shyly sight sky so star still
stone such tell tells that the there thing
through time to trees unravel untamed wanting watch
what when wind with you your
Следующая наша задача – отсортировать строки по длине. Для этого мы воспользуемся не алгоритмом sort(), а алгоритмом stable_sort(), который сохраняет относительные положения равных элементов. В результате для элементов равной длины сохраняется алфавитный порядок. Для сортировки по длине мы применим собственную операцию сравнения “меньше”. Один из возможных способов таков:
bool less_than( const string & s1, const string & s2 ) { return s1.size() < s1.size(); } void process_vocab( vector<textwords, allocator> *pvec ) { // ... // отсортировать элементы вектора texts по длине, // сохранив также прежний порядок stable_sort( texts.begin(), texts.end(), less_than ); // ... |
}
Нужный результат при этом достигается, но эффективность существенно ниже, чем хотелось бы. less_than()
реализована в виде одной инструкции. Обычно она вызывается как встроенная (inline) функция. Но, передавая указатель на нее, мы не даем компилятору сделать ее встроенной. Способ, позволяющий добиться этого, –применение объекта-функции:
// объект-функция - операция реализована с помощью перегрузки // оператора operator() class LessThan { public: bool operator()( const string & s1, const string & s2 ) { return s1.size() < s2.size(); } |
Объект-функция – это класс, в котором перегружен оператор вызова operator(). В теле этого оператора и реализуется логика функции, в данном случае сравнение “меньше”. Определение оператора вызова выглядит странно из-за двух пар скобок. Запись
operator()
говорит компилятору, что мы перегружаем оператор вызова. Вторая пара скобок
( const string & s1, const string & s2 )
задает передаваемые ему формальные параметры. Если сравнить это определение с предыдущим определением функции less_than(), мы увидим, что, за исключением замены less_than на operator(), они совпадают.
Объект-функция определяется так же, как обычный объект класса (правда, в данном случае нам не понадобился конструктор: нет членов, подлежащих инициализации):
LessThan lt;
Для вызова экземпляра перегруженного оператора мы применяем оператор вызова к нашему объекту класса, передавая необходимые аргументы. Например:
string st1( "shakespeare" ); string st2( "marlowe" ); // вызывается lt.operator()( st1, st2 ); |
Ниже показана исправленная функция process_vocab(), в которой алгоритму stable_sort()
передается безымянный объект-функция LessThan():
void process_vocab( vector<textwords, allocator> *pvec ) { // ... stable_sort( texts.begin(), texts.end(), LessThan() ); // ... |
Внутри stable_sort()
перегруженный оператор вызова подставляется в текст программы как встроенная функция. (В качестве третьего аргумента stable_sort() может принимать как указатель на функцию less_than(), так и объект класса LessThan, поскольку аргументом является параметр-тип шаблона. Подробнее об объектах-функциях мы расскажем в разделе 12.3.)
Вот результат применения stable_sort() к вектору texts:
a i
as at by he in is it no
of on so to and but for has
her him its not now one red row
she sky the you asks bird blue coat
dark each emma fear grow hair held home
life like long mean more puts same says
star such tell that time what when wind
with your alice alive blows daddy falls fiery
lands leave looks quite rises shush shyly sight
still stone tells there thing trees watch almost
either flight houses night, ancient becomes bounded calling
distant flowing heaven, magical passion through unravel untamed
wanting darkened eternity beautiful darkening immensity journeying alternately
immeasurable inexpressibly
Подсчитать число слов, длина которых больше шести символов, можно с помощью обобщенного алгоритма count_if() и еще одного объекта-функции – GreaterThan. Этот объект чуть сложнее, так как позволяет пользователю задать размер, с которым производится сравнение. Мы сохраняем размер в члене класса и инициализируем его с помощью конструктора (по умолчанию – значением 6):
#include <iostream> class GreaterThan { public: GreaterThan( int size = 6 ) : _size( size ){} int size() { return _size; } bool operator()( const string & s1 ) { return s1.size() > 6; } private: int _size; |
Использовать его можно так:
void process_vocab( vector<textwords, allocator> *pvec ) { // ... // подсчитать число строк, длина которых больше 6 int cnt = count_if( texts.begin(), texts.end(), GreaterThan() ); cout << "Number of words greater than length six are " << cnt << endl; // ... |
}
Этот фрагмент программы выводит такую строку:
Number of words greater than length six are 22
Алгоритм remove()
ведет себя аналогично unique(): он тоже не изменяет размер контейнера, а просто разделяет элементы на те, что следует оставить (копируя их по очереди в начало контейнера), и те, что следует удалить (перемещая их в конец контейнера). Вот как можно воспользоваться им для исключения из коллекции слов, которые мы не хотим сохранять:
void process_vocab( vector<textwords, allocator> *pvec ) { // ... static string rw[] = { "and", "if", "or", "but", "the" }; vector< string > remove_words( rw, rw+5 ); vector< string >::iterator it2 = remove_words.begin(); for ( ; it2 != remove_words.end(); ++it2 ) { // просто для демонстрации другой формы count() int cnt = count( texts.begin(), texts.end(), *it2 ); cout << cnt << " instances removed: " << (*it2) << endl; texts.erase( remove(texts.begin(),texts.end(),*it2 ), texts.end() ); } // ... |
Результат применения remove():
1 instances removed: and
0 instances removed: if
0 instances removed: or
1 instances removed: but
1 instances removed: the
Теперь нам нужно распечатать содержимое вектора. Можно обойти все элементы и вывести каждый по очереди, но, поскольку при этом обобщенные алгоритмы не используются, мы считаем такое решение неподходящим. Вместо этого проиллюстрируем работу алгоритма for_each() для вывода всех элементов вектора. for_each() применяет указатель на функцию или объект-функцию к каждому элементу контейнера из диапазона, ограниченного парой итераторов. В нашем случае объект-функция PrintElem копирует один элемент в стандартный вывод:
class PrintElem { public: PrintElem( int lineLen = 8 ) : _line_length( lineLen ), _cnt( 0 ) {} void operator()( const string &elem ) { ++_cnt; if ( _cnt % _line_length == 0 ) { cout << '\n'; } cout << elem << " "; } private: int _line_length; int _cnt; |
};
void process_vocab( vector<textwords, allocator> *pvec ) { // ... for_each( texts.begin(), texts.end(), PrintElem() ); |
Вот и все. Мы получили законченную программу, для чего пришлось лишь последовательно записать обращения к нескольким обобщенным алгоритмам. Для удобства мы приводим ниже полный листинг вместе с функцией main() для ее тестирования (здесь используются специальные типы итераторов, которые будут обсуждаться только в разделе 12.4). Мы привели текст реально исполнявшегося кода, который не полностью удовлетворяет стандарту C++. В частности, в нашем распоряжении были лишь устаревшие реализации алгоритмов count() и count_if(), которые не возвращают результат, а требуют передачи дополнительного аргумента для вычисленного значения. Кроме того, библиотека iostream
отражает предшествующую принятию стандарта реализацию, в которой требуется заголовочный файл iostream.h.
#include <vector> #include <string> #include <algorithm> #include <iterator> // предшествующий принятию стандарта синтаксис <iostream> #include <iostream.h> class GreaterThan { public: GreaterThan( int size = 6 ) : _size( sz ){} int size() { return _size; } bool operator()(const string &s1) { return s1.size() > _size; } private: int _size; }; class PrintElem { public: PrintElem( int lineLen = 8 ) : _line_length( lineLen ), _cnt( 0 ) {} void operator()( const string &elem ) { ++_cnt; if ( _cnt % _line_length == 0 ) { cout << '\n'; } cout << elem << " "; } private: int _line_length; int _cnt; |
class LessThan { public: bool operator()( const string & s1, const string & s2 ) { return s1.size() < s2.size(); } }; typedef vector<string, allocator> textwords; void process_vocab( vector<textwords, allocator> *pvec ) { if ( ! pvec ) { // вывести предупредительное сообщение return; } vector< string, allocator > texts; vector<textwords, allocator>::iterator iter; for ( iter = pvec->begin() ; iter != pvec->end(); ++iter ) copy( (*iter).begin(), (*iter).end(), back_inserter( texts )); // отсортировать вектор texts sort( texts.begin(), texts.end() ); // теперь посмотрим, что получилось for_each( texts.begin(), texts.end(), PrintElem() ); cout << "\n\n"; // разделить части выведенного текста // удалить дубликаты vector<string, allocator>::iterator it; it = unique( texts.begin(), texts.end() ); texts.erase( it, texts.end() ); // посмотрим, что осталось for_each( texts.begin(), texts.end(), PrintElem() ); cout << "\n\n"; // отсортировать элементы // stable_sort сохраняет относительный порядок равных элементов stable_sort( texts.begin(), texts.end(), LessThan() ); for_each( texts.begin(), texts.end(), PrintElem() ); cout << "\n\n"; // подсчитать число строк, длина которых больше 6 int cnt = 0; // устаревшая форма count - в стандарте используется другая count_if( texts.begin(), texts.end(), GreaterThan(), cnt ); cout << "Number of words greater than length six are " << cnt << endl; static string rw[] = { "and", "if", "or", "but", "the" }; vector<string,allocator> remove_words( rw, rw+5 ); vector<string, allocator>::iterator it2 = remove_words.begin(); for ( ; it2 != remove_words.end(); ++it2 ) { int cnt = 0; // устаревшая форма count - в стандарте используется другая count( texts.begin(), texts.end(), *it2, cnt ); cout << cnt << " instances removed: " << (*it2) << endl; texts.erase( remove(texts.begin(),texts.end(),*it2), texts.end() ); } cout << "\n\n"; for_each( texts.begin(), texts.end(), PrintElem() ); } // difference_type - это тип, с помощью которого можно хранить результат // вычитания двух итераторов одного и того же контейнера // - в данном случае вектора строк ... // обычно это предполагается по умолчанию typedef vector<string,allocator>::difference_type diff_type; // предшествующий принятию стандарта синтаксис для <fstream> #include <fstream.h> main() { vector<textwords, allocator> sample; vector<string,allocator> t1, t2; string t1fn, t2fn; // запросить у пользователя имена входных файлов ... // в реальной программе надо бы выполнить проверку cout << "text file #1: "; cin >> t1fn; cout << "text file #2: "; cin >> t2fn; // открыть файлы ifstream infile1( t1fn.c_str()); ifstream infile2( t2fn.c_str()); // специальная форма итератора // обычно diff_type подразумевается по умолчанию ... istream_iterator< string, diff_type > input_set1( infile1 ), eos; istream_iterator< string, diff_type > input_set2( infile2 ); // специальная форма итератора copy( input_set1, eos, back_inserter( t1 )); copy( input_set2, eos, back_inserter( t2 )); sample.push_back( t1 ); sample.push_back( t2 ); process_vocab( &sample ); |
}
Упражнение 12.2
Длина слова – не единственная и, вероятно, не лучшая мера трудности текста. Другой возможный критерий – это длина предложения. Напишите программу, которая читает текст из файла либо со стандартного ввода, строит вектор строк для каждого предложения и передает его алгоритму count(). Выведите предложения в порядке сложности. Любопытный способ сделать это – сохранить каждое предложение как одну большую строку во втором векторе строк, а затем передать этот вектор алгоритму sort()
вместе с объектом-функцией, который считает, что чем строка короче, тем она меньше. (Более подробно с описанием конкретного обобщенного алгоритма, а также с иллюстрацией его применения вы может ознакомиться в Приложении, где все алгоритмы перечислены в алфавитном порядке.)
Упражнение 12.3
Более надежную оценку уровня трудности текста дает анализ структурной сложности предложений. Пусть каждой запятой присваивается 1 балл, каждому двоеточию или точке с запятой – 2 балла, а каждому тире – 3 балла. Модифицируйте программу из упражнения 12.2 так, чтобы она подсчитывала сложность каждого предложения. Воспользуйтесь алгоритмом count_if() для нахождения каждого из знаков препинания в векторе предложений. Выведите предложения в порядке сложности.
Использование пространства имен
Предположим, что мы хотим предоставить в общее пользование наш класс Array, разработанный в предыдущих примерах. Однако не мы одни занимались этой проблемой; возможно, кем-то где-то, скажем, в одном из подразделений компании Intel был создан одноименный класс. Из-за того что имена этих классов совпадают, потенциальные пользователи не могут задействовать оба класса одновременно, они должны выбрать один из них. Эта проблема решается добавлением к имени класса некоторой строки, идентифицирующей его разработчиков, скажем,
class Cplusplus_Primer_Third_Edition_Array { ... };
Конечно, это тоже не гарантирует уникальность имени, но с большой вероятностью избавит пользователя от данной проблемы. Как, однако, неудобно пользоваться столь длинными именами!
Стандарт С++ предлагает для решения проблемы совпадения имен механизм, называемый пространством имен. Каждый производитель программного обеспечения может заключить свои классы, функции и другие объекты в свое собственное пространство имен. Вот как выглядит, например, объявление нашего класса Array:
namespace Cplusplus_Primer_3E { template <class elemType> class Array { ... }; |
}
Ключевое слово namespace
задает пространство имен, определяющее видимость нашего класса и названное в данном случае Cplusplus_Primer_3E. Предположим, что у нас есть классы от других разработчиков, помещенные в другие пространства имен:
namespace IBM_Canada_Laboratory {
template <class elemType> class Array { ... };
class Matrix { ... };
}
namespace Disney_Feature_Animation {
class Point { ... };
template <class elemType> class Array { ... };
}
По умолчанию в программе видны объекты, объявленные без явного указания пространства имен; они относятся к глобальному пространству имен. Для того чтобы обратиться к объекту из другого пространства, нужно использовать его квалифицированное имя, которое состоит из идентификатора пространства имен и идентификатора объекта, разделенных оператором разрешения области видимости (::). Вот как выглядят обращения к объектам приведенных выше примеров:
Cplusplus_Primer_3E::Array<string> text; |
Disney_Feature_Animation::Point origin(5000,5000);
Для удобства использования можно назначать псевдонимы
пространствам имен. Псевдоним выбирают коротким и легким для запоминания. Например:
// псевдонимы
namespace LIB = IBM_Canada_Laboratory;
namespace DFA = Disney_Feature_Animation;
int main()
{
LIB::Array<int> ia(1024);
}
Псевдонимы употребляются и для того, чтобы скрыть использование пространств имен. Заменив псевдоним, мы можем сменить набор задействованных функций и классов, причем во всем остальном код программы останется таким же. Исправив только одну строчку в приведенном выше примере, мы получим определение уже совсем другого массива:
namespace LIB = Cplusplus_Primer_3E;
int main()
{
LIB::Array<int> ia(1024);
}
Конечно, чтобы это стало возможным, необходимо точное совпадение интерфейсов классов и функций, объявленных в этих пространствах имен. Представим, что класс Array из Disney_Feature_Animation не имеет конструктора с одним параметром – размером. Тогда следующий код вызовет ошибку:
namespace LIB = Disney_Feature_Animation;
int main()
{
LIB::Array<int> ia(1024);
}
Еще более удобным является способ использования простого, неквалифицированного имени для обращения к объектам, определенным в некотором пространстве имен. Для этого существует директива using:
#include "IBM_Canada_Laboratory.h"
using namespace IBM_Canada_Laboratory;
int main()
{
// IBM_Canada_Laboratory::Matrix
Matrix mat(4,4);
// IBM_Canada_Laboratory::Array
Array<int> ia(1024);
// ...
}
Пространство имен IBM_Canada_Laboratory
становится видимым в программе. Можно сделать видимым не все пространство, а отдельные имена внутри него (селективная директива using):
#include "IBM_Canada_Laboratory.h"
using namespace IBM_Canada_Laboratory::Matrix;
// видимым становится только Matrix
int main()
{
// IBM_Canada_Laboratory::Matrix
Matrix mat(4,4);
// Ошибка: IBM_Canada_Laboratory::Array невидим
Array<int> ia(1024);
// ...
}
Как мы уже упоминали, все компоненты стандартной библиотеки С++ объявлены внутри пространства имен std. Поэтому простого включения заголовочного файла недостаточно, чтобы напрямую пользоваться стандартными функциями и классами:
#include <string>
// ошибка: string невидим
string current_chapter = "Обзор С++";
Необходимо использовать директиву using:
#include <string>
using namespace std;
// Ok: видим string
string current_chapter = "Обзор С++";
Заметим, однако, что таким образом мы возвращаемся к проблеме “засорения” глобального пространства имен, ради решения которой и был создан механизм именованных пространств. Поэтому лучше использовать либо квалифицированное имя:
#include <string> |
std::string current_chapter = "Обзор С++";
либо селективную директиву using:
#include <string>
using namespace std::string;
// Ok: string видим
string current_chapter = "Обзор С++";
Мы рекомендуем пользоваться последним способом.
В большинстве примеров этой книги директивы пространств имен были опущены. Это сделано ради сокращения размера кода, а также потому, что большинство примеров были скомпилированы компилятором, не поддерживающим пространства имен – достаточно недавнего нововведения С++. (Детали применения using-объявлений при работе с стандартной библиотекой С++ обсуждаются в разделе 8.6.)
В нижеследующих главах мы создадим еще четыре класса: String, Stack, List и модификацию Stack. Все они будут заключены в одно пространство имен – Cplusplus_Primer_3E. (Более подробно работа с пространствами имен рассматривается в главе 8.)
Упражнение 2.21
Дано пространство имен
namespace Exercize { |
class Array { ... };
template <class EType>
void print (Array< EType > );
class String { ... }
template <class ListType>
class List { ... };
}
и текст программы:
int main() { |
Array<String> as (size);
List<int> il (size);
// ...
Array<String> *pas = new Array<String>(as);
List<int> *pil = new List<int>(il);
print (*pas);
}
Программа не компилируется, поскольку объявления используемых классов заключены в пространство имен Exercise. Модифицируйте код программы, используя
(a) квалифицированные имена
(b) селективную директиву using
(c) механизм псевдонимов
(d) директиву using
Использование шаблонов
Наш класс IntArray
служит хорошей альтернативой встроенному массиву целых чисел. Но в жизни могут потребоваться массивы для самых разных типов данных. Можно предположить, что единственным отличием массива элементов типа double от нашего является тип данных в объявлениях, весь остальной код совпадает буквально.
Для решения данной проблемы в С++ введен механизм шаблонов. В объявлениях классов и функций допускается использование параметризованных типов. Типы-параметры заменяются в процессе компиляции настоящими типами, встроенными или определенными пользователем. Мы можем создать шаблон класса Array, заменив в классе IntArray тип элементов int на обобщенный тип-параметр. Позже мы конкретизируем
типы-параметры, подставляя вместо них реальные типы int, double и string. В результате появится способ использовать эти конкретизации так, как будто мы на самом деле определили три разных класса для этих трех типов данных.
Вот как может выглядеть шаблон класса Array:
template <class elemType> class Array { public: explicit Array( int sz = DefaultArraySize ); Array( const elemType *ar, int sz ); Array( const Array &iA ); virtual ~Array() { delete[] _ia; } Array& operator=( const Array & ); int size() const { return _size; } virtual elemType& operator[]( int ix ) { return _ia[ix]; } virtual void sort( int,int ); virtual int find( const elemType& ); virtual elemType min(); virtual elemType max(); protected: void init( const elemType*, int ); void swap( int, int ); static const int DefaultArraySize = 12; int _size; elemType *_ia; |
};
Ключевое слово template
говорит о том, что задается шаблон,
параметры которого заключаются в угловые скобки (<>). В нашем случае имеется лишь один параметр elemType; ключевое слово class
перед его именем сообщает, что этот параметр представляет собой тип.
При конкретизации класса-шаблона Array
параметр elemType
заменяется на реальный тип при каждом использовании, как показано в примере:
#include <iostream> #include "Array.h" int main() { const int array_size = 4; // elemType заменяется на int Array<int> ia(array_size); // elemType заменяется на double Array<double> da(array_size); // elemType заменяется на char Array<char> ca(array_size); int ix; for ( ix = 0; ix < array_size; ++ix ) { ia[ix] = ix; da[ix] = ix * 1.75; ca[ix] = ix + 'a'; } for ( ix = 0; ix < array_size; ++ix ) cout << "[ " << ix << " ] ia: " << ia[ix] << "\tca: " << ca[ix] << "\tda: " << da[ix] << endl; return 0; |
Здесь определены три экземпляра класса Array:
Array<int> ia(array_size); |
Array<char> ca(array_size);
Что делает компилятор, встретив такое объявление? Подставляет текст шаблона Array, заменяя параметр elemType на тот тип, который указан в каждом конкретном случае. Следовательно, объявления членов приобретают в первом случае такой вид:
// Array<int> ia(array_size);
int _size;
int *_ia;
Заметим, что это в точности соответствует определению массива IntArray.
Для оставшихся двух случаев мы получим следующий код:
// Array<double> da(array_size); |
double *_ia;
// Array<char> ca(array_size);
int _size;
char *_ia;
Что происходит с функциями-членами? В них тоже тип-параметр elemType заменяется на реальный тип, однако компилятор не конкретизирует те функции, которые не вызываются в каком-либо месте программы. (Подробнее об этом в разделе 16.8.)
При выполнении программа этого примера выдаст следующий результат:
[ 0 ] ia: 0 ca: a da: 0
[ 1 ] ia: 1 ca: b da: 1.75
[ 2 ] ia: 2 ca: c da: 3.5
[ 3 ] ia: 3 ca: d da: 5.25
Механизм шаблонов можно использовать и в наследуемых классах. Вот как выглядит определение шаблона класса ArrayRC:
#include <cassert> |
template <class elemType>
class ArrayRC : public Array<elemType> {
public:
ArrayRC( int sz = DefaultArraySize )
: Array<elemType>( sz ) {}
ArrayRC( const ArrayRC& r )
: Array<elemType>( r ) {}
ArrayRC( const elemType *ar, int sz )
: Array<elemType>( ar, sz ) {}
elemType& ArrayRC<elemType>::operator[]( int ix )
{
assert( ix >= 0 && ix < Array<elemType>::_size );
return _ia[ ix ];
}
private:
// ...
};
Подстановка реальных параметров вместо типа-параметра elemType происходит как в базовом, так и в производном классах. Определение
ArrayRC<int> ia_rc(10);
ведет себя точно так же, как определение IntArrayRC из предыдущего раздела. Изменим пример использования из предыдущего раздела. Прежде всего, чтобы оператор
// функцию swap() тоже следует сделать шаблоном |
был допустимым, нам потребуется представить функцию swap() в виде шаблона.
#include "Array.h" |
inline void
swap( Array<elemType> &array, int i, int j )
{
elemType tmp = array[ i ];
array[ i ] = array[ j ];
array[ j ] = tmp;
}
При каждом вызове swap()
генерируется подходящая конкретизация, которая зависит от типа массива. Вот как выглядит программа, использующая шаблоны Array и ArrayRC:
#include <iostream> |
#include "ArrayRC.h"
template <class elemType>
inline void
swap( Array<elemType> &array, int i, int j )
{
elemType tmp = array[ i ];
array[ i ] = array[ j ];
array[ j ] = tmp;
}
int main()
{
Array<int> ia1;
ArrayRC<int> ia2;
cout << "swap() with Array<int> ia1" << endl;
int size = ia1.size();
swap( ia1, 1, size );
cout << "swap() with ArrayRC<int> ia2" << endl;
size = ia2.size();
swap( ia2, 1, size );
return 0;
}
Упражнение 2.13
Пусть мы имеем следующие объявления типов:
template<class elemType> class Array; |
typedef string *Pstring;
Есть ли ошибки в приведенных ниже описаниях объектов?
(a) Array< int*& > pri(1024); (b) Array< Array<int> > aai(1024); (c) Array< complex< double > > acd(1024); (d) Array< Status > as(1024); |
Упражнение 2.14
Перепишите следующее определение, сделав из него шаблон класса:
class example1 { public: example1 (double min, double max); example1 (const double *array, int size); double& operator[] (int index); bool operator== (const example1&) const; bool insert (const double*, int); bool insert (double); double min (double) const { return _min; }; double max (double) const { return _max; }; void min (double); void max (double); int count (double value) const; private: int size; double *parray; double _min; double _max; |
Упражнение 2.15
Имеется следующий шаблон класса:
template <class elemType> class Example2 { |
explicit Example2 (elemType val=0) : _val(val) {};
bool min(elemType value) { return _val < value; }
void value(elemType new_val) { _val = new_val; }
void print (ostream &os) { os << _val; }
private:
elemType _val;
}
template <class elemType>
ostream& operator<<(ostream &os,const Example2<elemType> &ex)
{ ex.print(os); return os; }
Какие действия вызывают следующие инструкции?
(a) Example2<Array<int>*> ex1; (b) ex1.min (&ex1); (c) Example2<int> sa(1024),sb; (d) sa = sb; (e) Example2<string> exs("Walden"); |
Упражнение 2.16
Пример из предыдущего упражнения накладывает определенные ограничения на типы данных, которые могут быть подставлены вместо elemType. Так, параметр конструктора имеет по умолчанию значение 0:
explicit Example2 (elemType val=0) : _val(val) {};
Однако не все типы могут быть инициализированы нулем (например, тип string), поэтому определение объекта
Example2<string> exs("Walden");
является правильным, а
Example2<string> exs2;
приведет к синтаксической ошибке[4]. Также ошибочным будет вызов функции min(), если для данного типа не определена операция меньше. С++ не позволяет задать ограничения для типов, подставляемых в шаблоны. Как вы думаете, было бы полезным иметь такую возможность? Если да, попробуйте придумать синтаксис задания ограничений и перепишите в нем определение класса Example2. Если нет, поясните почему.
Упражнение 2.17
Как было показано в предыдущем упражнении, попытка использовать шаблон Example2 с типом, для которого не определена операция меньше, приведет к синтаксической ошибке. Однако ошибка проявится только тогда, когда в тексте компилируемой программы действительно встретится вызов функции min(), в противном случае компиляция пройдет успешно. Как вы считаете, оправдано ли такое поведение? Не лучше ли предупредить об ошибке сразу, при обработке описания шаблона? Поясните свое мнение.
Итератор istream_iterator
В общем виде объявление потокового итератора чтения istream_iterator
имеет форму:
istream_iterator<Type> identifier( istream& );1[O.A.3].
где Type – это любой встроенный или пользовательский тип класса, для которого определен оператор ввода. Аргументом конструктора может быть объект либо класса istream, например cin, либо производного от него класса с открытым типом наследования – ifstream:
#include <iterator> #include <fstream> #include <string> #include <complex> // прочитать последовательность объектов типа complex // из стандартного ввода istream_iterator< complex > is_complex( cin ); // прочитать последовательность строк из именованного файла ifstream infile( "C++Primer" ); |
istream_iterator< string > is_string( infile );
При каждом применении оператора инкремента к объекту типа istream_iterator
читается следующий элемент из входного потока, для чего используется оператор operator>>(). Чтобы сделать то же самое в обобщенных алгоритмах, необходимо предоставить пару итераторов, обозначающих начальную и конечную позицию в файле. Начальную позицию дает istream_iterator, инициализированный объектом istream, – такой, скажем, как is_string. Для получения конечной позиции мы используем специальный конструктор по умолчанию класса istream_iterator:
// конструирует итератор end_of_stream, который будет служить маркером // конца потока в итераторной паре istream_iterator< string > end_of_stream vector<string> text; // правильно: передаем пару итераторов copy( is_string, end_of_stream, inserter( text, text.begin() )); |
Итератор ostream_iterator
Объявление потокового итератора записи ostream_iterator
может быть представлено в двух формах:
ostream_iterator<Type> identifier( ostream& ) |
ostream_iterator<Type> identifier( ostream&, char * delimiter )
где Type – это любой встроенный или пользовательский тип класса, для которого определен оператор вывода (operator<<). Во второй форме delimiter – это разделитель, то есть C-строка символов, которая выводится в файл после каждого элемента. Такая строка должна заканчиваться двоичным нулем, иначе поведение программы не определено (скорее всего, она аварийно завершит выполнение). В качестве аргумента ostream может выступать объект класса ostream, например cout, либо производного от него класса с открытым типом наследования, скажем ofstream:
#include <iterator> #include <fstream> #include <string> #include <complex> // записать последовательность объектов типа complex // в стандартный вывод, разделяя элементы пробелами ostream_iterator< complex > os_complex( cin, " " ); // записать последовательность строк в именованный файл ofstream outfile( "dictionary" ); |
ostream_iterator< string > os_string( outfile, "\n" );
Вот простой пример чтения из стандартного ввода и копирования на стандартный вывод с помощью безымянных потоковых итераторов и обобщенного алгоритма copy():
#include <iterator> #include <algorithm> #include <iostream> int main() { copy( istream_iterator< int >( cin ), istream_iterator< int >(), ostream_iterator< int >( cout, " " )); |
}
Ниже приведена небольшая программа, которая открывает указанный пользователем файл и копирует его на стандартный вывод, применяя для этого алгоритм copy() и потоковый итератор записи ostream_iterator:
#include <string> #include <algorithm> #include <fstream> #include <iterator> main() { string file_name; cout << "please enter a file to open: "; cin >> file_name; if ( file_name.empty() || !cin ) { cerr << "unable to read file name\n"; return -1; } ifstream infile( file_name.c_str()); if ( !infile ) { cerr << "unable to open " << file_name << endl; return -2; } istream_iterator< string > ins( infile ), eos; ostream_iterator< string > outs( cout, " " ); copy( ins, eos, outs ); |
}
Итераторы
Итератор предоставляет обобщенный способ перебора элементов любого контейнера– как последовательного, так и ассоциативного. Пусть iter
является итератором для какого-либо контейнера. Тогда
++iter;
перемещает итератор так, что он указывает на следующий элемент контейнера, а
*iter;
разыменовывает итератор, возвращая элемент, на который он указывает.
Все контейнеры имеют функции-члены begin() и end().
· begin()
возвращает итератор, указывающий на первый элемент контейнера.
· end()
возвращает итератор, указывающий на элемент, следующий за последним в контейнере.
Чтобы перебрать все элементы контейнера, нужно написать:
for ( iter = container. begin(); iter != container.end(); ++iter ) |
do_something_with_element( *iter );
Объявление итератора выглядит слишком сложным. Вот определение пары итераторов вектора типа string:
// vector<string> vec; vector<string>::iterator iter = vec.begin(); |
vector<string>::iterator iter_end = vec.end();
В классе vector для определения iterator
используется typedef. Синтаксис
vector<string>::iterator
ссылается на iterator, определенный с помощью
typedef
внутри класса vector, содержащего элементы типа string.
Для того чтобы напечатать все элементы вектора, нужно написать:
for( ; iter != iter_end; ++iter ) |
cout << *iter << '\n';
Здесь значением *iter
выражения является, конечно, элемент вектора.
В дополнение к типу iterator в каждом контейнере определен тип const_iterator, который необходим для навигации по контейнеру, объявленному как const. const_iterator позволяет только читать элементы контейнера:
#include <vector> void even_odd( const vector<int> *pvec, vector<int> *pvec_even, vector<int> *pvec_odd ) { // const_iterator необходим для навигации по pvec vector<int>::const_iterator c_iter = pvec->begin(); vector<int>::const_1terator c_iter_end = pvec->end(); for ( ; c_iter != c_iter_end; ++c_iter ) if ( *c_iter % 2 ) pvec_even->push_back( *c_iter ); else pvec_odd->push_back( *c_iter ); |
}
Что делать, если мы хотим просмотреть некоторое подмножество элементов, например взять каждый второй или третий элемент, или хотим начать с середины? Итераторы поддерживают адресную арифметику, а значит, мы можем прибавить некоторое число к итератору:
vector<int>::iterator iter = vec->begin()+vec.size()/2;
iter
получает значение адреса элемента из середины вектора, а выражение
iter += 2;
сдвигает iter на два элемента.
Арифметические действия с итераторами возможны только для контейнеров vector и deque. list не поддерживает адресную арифметику, поскольку его элементы не располагаются в непрерывной области памяти. Следующее выражение к списку неприменимо:
ilist.begin() + 2;
так как для перемещения на два элемента необходимо два раза перейти по адресу, содержащемуся в закрытом члене next. У классов vector и deque перемещение на два элемента означает прибавление 2 к указателю на текущий элемент. (Адресная арифметика рассматривается в разделе 3.3.)
Объект контейнерного типа может быть инициализирован парой итераторов, обозначающих начало и конец последовательности копируемых в новый объект элементов. (Второй итератор должен указывать на элемент, следующий за последним копируемым.) Допустим, есть вектор:
#include <vector> #include <string> #include <iostream> int main() { vector<string> svec; string intext; while ( cin >> intext ) svec.push_back( intext ); // обработать svec ... |
Вот как можно определить новые векторы, инициализируя их элементами первого вектора:
int main() { vector<string> svec; // ... // инициализация svec2 всеми элементами svec vector<string> svec2( svec.begin(), svec.end() ); // инициализация svec3 первой половиной svec vector<string>::iterator it = svec.begin() + svec.size()/2; vector<string> svec3 ( svec.begin(), it ); // ... |
Использование специального типа istream_iterator (о нем рассказывается в разделе 12.4.3) упрощает чтение элементов из входного потока в svec:
#include <vector> #include <string> #include <iterator> int mainQ { // привязка istream_iterator к стандартному вводу istream_iterator<string> infile( cin ); // istream_iterator, отмечающий конец потока istream_iterator<string> eos; // инициализация svec элементами, считываемыми из cin; vector<string> svec( infile, eos ); // ... |
Кроме итераторов, для задания диапазона значений, инициализирующих контейнер, можно использовать два указателя на массив встроенного типа. Пусть есть следующий массив строк:
#include <string> string words[4] = { "stately", "plump", "buck", "mulligan" |
Мы можем инициализировать вектор с помощью указателей на первый элемент массива и на элемент, следующий за последним:
vector< string > vwords( words, words+4 );
Второй указатель служит “стражем”: элемент, на который он указывает, не копируется.
Аналогичным образом можно инициализировать список целых элементов:
int ia[6] = { 0, 1, 2, 3, 4, 5 }; |
В разделе 12.4 мы снова обратимся к итераторам и опишем их более детально. Сейчас информации достаточно для того, чтобы использовать итераторы в нашей системе текстового поиска. Но прежде чем вернуться к ней, рассмотрим некоторые дополнительные операции, поддерживаемые контейнерами.
Упражнение 6.9
Какие ошибки допущены при использовании итераторов:
const vector< int > ivec; vector< string > svec; list< int > ilist; (a) vector<int>::iterator it = ivec.begin(); (b) list<int>::iterator it = ilist.begin()+2; (c) vector<string>::iterator it = &svec[0]; (d) for ( vector<string>::iterator it = svec.begin(); it != 0; ++it ) |
Упражнение 6.10
Найдите ошибки в использовании итераторов:
int ia[7] = { 0, 1, 1, 2, 3, 5, 8 }; string sa[6] = { "Fort Sumter", "Manassas", "Perryville", "Vicksburg", "Meridian", "Chancellorsvine" }; (a) vector<string> svec( sa, &sa[6] ); (b) list<int> ilist( ia+4, ia+6 ); (c) list<int> ilist2( ilist.begin(), ilist.begin()+2 ); (d) vector<int> ivec( &ia[0], ia+8 ); (e) list<string> slist( sa+6, sa ); |
Итераторы вставки
Вот еще один фрагмент программы, в котором есть тонкая, но серьезная ошибка. Видите ли вы, в чем она заключается?
int ia[] = { 0, 1, 1, 2, 3, 5, 5, 8 }; vector< int > ivec( ia, ia+8 ), vres; // ... // поведение программы во время выполнения не определено |
unique_copy( ivec.begin(), ivec.end(), vres.begin() );
Проблема вызвана тем, что алгоритм unique_copy() использует присваивание для копирования значения каждого элемента из вектора ivec, но эта операция завершится неудачно, поскольку в vres не выделено место для хранения девяти целых чисел.
Можно было бы написать две версии алгоритма unique_copy(): одна присваивает элементы, а вторая вставляет их. Эта последняя версия должна, в таком случае, поддерживать вставку в начало, в конец или в произвольное место контейнера.
Альтернативный подход, принятый в стандартной библиотеке, заключается в определении трех адаптеров, которые возвращают специальные итераторы вставки:
· back_inserter()
вызывает определенную для контейнера операцию вставки push_back()
вместо оператора присваивания. Аргументом back_inserter() является сам контейнер. Например, вызов unique_copy()
можно исправить, написав:
// правильно: теперь unique_copy() вставляет элементы с помощью // vres.push_back()... unique_copy( ivec.begin(), ivec.end(), |
back_inserter( vres ) );
· front_inserter()
вызывает определенную для контейнера операцию вставки push_front()
вместо оператора присваивания. Аргументом front_inserter()
тоже является сам контейнер. Заметьте, однако, что класс vector не поддерживает push_front(), так что использовать такой адаптер для вектора нельзя:
// увы, ошибка: // класс vector не поддерживает операцию push_front() // следует использовать контейнеры deque или list unique_copy( ivec.begin(), ivec.end(), |
front_inserter( vres ) );
· inserter()
вызывает определенную для контейнера операцию вставки insert()
вместо оператора присваивания. inserter() принимает два аргумента: сам контейнер и итератор, указывающий позицию, с которой должна начаться вставка:
unique_copy( ivec.begin(), ivec.end(), |
· Итератор, указывающий на позицию начала вставки, сдвигается вперед после каждой вставки, так что элементы располагаются в нужном порядке, как если бы мы написали:
vector< int >::iterator iter = vres.begin(), iter2 = ivec.begin(); for ( ; iter2 != ivec.end() ++ iter, ++iter2 ) |