Для изменения компонентных переменных x_flags, x_width, x_fill, x_precision программист может использовать общедоступные функции класса ios:
static long bitalloc ();
возвращаемое значение может быть использовано для установки, очистки и проверки флагов. Функция предназначена для заданных пользователем флагов форматирования;
char fill ();
возвращает текущий символ заполнения незанятых (пустых) позиций поля вывода;
char fill(char);
заменяет символ заполнения значением параметра, возвращает предыдущий символ заполнения;
long flags ();
возвращает текущий набор битов флагов форматирования;
long flags(long);
устанавливает биты флагов форматирования в соответствии со значением параметра. Возвращает предыдущее значение флагов;
int precision();
возвращает текущее значение точности представления при выводе вещественных чисел (типа float и double);
int precision (int n);
устанавливает по значению параметра n точность представления вещественных чисел, возвращает предыдущее значение точности;
long setf(long);
устанавливает флаги в соответствии с тем, как они помечены в фактическом параметре. Возвращает предыдущую установку флагов;
long setf(long _setbits, long _field);
устанавливает флаги в соответствии со значениями параметров. Биты, помеченные в параметре _field, сбрасываются (очищаются), а затем устанавливаются те биты, которые отмечены в параметре _setbite;
long unsetf (long);
сбрасываются (очищаются) все биты флагов, которые помечены в параметре. Функция возвращает предыдущее значение флагов;
int width ();
возвращает установленное значение ширины поля;
int width(int);
устанавливает значение ширины поля в соответствии со значением параметра;
static int xalloc ();
возвращает индекс массива до сих пор, не использованных слов, которые можно использовать в качестве определенных флагов форматирования.
Следующие компоненты (константы) класса ios определены как статические, т.е. существуют в единственном экземпляре для класса в целом и требуют при обращении указания имени класса (ios::). В определении класса ios они описаны таким образом:
static const long adjustfield; // left | right | internal
static const long basefield; // dec | oct | hex
static const long floatfield; // scientific | fixed
Каждая из этих констант объединяет несколько установленных битов флагов форматирования. Эти константы удобно использовать в том случае, когда перед установкой флага требуется сбросить все флаги, которые не могут быть одновременно с ним установлены. Для сбрасывания флагов константа используется в качестве второго параметра функции setf ().
Объяснять тонкости применения перечисленных компонентов класса ios нужно на примерах, причем понимание смысла и значимости отдельных компонент приходит только с учетом их взаимосвязей.
В следующей программе демонстрируются основные принципы форматирования с помощью компонентных функций класса ios. Отметим, что определение класса ios включается в программу автоматически, так как файл iostream.h содержит описания классов, производных от класса ios.
//Р11-02.СРР - форматирование выводимой информации
#include < strstrem.h >
void main()
{ char name[] " "Строка длиной 52 символа "
"в поле шириной 58 позиций.";
cout << "\n\n";
cout.width(58); // Ширина поля вывода для потока cout
// Символ заполнения пустых позиций поля:
cout .fill ('$');
// Первый вывод строки в поток cout:
cout << nаmе << end1;
cout.width(58); // Убрать нельзя
// Заполнитель между знаком и значением:
cout.self(ios::internal);
double dd = -33.4455;
cout << dd << end1; // Вывод вещественного значения
cout.width(58); // Убрать нельзя
// Смена выравнивания:
cout.setf(ios::left,ios::adjustfield);
// Символ заполнения пустых позиций поля:
cout.fill('#');
// Второй вывод строки в поток cout:
cout << nаmе << end1;
long nn = 90000; // Шестнадцатеричное значение Oxl5f90
// Смена основания счисления:
cout.setf(ios:: hex, ios::basefield);
// Выводить признак основания счисления:
cout.setf(ios::showbase);
// Переход на верхний регистр:
cout.setf(ios::uppercase);
cout.width(58); // Убрать нельзя
cout << nn << end1; // Вывод целого значения типа long
cout. width (58); //Убрать нельзя
// Смена выравнивания:
cout.self(ios::internal,ios::adjustfield);
// Символ заполнения (пустых позиций поля:
cout.fill('$');
cout.unsetf(0х0200); // Переход на нижний регистр
cout << nn << end1; // Вывод целого значения типа long
}
Результат выполнения программы:
$$$$$$Строка длиной 52 символа в поле шириной 58 позиций.
-$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$33.4455
Строка длиной 52 символа в поле шириной 58 позиций. # # # # # #
OX15F90# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
Ox$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$15f90
Прокомментируем программу и результаты. По умолчанию ширина поля вывода устанавливается равной длине принятого представления выводимого значения. Поэтому действие функции width () однократное, и ее нужно при каждом выводе значения явно использовать, если умалчиваемое значение ширины поля вывода не устраивает программиста. Функция fill () устанавливает символ заполнения пустых позиций поля. При первом выводе строки nаmе [] по умолчанию установлено выравнивание по правому краю поля, и символы ' $ ' помещены слева от содержимого строки. Перед выводом значения вещественной переменной dd функцией setf() установлен флаг internal. Под его влиянием символ заполнения разместился между знаком ' - ' и числовым значением 33. 4455. Ширина поля явно указана в 58 позиций.
Перед вторым выводом строки nаmе [] "под влиянием" второго параметра (adjustfield) функции setf() сброшены флаги right и internal и явно установлен флаг left выравнивания по левому краю. Изменен символ заполнения пустых позиций ' # '. Перед выводом длинного целого числа nn установлено основание системы счисления (basefield - сбрасывает флаги оснований счисления; hex -явно устанавливает шестнадцатеричное основание). Установлены флаги showbase и uppercase и ширина поля вывода.
Число 90000 выведено в шестнадцатеричном виде, признаком ох обозначено основание системы счисления, для изображения шестнадцатеричных цифр и признака основания используются прописные буквы. Так как при переходе к выравниванию по левому краю флаг internal оказался сброшенным, то символ заполнения ' # ' размещен не после признака основания счисления ОХ, а заполняет правую пустую часть поля. Заключительный вывод значения nn, равного 90000, выполнен с флагами internal и left. Для перехода на нижний регистр использована функция unsetf () с явным значением флага uppercase.
Манипуляторы. Несмотря на гибкость и большие возможности управления форматами с помощью компонентных функций класса ios, их применение достаточно громоздко. Более простой способ изменения параметров и флагов форматирования обеспечивают манипуляторы, к возможностям которых мы перейдем.
Манипуляторами называют специальные функции, позволяющие программисту изменять состояния и флаги потока. Особенность манипуляторов и их отличие от обычных функций состоит в том, что их имена (без параметров) и вызовы (с параметрами) можно использовать в качестве правого операнда для операции обмена (<< или >>). В качестве левого операнда в этом выражении, как обычно, используется поток (ссылка на поток), и именно на этот поток оказывает влияние манипулятор.
Прежде чем переходить к перечислению манипуляторов и их свойств, напомним, что мы уже пользовались некоторыми из них (см. гл. 5, программу Р5-ОЗ. СРР). Например, манипулятор hex позволяет устанавливать шестнадцатеричное основание счисления выводимых в поток cout числовых значений. Выполнив последовательность операторов:
cout << "\nДесятичное число: " << 15 << hex;
cout << "\nШестнадцатеричное представление: " << 15;
получим на экране:
Десятичное число: 15
Шестнадцатеричное представление: OxF
Как наглядно показывает результат, применение манипулятора hex изменило одно из свойств потока cout. Десятичная целая константа 15 воспринимается и выводится на экран в шестнадцатеричном виде.
В качестве параметра каждый манипулятор автоматически (без явного участия программиста) получает ссылку на тот поток, с которым он используется в выражении. После выполнения манипулятора он возвращает ссылку на тот же поток. Поэтому манипуляторы можно использовать в цепочке включений в поток или извлечений из потока. При выполнении манипулятора никаких обменов данными с потоком не происходит, но манипулятор изменяет состояние потока. Например, выполнив, оператор с манипуляторами hex, oct, dec:
cout << 15 << hex << 15 << oct << 15 << dec << 15;
в качестве результата получим:
150XF01715
Манипуляторы библиотеки классов ввода-вывода языка Си++ делятся на две группы: манипуляторы с параметрами и манипуляторы без параметров.
Манипуляторы без параметров:
dec - при вводе и выводе устанавливает флаг десятичной системы счисления;
hex - при вводе и выводе устанавливает флаг шестнадцатеричной системы счисления;
oct - при вводе и выводе устанавливает флаг восьмеричной системы счисления;
ws - действует только при вводе и предусматривает извлечение из входного потока пробельных символов (пробел, знаки табуляции '\t' и '\v', символ перевода строки '\n', символ возврата каретки '\r', символ перевода страницы'\f');
endl - действует только при выводе, обеспечивает включение в выходной поток символа новой строки и сбрасывает буфер (выгружает содержимое) этого потока;
ends - действует только при выводе и обеспечивает включение в поток нулевого признака конца строки;
flush - действует только при выводе и очищает выходной поток, т.е. сбрасывает его буфер (выгружает содержимое буфера).
Обратите внимание, что не все перечисленные манипуляторы действуют как на входные, так и на выходные потоки. ws действует только при вводе; endl, ends, flush - только при выводе.
Манипуляторы dec, hex, oct, задающие основание системы счисления, изменяют состояние потока, и это изменение остается в силе до следующего явного изменения.
Манипулятор endl рекомендуется использовать при каждом выводе, который должен быть незамедлительно воспринят пользователем. Например, его использование просто необходимо в таком операторе:
cout << "Ждите! Идет набор статистики." << end1;
При отсутствии endl здесь нельзя гарантировать, что сообщение пользователю не останется в буфере потока cout до окончания набора статистики.
Рекомендуется с помощью манипулятора flush сбрасывать буфер входного потока при выводе на экран подсказки до последующего ввода информации:
cout << "Введите название файла:" << flush;
cin >> fileName; // Здесь fileName - символьный массив
Манипуляторы с параметрами определены в файле iomanip.h:
setbase(int n)
устанавливает основание (n) системы счисления. Значениями параметра n могут быть: 0, 8, 10 или 16. При использовании параметра 0 основание счисления при выводе выбирается десятичным. При вводе параметр 0 означает, что целые десятичные цифры из входного потока должны обрабатываться по правилам стандарта ANSI языка Си;
resetiosflags(long L)
сбрасывает (очищает) отдельные флаги состояния потоков ввода и вывода на основе битового представления значения параметра L (сбрасывает флаги в соответствии с единичными битами);
setiosflags(long L)
устанавливает отдельные флаги состояния (форматные биты) потоков ввода-вывода на основе битового представления значения параметра L (устанавливаются флаги в соответствии с единичными битами параметра);
setfill(int n)
значение параметра n в дальнейшем используется в качестве кода символа-заполнителя, который помещается в незанятых позициях поля при вводе значения. (См. компонент х_ fill класса ios.);
setprecision(int n)
определяет с помощью значения параметра n точность представления вещественных чисел, т.е. максимальное количество цифр дробной части числа при вводе и выводе. (См. компонент x_precision класса ioas);
setw(int n)
значение параметра n задает минимальную ширину поля вывода. (См. компонент x_width класса ios.)
С помощью манипуляторов можно управлять представлением информации в выходном потоке. Например, манипулятор setw (int n) позволит выделить для числового значения поле фиксированной ширины, что удобно при печати таблиц.
Итак, для управления форматом вывода (включения в поток) класс ios имеет следующие компоненты:
При передачах данных базовых типов потоки cin, cout "знают" как выполнять преобразования значений разных типов. Напомним, что при перегрузке функций конкретная реализация выбирается в соответствии с сигнатурой фактических параметров. Подобным же образом в выражениях cin >> операнд и cout << операнд каждому типу правого операнда соответствует свое правило преобразования данных. Эти правила заранее оформлены в виде процедур (функций) специального вида, называемых операциями-функциями. Определения этих функций находятся в библиотеке ввода-вывода, а прототипы размещены в заголовочном файле iostream.h.
Чтобы использовать операции обмена >> и << с данными производных типов, определяемых пользователем, необходимо расширить действие указанных операций, введя новые операции-функции. Каждая из операций обмена << и >> бинарная, причем левым операндом служит объект, связанный с потоком, а правый операнд должен иметь желаемый тип. Этот бинарный характер операций обмена отражает спецификация параметров соответствующих операций-функций. Первый параметр - ссылка на объект потокового класса (тип istream& либо ostream&), второй параметр - ссылка или объект желаемого типа. Тип возвращаемого значения должен быть ссылкой на тот поток, для которого предназначена операция. Таким образом, формат операции-функции для перегрузки операций таков:
ostream& operator << (ostream& out, новый_тип имя)
{ ... // Любые операторы для параметра нового_типа
out <<... // Вывод значений нового_типа
return out; // Возврат ссылки на объект класса оstream
}
Здесь новый_тип - тип, определенный пользователем. Таким образом, если определить структурный тип (класс):
struct point // Точка трехмерного евклидова пространства
{ float х; // Декартовы координаты точки
float у;
float х;
}
то для типа point можно определить правила вывода (по умолчанию на экран дисплея) значений, например, с помощью такой операции-функции:
оstrеаm& // Тип возвращаемого значения
operator << (ostream& t, point d)
{ return t << "\nx = " << d.x << " у = " << d.y << "
z = " << d.z;
}
Отметим, что в теле функции с "названием" operator <<, операция << используется в первом бинарном выражении с объектом t типа ostream& в качестве левого операнда. Правыми операндами для каждой из операций << служат значения стандартных (базовых) типов - символьные строки и вещественные числа типа float. Результат операции t << "\nx = " -ссылка на объект типа ostream. Эта ссылка используется в качестве левого операнда при выводе << d.x и т.д. Так как d - имя формального параметра структурного типа point, то для обращения к компонентам структуры (х, у, в) используются уточненные имена d.x, d.y, d.x. В результате выполнения всей цепочки операций вывода в теле операции-функции формируется ссылка на объект типа ostream, и именно это значение ссылки возвращает в точку вызова оператор return. Тем самым операция включения в поток <<, применяемая в вызывающей программе к операнду типа point, вернет ссылку на выходной поток. Таким образом, сохраняется возможность применения операции << несколько раз в одном выражении, как это разрешено для операндов базовых типов (т.е. допустимы цепочки вывода). Например, если, определив структуру point и введя для нее приведенную выше операцию-функцию operator << (), выполнить программу
void main ()
{ point F = ( 10.0, 20.0, 30.0 );
cout << "\nКоординаты точки: " << F;
}
то на экране дисплея получим:
Координаты точки:
х = 10.0 у = 20.0 х = 30.0
Как видите, в данном примере при выводе в поток cout равноправно используются и вывод значения базового типа char[], и вывод значений объекта F типа point, определенного пользователем.
Напомним, что класс ostream, поток (объект) cout и "стандартные" режимы выполнения операции вывода (для базовых типов) определены в заголовочном файле iostream.h, который необходимо поместить в начале текста программы до текста операции-функции operator << ().
В качестве еще одного примера перегрузки (расширения действия) операции вывода <<, рассмотрим следующую программу, в которой для представления почтового адреса используется структурный тип с названием address:
//Р11-ОЗ. СРР - перегрузка операции вывода "<<"
#include < iostream.h >
struct address // Почтовый адрес
{ char *country; // Страна
char *city; // Город
char *street; // Улица
int number_of_house; // Номер дома
// Определение операции-функции, "распространяющей"
// действие операции включения в поток << на операнд
// типа address:
ostream& // Тип возвращаемого значения
operator << (ostream& out, address ad)
{ out << "\nCountry: " << ad.country;
out << "\nCity: " << ad.city;
out << "\nStreet: " << ad.street;
out << "\nHouse: " << ad.number_of_house;
return out;
}
void main()
{ address ad = {"Russia", "Moscow", "Arbat", 11};
cout << "\n3начение структуры (почтовый адрес): ";
cout << ad << "\n";
}
Результаты выполнения программы:
Значение структуры (почтовый адрес):
Country: Russia
City: Moscow
Street: Arbat
House: 11
В теле операции-функции operator << () использовано несколько операторов с выражениями, выводящими значения в поток вывода.
В операторе return возвращается ссылка на него (имеющая тип ostream&).
Как показано на примерах, перегрузка операции вывода << позволяет не только организовать с ее помощью вывод значений пользовательских типов, но и обеспечивает программиста широкими возможностями оформления результатов. К сожалению, расширить действие операции можно только на пользовательские типы и поэтому невозможно непосредственно изменить формат вывода какого-либо из базовых типов. Например, не удастся ввести процедуру operator << (), с помощью которой при использовании операнда типа char * в стандартный поток будет выводиться и длина символьной строки, и ее содержимое.
Чтобы решить указанную задачу, необходимо определить структуру, компонентами которой будут связанный со строкой указатель char * и целая переменная со значением, равным длине строки. Вот для такой структуры перегрузка операции вывода << вполне допустима. В следующей программе это реализовано:
//Р11-04.СРР - вывод информации о структуре-строке
#include < iostream.h >
#include < string.h > // Для работы со строковыми функциями
// Определение класса (пользовательского типа):
struct string
{ int length;
char *line;
};
// Прототип операции-функции для перегрузки операции <<:
ostream& operator << (ostream& out, string str);
void main()
{ string st; // Объект st класса string
strcpy(st.line,"Содержимое строки." );
st.length = strlen(st.line);
cout << st;
}
ostream& // Тип возвращаемого значения
operator << (ostream& out, string str)
{ out << "\n Длина строки: " << str.length;
out << "\n Значение строки: " << str.line;
return out;
}
Результат выполнения программы:
Длина строки: 18
Значение строки: Содержимое строки.
Отметим, что в файле с тестом программы определение операции-функции помещено ниже, чем обращение к ней. Поэтому в вызывающую программу пришлось поместить прототип операции-функции.
Для перегрузки (расширения действия) операции ввода >> необходимо определить операцию-функцию вида:
istream& operator >> (istream& in, новый_тип& имя)
{ // Любые операторы для параметра нового типа
in >> ... // Ввод значений нового типа
return in; // Возврат ссылки на объект класса istream
}
Здесь новый_тип - тип, определенный пользователем, т.е. некоторый класс или его частный случай - структурный тип. Основное отличие от перегрузки операции вывода - необходимость в качестве второго параметра использовать ссылку. Для уже введенного выше структурного типа "точка трехмерного евклидова пространства" можно с помощью перегрузки операции ввода >> очень изящно определить последовательность ввода с консоли значений координат. Например, удобной может оказаться операция-функция, использованная в следующей программе:
//Р11-05.СРР - перегрузка операции ввода >>
#include < iostream.h >
struct point // Точка трехмерного евклидова пространства
{ float x;
float у;
float z;
};
istream& // Тип возвращаемого значения
operator >> (istream& in, point& d)
{ cout << "\n Введите три координаты точки: " << "\nх = ";
in >> d.x;
cout << "у = "; in >> d.y;
cout << "z = "; in >> d.x;
return in;
}
void main ()
{ point 0;
cin >> D;
}
При выполнении на экране может быть, например, такой текст:
Введите три координаты точки:
х = 100 < Enter >
у = 200 < Enter >
z = 300 < Enter >
В предложенной операции-функции operator >> () выполняется не только "чтение" (ввод) данных, набираемых пользователем на клавиатуре (в примере 100, 200, 300), но и выводятся на экран соответствующие подсказки, что упрощает использование программы. В данной программе операция-функция operator >> () работает не только с входным потоком, ссылка на который передается как параметр, но и со стандартным выходным потоком cout, для которого обычным образом используется операция вывода <<. Поэтому до определения операции-функции operator >> () в текст программы необходимо включить заголовочный файл iostream.h.
Следующая программа еще раз иллюстрирует сказанное. В программе определен структурный тип employee, предназначенный для структур, содержащих сведения о некоторых "служащих":
Можно было бы ввести и другие компоненты, например, название отдела, должность, стаж работы и т.д., но для целей нашего примера это лишнее. В теле операции-функции operator >> (), распространяющей действие операции ввода >> на структуру типа employee, на экран (поток cout) выводятся подсказки и из потока типа istream считываются набираемые на клавиатуре данные.
Текст программы:
//Р11-06.СРР - перегрузка операций ввода >> и вывода <<
#include < iostream.h >
// Определение класса "служащий":
struct employee // Определение класса "служащий"
{ char nаmе[50]; // Фамилия
long salery; // Оклад
int age; // Возраст
};
// Определение операции-функции, "распространяющей"
// действие операции ввода >> на операнд типа employee:
istream& // Тип возвращаемого значения
operator >> (istream& input, employee& em)
{ cout. << "\n\nВведите сведения о служащем: " << "\nфамилия: ";
input >> еm.name;
cout << "Оклад: ";
input >> em.salery;
cout << "Возраст: ";
input >> em.age;
return input;
}
//Прототип операции-функции для перегрузки операции <<:
ostream& operator << (ostream&, employee);
void main()
{ employee Z; // Определен объект класса employee
cin >> E;
cout << Z;
}
// Определение операции-функции для перегрузки
// операции <<:
ostream& operator << (оstrеаm& out, employee e)
{ out << "\nВведены следующие сведения о служащем: ";
out << "\nИмя: " << е. nаmе;
out << ", оклад: " << e.salery << " руб.";
out << ", возраст: " << е.аgе << " лет.\n";
return out;
}
Возможный результат выполнения программы:
Введите сведения о служащем:
Фамилия: Иванов < Enter >
Оклад: 140000 < Enter >
Возраст: 39 < Enter >
Введены следующие сведения о служащем:
Имя: Иванов, оклад: 140000 руб., возраст: 39 лет.
В программе для структуры типа employee перегружены как операция ввода >>, так и операция вывода <<.
Кроме операции включения (записи) в поток << и извлечения (чтения) из потока >>, в классах библиотеки ввода-вывода есть весьма полезные функции, обеспечивающие программиста альтернативными средствами для обмена с потоками.
При выводе в качестве основного класса, формирующего выходные потоки, используется класс ostream. В нем определены (ему принадлежат) две функции для двоичного вывода данных:
ostream& ostream::put(char cc);
ostream& ostream::write(const signed char *array, int n);
ostream& ostream::write(const unsigned char *array, int n);
Функция put () помещает в тот выходной поток, для которого она вызвана, символ, использованный в качестве фактического параметра.
В этом случае эквивалентны операторы:
cout << ' Z ';
и
cout.put('Z');
Функция writ* () имеет два параметра - указатель array на участок памяти, из которого выполняется вывод, и целое значение n, определяющее количество выводимых из этого участка символов (байт).
В отличие от операции << включения в поток функции put () и write () не обеспечивают форматирования выводимых данных. Например, если при выводе одного символа с помощью операции << можно, используя функцию width (), разместить его в поле из нужного количества позиций, то функция put() всегда разместит символ в одной позиции выходного потока. Флаги форматирования также не применимы к функциям put () и write ().
Так как функции put () и wrif() возвращают ссылки на объект того класса, для которого они выполняются, то можно организовать цепочку вызовов:
charss [] << "Merci";
cout.put('\n').write(ss,sizeof(ss)-1).put('!').put('\n');
На экране (в потоке cout) появится:
Merci!
Если необходимо прочитать из входного потока строку символов, содержащую пробелы, то с помощью операции извлечения >> это делать неудобно - каждое чтение строки выполняется до пробела, а ведущие (левые) пробельные символы игнорируются. Если мы хотим, набрав на клавиатуре строку: "Qui vivra verra - будущее покажет (лат.) ", ввести ее в символьный массив, то с помощью операции из влечения >> это сделать несколько хлопотно, все слова будут читаться отдельно (до пробела). Гораздо удобнее воспользоваться функциями бесформатного (двоичного) чтения.
Функции двоичного (бесформатного) чтения данных принадлежат потоку istream. Прежде чем перечислить их, отметим основное свойство двоичного чтения данных. Данные читаются без преобразования их из двоичного представления в текстовое. Например, если во входном потоке размещено представление вещественного числа l. ЗЕ-З, то это будет воспринято как последовательность из шести байт, и читать эту последовательность с помощью функций двоичного ввода можно только в символьный массив.
Итак, функции чтения. Во-первых, это 6 перегруженных функций get (). Две из них имеют следующие прототипы:
istream& get(signed char *array, int max_len, char='\n');
istream& get(unsigned char *array, int max_len, char='\n');
Каждая из этих функций выполняет извлечение (чтение) последовательности байтов из стандартного входного потока и перенос их в символьный массив, задаваемый первым параметром. Второй параметр определяет максимально допустимое количество прочитанных байтов. Третий параметр определяет ограничивающий символ (байт), при появлении которого во входном потоке следует завершить чтение. По умолчанию третий параметр имеет значение '\n' - переход на следующую строку, однако при обращении к функции его можно задавать и по-другому. Значение этого третьего параметра из входного потока не удаляется, он в формируемую строку (символьный массив) не переносится, а вместо него автоматически добавляется "концевой" символ строки '\0'. Если из входного потока извлечены ровно mах_ len - 1 символов, однако ограничивающий символ (например, по умолчанию ' \n ') не встретился, то концевой символ помещается после введенных символов.
Массив, в который выполняется чтение, должен иметь длину не менее аах_1еп символов. Если из входного потока не извлечено ни одного символа, то устанавливается код ошибки. Если до появления ограничивающего символа и до извлечения max_len - l символов встретился конец файла EОР, то чтение прекращается как при появлении ограничивающего символа.
Функция с прототипом
istream& get(streambuf& buf, char = '\n');
извлекает из входного потока символы и помещает их в буфер, определенный первым параметром. Чтение продолжается до появления ограничивающего символа, которым по умолчанию является ' \n ', но он может быть установлен явно любым образом.
Три следующие варианта функции get() позволяют прочесть из входного потока один символ. Функции
istream& get(unsigned char& cc);
istream& get(signed char& cc);
присваивают извлеченный символ фактическому параметру и возвращают ссылку на поток, из которого выполнено чтение. Функция
int get();
получает код извлеченного из потока символа в качестве возвращаемого значения. Если поток пуст, то возвращается код конца файла EOF. Функции "ввода строк":
istream& getline(signed char *array, int len, char='\n');
istream& getline(unsigned char *array, int len, char='\n');
подобны функциям get () с теми же сигнатурами, но переносят из входного потока и символ-ограничитель. Функция
int peek ();
позволяет "взглянуть" на очередной символ входного потока. Точнее, она возвращает код следующего символа потока (или EOF, если поток пуст), но оставляет этот символ во входном потоке. При необходимости этот символ можно в дальнейшем извлечь из потока с помощью других средств библиотеки. Например, следующий цикл работает до конца строки (до сигнала от клавиши Enter):
char cim;
while(cin.peek() != '\n')
{ cin.get(cim); cout.put(cim); }
Принадлежащая классу istream функция
istream& putback(char cc);
не извлекает ничего из потока, а помещает в него символ cc, который становится текущим и будет следующим извлекаемым из потока символом. Аналогичным образом функция
int gcount ();
подсчитывает количество символов, которые были извлечены из входного потока при последнем обращении к нему.
Функция
istream& ignore(int n = 1, int = EOF);
позволяет извлечь из потока и "опустить" то количество символов n, которое определяется первым параметром. Второй параметр определяет символ-ограничитель, при появлении которого выполнение функции нужно прекратить, даже если из потока еще не извлечены все n символов.
Функции
istream& read(signed char *array, int numb);
istream& read(unsigned char * array, int numb);
выполняют чтение заданного количества numb символов в массив array.
Полезны следующие функции того же класса istream:
istream& seekg(long pos);
устанавливает позицию чтения из потока в положение, определяемое значением параметра.
istream& seekg(long pos, seek_dir dir);
выполняет перемещение позиции чтения вдоль потока в направлении, определенном параметром dir, принимающим значения из перечисления enum seek_dir (beg, cur, end). Относительная величина перемещения (в байтах) определяется значением параметра long pos. Если направление определено как beg, то смещение от начала потока; cur - от текущей позиции; end - о нца потока;
long tellg ()
определяет текущую позицию чтения из потока.
Подобные перечисленным функции класса ostream:
long tellp ()
определяет текущую позицию записи в поток;
ostream& seekp(long pos, seek_dir dir);
аналогична функции seekg (), но принадлежит классу ostream и выполняет относительное перемещение позиции записи в лоток;
ostream& seekp(long pos);
устанавливает абсолютную позицию записи в поток.
Классы istrstream, osfcrstream, strstream, определяемые в заголовочном файле strstream.h (он в компиляторах под MS-DOS имеет более короткое название strstrea.h, так как длина имени файла в MS-DOS не может превышать 8 символов), предназначены для создания потоков, связанных с участками (областями) основной памяти.
Достаточно часто такие участки памяти определяются в программе как символьные массивы. Именно поэтому в обозначениях указанных классов используется аббревиатура (приставка) "str" - сокращение английского слова string (строка), а объекты этих классов называют строковыми потоками.
Строковый поток определяется и одновременно связывается с областью памяти с помощью конструктора объектов соответствующего класса. Формат вызова конструктора:
имя_класса имя_потока (параметры конструктора);
Имя_класса в данном случае - это одно из имен istrstream, ostrstream, strstream. Имя_потока - это идентификатор (произвольно выбираемое программистом имя объекта данного класса). Типы параметров и их число различны для конструкторов разных классов.
Входные строковые потоки. Эти потоки создаются с помощью такого конструктора класса istrstream:
istrstream имя_потока(char *str);
Обязательным параметром конструктора объектов класса istrstream является указатель str на уже существующий участок основной памяти.
Например, следующие операторы
char buf[40];
istrstream inBuf(buf);
определят входной строковый поток с именем inBuf и свяжут его с участком памяти, предварительно выделенным для символьного массива buf[]. Теперь этот строковый поток inBuf может использоваться как левый операнд операции извлечения >>.
В следующей программе определена строка, содержащая символьную информацию "123.5 Salve!", затем определен и связан с этой строкой входной строковый поток instr. Из потока instr и тем самым из строки, адресуемой указателем stroka, разделенные пробелами значения переносятся в переменную real типа double и символьный массив array [10]. Текст программы:
//Р11-07.СРР - строковые потоки, операция извлечения >>
// Автоматически включается файл iostream.h:
#include < strstrea.h >
void main()
{ // Выделена область памяти (строка):
char *stroka = "123.5 Salve!";
// Создан входной строковый поток instr:
istrstream instr(stroka);
char array[10];
double real;
// Извлекаем информацию из строкового потока:
instr >> real >> array;
// Вывод на экран:
cout << "\narray = " << array << " real = " <<
real << end1;
}
Результат выполнения программы (на экране):
array = Salve! real = 123.5
В следующем примере с помощью входного строкового потока выполняется чтение информации, передаваемой в качестве параметра командной строки функции main:
//Р11-08.СРР - входной строковый поток; чтение аргумента
// функции main()
// Автоматически включается файл iostream.h:
#include < strstrеa.h >
void main(int argc, char *argv[] ) // Определены аргументы
{ char name[40]; // Выделяется область памяти
// Создает строковый поток input:
istrstream input(argv[1] );
// Извлекаем информацию из строкового потока:
input >> name;
// Вывод в стандартный поток (на экран):
cout << "\nПри вызове аргумент = " << name << end1;
}
Если программа будет запущена на выполнение директивой:
C:\>P11-08.EXE FileName < Enter >
то на экране появится сообщение:
При вызове аргумент = FileName
Извлечение информации из строкового потока с помощью операции >> выполняется, начиная от первого не пробельного символа до ближайшего пробела (точнее до ближайшего обобщенного пробельного символа). Если необходимо читать и пробельные символы, то можно воспользоваться функциями бесформатного обмена get() и getline(). Эти функции, вызываемые для символьных потоков, позволяют организовать, например, копирование строк. Для копирования строк в библиотеке языка Си существует специальная функция strcpy(), определенная в заголовочном файле string, h. Копирование несложно организовать и с помощью циклического переноса символов из одного массива в другой. Однако интересный вариант копирования получается при использовании строковых потоков. Например, в следующей программе выполняется "копирование" содержимого строки, адресуемой указателем line, в заранее созданный символьный массив array []. В соответствии с форматом функции getline () ее первый параметр - массив, в который ведется чтение, второй параметр - предельное количество читаемых символов, третий параметр - ограничивающий символ, после извлечения, которого обмен прекращается. Функция getline() вызывается для чтения из потока inpotok, связанного со строкой line. Текст программы:
//Р11-09.СРР - копирование строки функцией getline().
#include < strstrea.h >
void main()
{char *line = "000 111 \t222\n333\t444 555";
istrstream inpotok(line);
char array[80];
inpotok.getline(array,sizeof(array),'\0');
cout << "\narray = " << array << end1;
}
Результат выполнения программы:
array = 000 111 222
333 444 555
В результатах выполнения обратите внимание на влияние символов '\t, '\n', присутствующих как в исходной строке line, так и перенесенных в символьный массив array. Они, естественно, не выводятся на экран, а обеспечивают табуляцию и смену строки.
Безымянные входные строковые потоки. Конструктор класса istrstream позволяет создавать и связывать с заданной областью памяти безымянные входные строковые потоки. Например, чтение информации, переданной при запуске функции main () в командной строке, обеспечит следующая программа:
//Р11-10.СРР - безымянный входной строковый поток; чтение
// данных с помощью операции извлечения >>
#include < strstrea.h >
void main(int Narg, char *arg[])
{char path[80];
// Чтение из безымянного потока:
istrstream(arg[0] ) >> path;
cout << "\nПолное имя программы:\n" << path << end1;
}
Результат выполнения программы, например, такой:
Полное имя программы: D:\WWP\TESTPROG\P11-10.EXE
По соглашениям языка и операционной системы arg[0] всегда содержит полное наименование программы с указанием диска и полного пути к каталогу, где находится ЕХЕ-файл. Вызов конструктора istrstream(arg[0]) создает безымянный объект - входной строковый поток, связанный с символьным массивом, адрес которого передается как значение указателя arg[0]. К этому потоку применима операция извлечения >>, с помощью которой полное наименование программы переносится в виде строки из arg [О] в символьный массив path [].
В следующей программе с одним и тем же участком памяти (строка, адресуемая указателем line) последовательно связываются три безымянных входных строковых потока.
//P11-11.CPP - чтение из безымянных строковых потоков
#include < strstrea.h >
void main()
{ char *line = "000 111 \t222\n333\t444 555";
char array[80]; // Вспомогательный массив
// Чтение из безымянного потока до пробела:
istrstream(line) >> array;
cout << "\narray = " << array << end1;
//Повторное чтение из безымянного потока:
istrstream(line) >> array;
cout << "\narray = " << array << end1;
// Вызов функции getline() для безымянного потока:
istrstream(line).getline(array,sizeof(array),'\0');
cout << "array = " << array << end1;
}
Результат выполнения программы:
array = 000
array = 000
array = 000 111 222
333 444 555
Из первого безымянного потока данные в массив array извлекаются (до первого пробела) операцией >>. Второй безымянный поток связан с той же строкой. Из него в массив array снова с начала строки читается та же самая информация. Третий безымянный поток читается с помощью функции getline (), которая полностью копирует строку line в символьный массив array []. Извлечение данных функцией getline () продолжается до появления нулевого признака конца строки '\0'. Этот символ также переносится в массив array и служит признаком конца созданной строки.
Выходные строковые потоки. Эти потоки обычно создаются с помощью такого конструктора класса ostrstream:
ostrstream имя_ потока(сhаг *str, int len, int mode);
Необязательное имя_потока - это идентификатор, произвольно выбираемый программистом. Указатель str должен адресовать уже существующий участок памяти. Параметр int len определяет размеры этого участка (буфера). Последний параметр - индикатор режима обмена mode. Режим обмена строкового потока при выводе определяет размещение информации в связанной с потоком строке. Для задания конкретного режима используется флаг или дизъюнкция нескольких флагов:
ios :: out
строковый поток создается для вывода, запись информации ведется с начала строки;
ios :: ate
позиция записи устанавливается в месте размещения нулевого признака конца строки ' \0 ' (запись в продолжение);
ios :: арр
для строковых типов этот флаг действует аналогично флагу ios :: ate, но в конкретных реализациях могут быть отличия, обычно отражаемые в документации.
Вместе с флагом ios:: out могут быть указаны (в дизъюнктивной форме) флаги ios:: ate или ios:: арр. В обоих случаях при формировании потока позиция записи устанавливается на нулевой признак '\0' конца строки (буфера потока), т.е. предполагается запись в конец потока.
По умолчанию выбирается ios:: out, т.е. строковый поток создается для вывода (записи) информации с начала строки (буфера).
Начнем рассмотрение основных возможностей выходных строковых потоков с их безымянного варианта. В следующей программе значение строки обязательного аргумента функции main с помощью операции вставки << переносится в безымянный строковый поток, связанный с символьным массивом path []:
//Р11-12.СРР - запись в безымянный выходной строковый
// поток(<<)
#include < strstrea.h >
void main(int Narg, char *arg[])
{char path[80];
ostrstream(path,sizeof(path)) << arg[0] << '\0';
cout << "\nПолное имя программы: " << path << end1;
}
Результат выполнения программы:
Полное имя программы: D:\WWP\TESTPROG\P11-I2.EXE
Так как операция включения << не переносит в выходной поток признак конца строки ' \0 ', то его пришлось явным образом поместить в выходной поток (тем самым в буфер path []).
Функция write () применительно к выходному потоку позволяет записывать в него данные без форматирования, т.е. строка записывается вместе с пробельными символами и символом конца строки ' \0 '. Чтобы продемонстрировать особенности ее применения к безымянным выходным строковым потокам, рассмотрим следующую программу:
//Р11-13.СРР - запись в безымянный выходной строковый
// поток; копирование строки с использованием
// функции write()
#include < strstrea.h >
void main()
{ char lat[] = "Quod erat demonstrandum! ";
char rus [] = " - Что и требовалось доказать! ";
char result[60];
ostrstream(result,sizeof(result)).write(lat,sizeof(lat));
ostrstream(result,
sizeof(result),ios : rate).write(rus,sizeof(rus));
cout << "\n" << result << end1;
}
Результат выполнения программы:
Quod erat demonstrandum ! - Что и требовалось доказать!
В программе два безымянных потока, каждый из которого "настроен" на символьный массив result []. При создании первого безымянного потока в качестве параметров конструктора указываются - массив result[] и его размеры, т.е. длина массива в байтах. Функция write () "присоединяется" с помощью операции "точка" непосредственно к конструктору и тем самым вызывается для созданного им безымянного потока. В качестве фактических параметров функции write() используются указатель lat на строку и количество записываемых символов. Так как длина строки меньше длины буфера result, то буфер не заполняется целиком. При создании второго безымянного потока, кроме буфера result и его длины, в конструкторе (в качестве третьего параметра) использован флаг ios:: ate, под действием которого поток создается как "дополняемый". Тем самым последующая запись в поток выполняется не с начала потока, а начиная с позиции окончания предыдущей записи ' \0 '. Именно туда функция write () помещает строку, адресованную указателем rus. Тем самым в массиве result [] осуществляется конкатенация строк, что видно из результатов. Из массива result [] вывод в поток cout выполняется до появления символа конца строки ' \0 ' в массиве result.
Следующая программа иллюстрирует особенности последовательного вывода в строковый поток данных разных типов с помощью операции включения:
//Р11-14.СРР - вывод в строковый поток операцией <<
#include < strstrea.h >
void main ()
{ char buffer[180];
ostrstream outstring(buffer,sizeof(buffer),
ios : : out| ios : : ate);
outstring << "\nБез явного включения разделителей"
<< " текст в потоке\n\"сливается\":\n ";
outstring << 123456789 << -456 << +1.23456789;
outstring << -0.123456789е+1 << +123.456789е-3 << ends;
cout << "\n" << buffer << end1;
}
Результат выполнения программы:
Без явного включения разделителей текст в потоке "сливается":
123456789-456 1.23456789-1.2345678 0.123457
Как показывают результаты, последовательное обращение к строковому потоку приводит к записи "в продолжение", т.е. указатель позиции записи при создании потока устанавливается на его начало, а затем перемещается на длину каждой новой записи. Никаких промежутков или разделителей между выводимыми данными не добавляется. Более того, операция включения в поток << даже не переносит в него нулевой ограничитель конца строки '\0'. Поэтому его нужно добавлять явно, если в дальнейшем требуется использовать буфер потока в качестве строки. Числовая информация, включаемая в поток операцией <<, форматируется. При этом знак + для чисел заменяется пробелом, вещественные числа, заданные в экспоненциальной форме (в научной нотации), переводятся в форму с фиксированной точкой. Выполняется округление дробной части вещественного числа. Перечисленные и проиллюстрированные результатами особенности форматирования могут быть изменены с помощью средств управления форматом, как и для стандартных потоков.
Двунаправленные строковые потоки. Основной конструктор строковых потоков, создаваемых как для чтения, так и для записи, имеет следующий формат:
sir stream имя_потока(сhаr *buf, int lenBuf, int mode);
где
В следующей программе с символьным массивом buffer [] связывается двунаправленный поток string. Затем последовательно выполняются операции записи в поток и чтения из потока.
//Р11-15.СРР - ввод и вывод для двунаправленного
// строкового потока
#include < strstrea.h >
void main()
{ char buffer[180];
char stroka[150], as[150];
// Строковый поток string связан с массивом buffer:
strstream string(buffer,sizeof(buffer),ios::in| ios::out);
sfcring << "В строковый поток записывается "
"это предложение." << ends;
// Чтение из строкового потока string в массив stroka:
string.getline(stroka,sizeof(stroka),'\0');
// Вывод в стандартный поток содержимого массива
// stroka:
cout << "\nstroka = "<< stroka;
// Возвращение позиции чтения/записи к началу потока
// string:
string.seekg(OL,ios : : beg);
// Чтение из строкового потока до пробельного символа:
string >> ss;
cout << \nss = " << ss;
string >> ss;
cout << "\nss = " << ss;
string.gefcline(ss,sizeof(ss),'\0');
cout << "\nss = " << ss;
}
Результат выполнения программы (на экране):
stroka = В строковый поток записывается это предложение.
ss = В
ss = строковый
ss = поток записывается это предложение.
Обратите внимание, что функция getline() переносит из потока string даже ведущие пробельные символы. Комментарии в тексте программы и результаты ее выполнения достаточно подробно иллюстрируют основные особенности работы с двунаправленными потоками. Отметим только необходимость при операциях << явно занести в поток признак конца строки (манипулятор ends), а при повторном чтении из потока - необходимость перейти к его началу.
Переход к началу потока для чтения из него выполняет функция seekg(), первый параметр которой (типа long) указывает нулевую величину смещения, а второй - положение, от которого это смещение отсчитывается. В классе ios определены три возможных начала отсчета:
Обратите внимание, что для двунаправленного потока класса strstream определены два указателя позиций - позиции записи и позиции чтения. Именно поэтому в программе после окончания записи в поток string функция getline() выполняет чтение от его начала, и не требуется перевода указателя чтения (что делает функция seekp () ).
Перегрузка операций ввода-вывода и использование строковых потоков для межмодульного обмена. Строковые потоки можно использовать по-разному. С их помощью в участок памяти, связанный со строковым потоком, можно заносить разнотипную информацию, затем извлекать ее по нужным правилам. Строковый поток можно в этом случае сделать внешним и с его помощью осуществлять межмодульный обмен. В следующей программе с помощью дуального (двунаправленного) строкового потока выполняется обмен между функциями. Строковый поток с именем obmen и связанный с ним символьный массив Link[] определены как глобальные и тем самым доступны во всех функциях файла с программой. В основной программе в строковый поток obmen заносятся данные из массива структур. Предварительно в поток записывается значение количества элементов массива. В функции result () выполняется чтение из строкового потока. При первом обращении к потоку "извлекается" значение количества элементов, используемое затем для организации цикла чтения структур. Структурный тип с именем element и две операции-функции для перегрузки операций обмена >> и << со строковыми потоками определены до функции main (). Текст программы:
//Р11-16.СРР - перегрузка операций обмена (<<, >>) и
// двунаправленный строковый поток ввода-вывода
#include < strstrea.h >
const int lenLink = 200; // Глобальная константа
char Link [lenLink]; // Глобальный символьный массив
// Строковый поток оbmen связан с массивом Link:
strstream obmen (Link, sizeof (Link) , ios: : in ( ios: : out);
struct element
{ int nk, n1;
float zn;
};
strstream& operator >> (strstream& in, element& el)
{ in >> el.nk; in >> el.nl; in >> el.zn;
return in;
};
strstream&. operator <<(strstream& out, element& el)
{ out << ' ' << el.nk << ' ' << el.n1 << ' ' << el.zn ;
return out ;
};
// Функция чтения из потока и вывода на экран:
void result(void)
{ element zuar;
int numb;
obman >> numb;
cout << "\nnuab = " << numb;
for (int j = 0; j < numb; j++)
{ obmen >> zuar;
cout << "\nelement [ " << j << " ] = ";
cout << zuar.nk << '\t' << zuar.n1 << '\t' << zuar.zn;
}
}
void main ()
{ char buffer[180];
const int numbeEl = 5;
element are1[numbeE1] = { 1, 2, 3.45, 2, 3, 4.56,
22, 11, 45.6, 3, 24, 4.33, 3, 6, -5.3 };
// Запись в строковый поток:
obmen << numbeE1;
for (int i = 0; i < numbeEl; i++) obmen << arel[i];
obmen << '\0';
result();
}
Результат выполнения программы:
numb = 5
element [0] =1 2 3.45
element [l] = 2 3 4.56
element[2] = 22 11 45.599998
element[3] = 3 24 4.33
element [4] =3 6 -5.3
Основное отличие внешней памяти ЭВМ от основной (иначе оперативной) памяти - возможность сохранения информации при отключении ЭВМ. Информация во внешней памяти (на диске, на магнитных лентах и т.п.) сохраняется в виде файлов - именованных объектов, доступ к которым поддерживает (обеспечивает) операционная система ЭВМ. Поддержка операционной системы состоит в том, что в ней имеются средства:
Библиотека ввода-вывода Си++ включает средства для работы с последовательными файлами. Логически последовательный файл можно представить как именованную цепочку (ленту, строку...) байтов, имеющую начало и конец. Последовательный файл отличается от файлов с другой организацией тем простым свойством, что чтение (или запись) из файла (в файл) ведутся байт за байтом от начала к концу. В каждый момент позиции в файле, откуда выполняется чтение и куда производится запись, определяются значениями указателей позиций записи и чтения файла. Позиционирование указателей записи и чтения (т.е. установка на нужные байты) выполняется либо автоматически, либо за счет явного управления их положением. В стандартной библиотеке ввода-вывода Си++ имеются соответствующие средства.
Рассматривая взаимосвязь файлов с потоками ввода-вывода, нужно отметить существование следующих процедур:
1 - создание файла;
2 - создание потока;
3 - открытие файла;
4 - "присоединение" файла к потоку;
5 - обмены с файлом с помощью потока;
6 - "отсоединение" потока от файла;
7 - закрытие файла;
8 - уничтожение файла.
Все перечисленные действия могут быть выполнены с помощью средств библиотеки классов ввода-вывода языка Си++. Однако существует несколько альтернативных вариантов их выполнения. Кратко остановимся на наиболее простых и удобных механизмах реализации указанных действий.
Создание файла может быть на "нижнем уровне" выполнено с помощью библиотечной функции (из библиотеки ANSI С):
int creat (const char *path, int amode);
Прототип этой функции находится в заголовочном файле io.h. Функция creat() по заданному имени файла path создает новый файл либо "очищает" и подготавливает для работы уже существующий. Параметр amode нужен только для вновь создаваемого файла. Файл создается для работы в таком режиме обмена, который соответствует значению определенной в файлах fcntl.h и atdlib.h глобальной переменной _fmode (O_TEXT или O_BINARY). Значение О_ТЕХТ определяет текстовый режим обмена с файлом, при котором в процессе чтения из файла каждая пара символов CR (OxOD - конец строки), LF (ОXОА - переход к началу строки) преобразуется в один символ новой строки ' \n '. При записи в файл в текстовом режиме обмена каждый символ новой строки '\n' преобразуется в "пару CR, LF. Чтение из файла в текстовом режиме не может продолжаться, если обнаружен символ, обозначающий конец файла. В этом случае считается, что достигнут конец файла, т.е. выполнено условие EOF.
В двоичном режиме обмена O_BINARY преобразований символов не происходит, и их значения не анализируются.
По умолчанию переменная _fmode устанавливается равной О_ТЕХТ, т.е. файл создается для работы в текстовом режиме. Программист может явно изменить режим обмена, используя последовательность:
#include < fcnt1.h >
_fmode=0_BIMARY;
Параметр amode определяет режим доступа к создаваемому файлу. Предопределены в заголовочном файле sys\stat.h следующие константы-значения этого параметра:
S__IWRITE - разрешена запись в файл;
S_IREAD> - разрешено только чтение из файла;
S_IREAD | S_IWRITE - разрешены и чтение, и запись.
Если файл с полным именем *path не существует, то он создается заново, для него устанавливаются по значению параметра amode режим доступа и по значению глобальной переменной _fmode выбирается текстовый или двоичный режим обмена.
Если файл с полным именем *path уже существует и для него был определен режим доступа "только чтение" S_IBEAD, то вызов функции creat() завершается неудачно, файл с указанным именем останется неизмененным.
Если файл с полным именем *path уже существует и для него установлен режим записи S_IWRITE, то функция creat() обнуляет длину файла, оставляя неизмененными атрибуты его режимов.
При успешном завершении функция creat() возвращает неотрицательное целое число - индивидуальный логический номер (дескриптор) файла. В противном случае возвращается значение -1. Пример создания файла с проверкой результата:
#include < io.h > // Для функций create()
#include < process.h > // Для функций exit()
#include < iostream.h >
#include < sys\stat.h > // Значения параметров amode
...
char *fileName = "EXAMPLE.CPP";
int fileNumb;
fileNumb = creat(fileName,S_ IWRITE);
if (fileNumb == -1)
{ cerr << "Ошибка при создании файла";
exit(1);
}
...
Здесь файл с именем EXAMPLE. CPP создается в текущем каталоге. Явно указан режим файла, предусматривающий только запись в файл. По умолчанию файл создается как текстовый О_ТЕХТ. Потоки для работы с файлами создаются как объекты следующих классов:
Чтобы использовать эти классы, в текст программы необходимо включить дополнительный заголовочный файл f stream, h. После этого в программе можно определять конкретные файловые потоки, соответствующих типов (объекты классов of stream, if stream, fstream), например, таким образом:
of stream outFile; // Определятся выходной файловый поток
ifstream inFile; // Определяется входной файловый поток
fstream ioFile; // Определяется файловый поток для ввода
// и вывода
Создание файлового потока (объекта соответствующего класса) связывает имя потока с выделяемым для него буфером и инициализирует переменные состояния потока. Так как перечисленные классы файловых потоков наследуют свойства класса ios, то и переменные состояния каждого файлового потока наследуются из этого базового класса. Так как файловые классы являются производными от классов oatream (класс ofstreaa), istream (класс ifstream), stream (класс fstream), то они поддерживают описанный в предыдущих параграфах форматированный и бесформатный обмен с файлами. Однако прежде чем выполнить обмен, необходимо открыть соответствующий файл и связать его с файловым потоком.
Открытие файла в самом общем смысле означает процедуру, информирующую систему о тех действиях, которые предполагается выполнять с файлом. Существуют функции стандартной библиотеки языка Си для открытия файлов f open (), open (). Но, работая с файловыми потоками библиотеки ввода-вывода языка Си++, удобнее пользоваться компонентными функциями соответствующих классов.
Создав файловый поток, можно "присоединить" его к конкретному файлу с помощью компонентной функции ореn(). Функция open () унаследована каждым из файловых классов of stream, ifsream, f stream от класса fstreambase (для простоты он не показан на рис. 11.3). С ее помощью можно не только открыть файл, но и связать его с уже определенным потоком. Формат функции:
void open(const char *fileName,
int mode = умалчиваемое_ значение,
int protection = умалчиваемое значение);
Первый параметр - fileName - имя уже существующего или создаваемого заново файла. Это строка, определяющая полное или сокращенное имя файла в формате, регламентированном операционной системой. Второй параметр - mode (режим) - дизъюнкция флагов, определяющих режим работы с открываемым файлом (например, только запись или только чтение). Флаги определены следующим образом:
enum ios::open_ mode {
in = 0х01, // Открыть только для чтения
out = 0х02, // Открыть только для записи
ate = 0х04, // При открытии искать конец файла
арр = 0х08, // Дописывать данные в конец файла
trunc = 0х10, // Вместо существующего создать
// новый файл
nocreate = 0х20, // Не открывать новый файл (Для
// несуществующего файла функция
// open выдаст ошибку)
noreplace = 0х40, // Не открывать существующий файл
// (Для существующего выходного
// файла, не имеющего режимов ate
// или арр, выдать ошибку)
binary = 0х80 // Открыть для двоичного (не
// текстового) обмена
};
Назначения флагов поясняют комментарии, однако надеяться, что именно такое действие на поток будет оказывать тот или иной флаг в конкретной реализации библиотеки ввода-вывода, нельзя. Как пишет автор языка Си++ [26], "смысл значений open_mode скорее всего зависит от реализации". Например, различие между флагами ios:: ate и ios:: арр проявляется весьма редко, и часто они действуют одинаково. Однако ниже в пояснениях к программе Р11-19.СРР приведен пример использования флага ios:: арр в конструкторе класса of stream, где использование ios:: ate приведет к ошибке открытия файла. Умалчиваемое значение параметра mode зависит от типа потока, для которого вызывается функция open ().
Третий параметр - protection (защита) - определяет защиту и достаточно редко используется. Точнее, он устанавливается по умолчанию и умалчиваемое значение обычно устраивает программиста.
Как обычно вызов функции ореn () осуществляется с помощью уточненного имени
имя_объекта_класса. вызов__принадлежащей_классу_функции
Итак, открытие и присоединение файла к конкретному файловому потоку обеспечивается таким вызовом функции open ():
имя_потока.open(ммя_файла, режим, защита);
Здесь имя_потока - имя одного из объектов, принадлежащих классам ofstream, ifstream, fstream. Примеры вызовов для определенных выше потоков:
outFile.open("С:\\USER\\RESULT.DAT");
inFile.open("DATA.TXT");
ioFile.open("CHANGE.DAT",ios::out);
При открытии файлов, с потоками класса ofstream второй параметр по умолчанию устанавливается равным ios:: out, т.е. файл открывается только для вывода. Таким образом, файл C:\USER\RESULT.DAT после удачного выполнения функции ореn() будет при необходимости (если он не существовал ранее) создан, а затем открыт для вывода (записи) данных в текстовом режиме обмена и присоединен к потоку outFile. Теперь к потоку outFile может применяться, например, операция включения <<, как к стандартным выходным потокам cout, cerr.
Поток inFile класса ifstream в нашем примере присоединяется функцией open () к файлу с именем DATA.TXT. Этот файл открывается для чтения из него данных в текстовом режиме. Если файла с именем DATA.TXT не существует, то попытка вызвать функцию inFile.open() приведет к ошибке.
Для проверки удачности завершения функции ореn() используется перегруженная операция!. Если унарная операция! применяется к потоку, то результат ненулевой при наличии ошибок. Если ошибок не было, то выражение!имя_потока имеет нулевое значение. Таким образом, можно проверить результат выполнения функции open ():
...
if (inFile)
{ cerr << "Ошибка при открытии файла!\n";
exit(1);
}
Для потоков класса f stream второй аргумент функции open () должен быть задан явно, так как по умолчанию неясно, в каком направлении предполагается выполнять обмен с потоком. В примере файл CHANGE . DAT открывается для записи и связывается с потоком ioFile, который будет выходным потоком до тех пор, пока с помощью повторного открытия файла явно не изменится направление обмена с файлом или потоком. (Чтобы изменить режимы доступа к файлу, его нужно предварительно закрыть с помощью функции close (), унаследованной всеми тремя файловыми классами из базового класса fstreambase.)
В классе fsfcreambase, который служит основой для файловых классов, имеются и другие средства для открытия уже существующих файлов.
Если файл явно создан с помощью библиотечной функции "нижнего уровня" creat(), то для него определен дескриптор файла. Этот дескриптор можно использовать в качестве фактического параметра функции fstreambase:: attach (). При вызове этой функции используется уточненное имя, содержащее название того потока, который предполагается присоединить к уже созданному файлу с известным дескриптором:
#include < fstream.h > // Классы файловых потоков
#include < sys\stat.h > // Константы режимов доступа к файлам
...
char name[20]; // Вспомогательный массив
cin >> name; // Ввести имя создаваемого файла
int descrip = create(name,S_WRITE); // Создать файл
if (descrip == -1)
{ cout << "\n Ошибка при создании файла" );
exit ();
}
// Определение выходного файлового потока:
of stream fileOut;
// Присоединение потока к файлу:
fileOut.attach(descrip);
if (!fileOut)
{ cerr << "\n0шибка присоединения файла! ")
exit(1);
}
...
В классах if stream, of stream, f stream определены конструкторы, позволяющие, по-иному выполнять создание и открытие файлов. Типы конструкторов для потоков разных классов очень похожи:
имя_ класса();
создает поток, не присоединяя его ни к какому файлу;
имя_класса(int fd);
создает поток и присоединяет его к уже открытому файлу, дескриптор которого используется в качестве параметра fd;
имя класса(int fd, char *buf, int);
создает поток, присоединяя его к уже открытому файлу с дескриптором fd , и использует явно заданный буфер (параметр buf);
имя_класса(char *FileNane, int mode, int = ...);
создает поток, присоединяет его к файлу с заданным именем Filename, а при необходимости предварительно создает файл с таким именем.
Детали и особенности перечисленных конструкторов лучше изучать по документации конкретной библиотеки ввода-вывода.
Работая со средствами библиотечных классов ввода-вывода, чаще всего употребляют конструктор без параметров и конструктор, в котором явно задано имя файла. Примеры обращений к конструкторам без параметров:
if stream fi; // Создает входной файловый поток fi
оstream fo; // Создает выходной файловый поток fo
fstream ff; // Создает файловый поток ввода-вывода ff
После выполнения каждого из этих конструкторов файловый поток можно присоединить к конкретному файлу, используя уже упомянутую компонентную функцию open ():
void open(char *FileName, int режим, int защита);
Примеры:
fi.open("File1.txt",ios::in); // Поток fi соединен
// с файлом File1.txt
fi.close(); // Разорвана связь потока fi с файлом
// File1.txt
fi. open ("File2.txt"); // Поток fi присоединен к файлу
// File2.txt
fo.open("NewFile" ); // Поток fo присоединяется к файлу
// NewFile; если такой файл
// отсутствует - он будет создан
При обращении к конструктору с явным указанием в параметре имени файла остальные параметры можно не указывать, они выбираются по умолчанию.
Примеры:
if stream flowl("File.1");
создает входной файловый поток с именем flowl для чтения данных. Разыскивается файл с названием File.l. Если такой файл не существует, то конструктор завершает работу аварийно. Проверка:
if (!flowl) cerr << "Не открыт файл File.1!";
of stream flow2("File.2");
создается выходной файловый поток с именем flow2 для записи информации. Если файл с названием File. 2 не существует, он будет создан, открыт и соединен с потоком flow2. Если файл уже существует, то предыдущий вариант будет удален и пустой файл создается заново. Проверка:
if (!flow2) cerr << "Не открыт файл File.2 ! ";
fstream flow3("File. 3 ");
создается файловый поток flow3, открывается файл file. 3 и присоединяется к потоку flow3.
Все файловые классы унаследовали от базовых классов функцию close (), позволяющую очистить буфер потока, отсоединить поток от файла и закрыть файл. Функцию close () необходимо явно вызывать при изменении режимов работы с файловым потоком. Автоматически эта функция вызывается только при завершении программы.
В качестве иллюстрации основных особенностей работы с файлами рассмотрим несколько программ.
//Р11-17.СРР - чтение текстового файла с помощью
// операции >>
#include < stdlib.h > // Для функции exit ()
#include < fstream.h > // Для файловых потоков
const int lenName = 13; // max длина имени файла
// Длина вспомогательного массива:
const int lenString =60;
void main()
{ char source[lenName]; // Массив для имени файла
cout << "\nВведите имя исходного файла: ";
cin >> source;
ifstream inFile; // Входной файловый поток
// Открыть файл source и связать его с потоком inFile:
inFile.open(source);
if (!inFile) // Проверить правильность открытия файла
{ cerr << "\n0шибка при открытии файла " << source;
exit(1); // Завершение программы
}
// Вспомогательный массив для чтения:
char string[lenString];
char next;
cout << "\n Текст файла:\n\n";
cin. get (); // Убирает код из потока cin
while(1)
// Неограниченный цикл
{ // Ввод из файла одного слова до пробельного
// символа либо EOF:
inFile >> string;
// Проверка следующего символа:
next = inFile.peek();
// Выход при достижении конца файла:
if (next == EOF) break;
// Печать с добавлением разделительного пробела:
cout << string << " ";
if (next == '\n') // Обработка конца строки
{ cout << '\n':
// 4 - смещение для первой страницы экрана:
static int i = 4;
// Деление по страницам до 20 строк каждая:
if ( ! (++i % 20))
{ cout << "\nДля\ продолжения вывода "
"нажмите ENTER.\n" << end1;
cin.get();
}
}
}
}
Результат выполнения программы - постраничный вывод на экран текстового файла, имя которого набирает на клавиатуре пользователь по "запросу" программы. Размер страницы - 20 строк. В начале первой страницы - результат диалога с пользователем и поэтому из файла читаются и выводятся только первые 16 строк.
Программа демонстрирует неудобства чтения текста из файла с помощью операции извлечения >>, которая реагирует на каждый обобщенный пробельный символ. Между словами, прочитанными из файла, принудительно добавлено по одному пробелу. А сколько их (пробелов) было в исходном тексте, уже не известно. Тем самым искажается содержащийся в файле текст. Читать пробельные символы позволяет компонентная функция getline() класса istream, наследуемая классом if stream. Текст из файла будет читаться и выводиться на экран (в поток cout) без искажений (без пропусков пробелов), если в предыдущей программе чтение и вывод в поток cout организовать таким образом:
while(1) // Неограниченный цикл
{ inFile.getline(string,lenString);
next = inFile.peek();
if (next == EOF) break;
cout << string;
...
Следующая программа читает текстовый файл и разделяет его на две части - строки, не содержащие последовательности из двух символов ' // ', и строки, начинающиеся такой парой символов. Иначе говоря, эта программа позволяет удалить из исходного текста программы на языке Си++ комментарии, начинающиеся парой символов ' // ' и заканчивающиеся признаком конца строки '\n'. В программе определены два выходных потока outtext и outcom , связанные соответственно с создаваемыми заново файлами text.cpp и comment. Имя входного файла с текстом анализируемой программы на языке Си++ определяет (вводит с клавиатуры) пользователь. С этим файлом "связывается" функцией ореn() входной поток inFile. Для проверки безошибочного открытия файлов проверяются значения выражений (!имя потока). При истинности результата вызывается вспомогательная функция errorF(). Вспомогательная переменная int len, позволяет проследить за необходимостью перехода к новой строке в потоке outtext, если во входном потоке inFile обнаружена пара символов ' // '. Символы входного потока последовательно читаются в переменную simb и выводятся в нужный выходной поток. Если не встречен символ ' / ', то все просто - вывод идет в поток outtext. Так как обнаружение во входном отдельного символа ' / ' не есть признак начала комментария, то в этом случае анализируется следующий символ, читаемый из входного потока в переменную next. Если next имеет значение ' / ', то это начало комментария, и последующий вывод нужно вести в поток outcom, предварительно "закрыв" строку в потоке outtext символом '\n'. Комментарии в тексте программы поясняют остальные детали алгоритма.
//Р11-18.СРР - выделение комментариев из текста на Си++;
// посимвольные чтение и запись из текстового
// файла
#include < stdlib.h >
#include < fstream.h >
void errorF(char *ss) // Вспомогательная функция
{ cerr << "\n0шибка при открытии файла" << ' ' <<
ss << 'n';
exit(1);
const int lenName = 23; // Длина массива для имени файла
void main ()
{ char progName[lenName]; //Массив для имени файла
cout << "\nВведите полное имя анализируемой программы: ";
cin >> progName;
if stream inFile; // Входной поток
// Связываем входной поток с файлом программы:
inFile.open(progName);
if (!inFile) errorF(progName);
char simb, last, next; // Вспомогательные переменные
of stream outtext, outcom; // Два выходных потока
// Переменная для вычисления длин строк программы:
int len = 0;
outtext.open("text.cpp",ios::ate);
if (!outtext) errorF("text.cpp");
outcom.open("comment",ios::app);
if (!outcom) errorF("comment");
while (inFile.get(simb)) // Читает символы до EOF
{ len++; // Длима очередной строки программы
if (simb == '\n'}
len =0; // Начнется новая строка программы
if (simb != '/') // Это не начало комментария
// Вывод символа строки программы:
outtext.put(simb);
else
// Когда simb == '/' - возможно начало
// комментария
{ // Проверка на EOF:
if (!inFile.get(next)) break;
if (next == '/')
{ // Теперь уже точно комментарий
if (len != 1)
// "Закрываем" строку программы:
outtext.put ('\n');
outcom.put(simb);
outcom.put(next);
// Цикл до конца комментария,
// т.е. до конца строки:
do
{ // Чтение символа из файла:
inFile.get(simb);
// Запись символа в поток:
outcom.put(simb);
}
while (simb!='\n');
}
else
// Вывод символов, не входящих
// в комментарий:
{ outtext.put(simb);
outtext.put(next);
}
}
}
}
Результат выполнения программы - два файла text. cpp и comment в текущем каталоге, из которого "запущена" на выполнение программа. В первом файле - текст программы без комментариев, во втором - текст всех комментариев. В качестве примера можно выполнить программу для текста из файла P11-18.CPP, т.е. разобрать исходный текст этой же программы.
Для разнообразия при открытии файлов text. cpp и comment в функциях open () использованы разные флаги, определяющие режим работы с соответствующим потоком. Результат одинаков - флаги ios:: ate и ios:: app в этом случае неразличимы. Запись в файлы идет с их дополнением. После каждого нового выполнения программы новая "порция" текстовой информации дописывается в каждый файл. Если необходимо, чтобы сохранялся в файлах только последний результат, второй параметр функции ореn() проще всего задавать по умолчанию.
Как и для других потоков, для потоков, связанных с файлами, допустима перегрузка операций обмена. Для иллюстрации приведем следующую программу pll-19.CPP, в которой перегружена операция включения в поток <<. Действие операции распространено на аргументы типа ofstream& И element, где element - пользовательский тип, а именно структура. В программе с помощью конструктора класса of stream определяется поток filel и связывается с файлом ABC.
Текст программы:
//Р11-19.СРР - запись структур в файл перегруженной
// операцией <<
#include < fstream.h >
struct element
{// Определение некоторой структуры
int nk, n1;
float zn;
};
// Операция-функция, расширяющая действие операции <<
of stream& operator << (of stream& out, element el)
{out << ' ' << el.nk << ' ' << el.n1 <<
' ' << el. zn << ' \n ';
return out;
}
int main()
( const int numbeE1 =5; // Количество структур в массиве
element arel [numbeEl] - {1, 2, 3.45, 2, 3, 4.56,
22, 11, 45.6, 3, 24, 4.33, 3, 6, -5.3};
// Определяем поток и связываем его с новым файлом АВС:
of stream filel("abc");
if (!filel)
{ cerr << "Неудача при открытии файла АВС.";
return 1;
}
// Запись в файл AВС массива структур:
for (int i = 0; i < numbE1; i++)
filel << arel[i];
}
Результат выполнения программы - создание файла с именем АВС в текущем каталоге и запись в этот файл элементов массива из пяти структур element. Содержимое файла АВС:
1 2 3.45
2 3 4.56
22 11 45.599998
3 24 4.33
3 6 -5.3
Файл АВС создается заново при каждом выполнении программы. Чтобы файл создавался один раз и была возможность его дополнения, нужно добавить в конструктор второй параметр таким образом:
of stream file1("abc",ios::app);
В этом случае при двух последовательных выполнениях программы результат в файле АВС будет таким:
1 2 3.45
2 3 4.56
22 11 45.599998
3 24 4.33
3 6 -5.3
1 2 3.45
2 3 4.56
22 11 45.599998
3 24 4.33
3 6 -5.3
В последних версиях компиляторов языка Си++ наконец появились рекомендованные стандартом ANSI средства для обработки особых ситуаций. Такие ситуации в Си++ называют исключительными ситуациями или исключениями (exceptions). Для компиляции приведенных ниже программ можно использовать, например, компиляторы Borland C++ версий 4.0 или 4.5, в которых реализованы все описанные средства.
Механизм обработки особых ситуаций присутствовал в разных языках программирования до появления Си++. Один из примеров -язык ПЛ/1, в котором программисты могли работать как со встроенными (заранее запланированными) ситуациями, так и с ситуациями, создаваемыми (формируемыми) по указанию программиста при наступлении того или иного события. Типичные встроенные ситуации это "деление на нуль", "достижение конца файла", "переполнение в арифметических операциях" и т.п. В языке Си++ практически любое состояние, достигнутое в процессе выполнения программы, можно заранее определить как особую ситуацию (исключение) и предусмотреть действия, которые нужно выполнить при ее возникновении.
Для реализации механизма обработки исключений в язык Си++ введены следующие три ключевых (служебных) слова: try (контролировать), catch (ловить), throw (генерировать, порождать, бросать, посылать, формировать).
Служебное слово try позволяет выделить в любом месте исполняемого текста программы так называемый контролируемый блок:
try { операторы }
Среди операторов, заключенных в фигурные скобки могут быть: описания, определения, обычные операторы языка Си++ и специальные операторы генерации (порождения, формирования) исключений:
throw выражeние_генерации_ исключения;
Когда выполняется такой оператор, то с помощью выражения, использованного после служебного слова throw, формируется специальный объект, называемый исключением. Исключение создается как статический объект, тип которого определяется типом значения выражeния_генерации_исключения. После формирования исключения исполняемый оператор throw автоматически передает управление (и само исключение как объект) непосредственно за пределы контролируемого блока. В этом месте (за закрывающейся фигурной скобкой) обязательно находятся один или несколько обработчиков исключений, каждый из которых идентифицируется служебным словом catch и имеет в общем случае следующий формат:
catch (тип_исключения имя) { операторы }
Об операторах в фигурных скобках здесь говорят как о блоке обработчика исключений. Обработчик исключений (процедура обработки исключений) внешне и по смыслу похож на определение функции с одним параметром, не возвращающей никакого значения. Когда обработчиков несколько, они должны отличаться друг от друга типами исключений. Все это очень похоже на перегрузку функций, когда несколько одноименных функций отличаются спецификациями параметров. Так как исключение передается как объект определенного типа, то именно этот тип позволяет выбрать из нескольких обработчиков соответствующий посланному исключению.
Проиллюстрируем сказанное примерами. Предварительно отметим, что механизм обработки исключений является весьма общим средством управления программой. Он может использоваться не только при обработке аварийных ситуаций, но и любых других состояний в программе, которые почему-либо выделил программист. Для этого достаточно, чтобы та часть программы, где планируется возникновение исключений, была оформлена в виде контролируемого блока, в котором выполнялись бы операторы генерации исключений при обнаружении заранее запланированных ситуаций.
Демонстрацию свойств механизма обработки исключений начнем с несложной функции для определения наибольшего общего делителя (НОД) двух целых чисел.
Классический алгоритм Евклида определения наибольшего общего делителя двух целых чисел (х, у) может применяться только при следующих условиях:
На каждом шаге алгоритма выполняются сравнения:
Следующая программа содержит функцию GCM () для определения наибольшего общего делителя, включающую контролируемый блок с проверкой исходных данных. В основной программе main() трижды вызывается функция GCM(). Два последних вызова выполнены с неверными значениями параметров.
//Р12-01.СРР - GCM - Greatest Соmmon Measure
#include < iostream.h >
// Определение функции с генерацией, контролем и
// обработкой исключений:
int GCM(int x, int у)
{ // Контролируемый блок:
try { if (x==0 | | у==0) throw "\nZERO! ";
if (х < 0) throw "\nNegative parameter 1.";
if (у < 0) throw "\nNegative parameter 2.";
while (х != у)
{ if (x > у) х = x - у
else
у = у - х;
}
return х;
} // Конец контролируемого блока
// Обработчик исключений стандартного типа "строка":
catch (const char *report)
{ cerr << report << " x = " << x <<
", у = " << y;
return 0;
}
} // Конец определения функции
void main()
{ // Безошибочный вызов:
cout << "\nGCM(66,44) = " << GCM(66,44);
// Нулевой параметр:
cout << "\nGCH(0,7) = " << GCM(0,7);
// Отрицательный параметр:
cout << "\nGСМ(-12,8) = " << GCM(-12,8);
}
Результат выполнения программы:
GCM(66,44) = 22
ZERO! х = 0, у = 7
GCM(0,7) = 0
Negative paramefcer 1. x = -12, у = 8
GCM(-12,8) = 0
Программа демонстрирует основные принципы применения механизма обработки исключений. Однако как генерация исключений, так и их обработка выполняются в одной и той же функции, что, не типично для эффективного применения исключений. Однако прежде чем объяснять полезность исключений и очерчивать область их применения рассмотрим текст программы и результаты ее выполнения.
Служебное слово try определяет следующий за ним набор операторов в фигурных скобках как контролируемый блок. Среди операторов этого контролируемого блока три условных оператора анализируют значения параметров. При истинности проверяемого условия в каждом из них с помощью оператора генерации throw формируется исключение, т.е. создастся объект - символьная строка, имеющая атрибуты const char *. При выполнении любого из операторов throw естественная последовательность исполнения операторов функции прерывается и управление автоматически без каких-либо дополнительных указаний программиста передается обработчику исключений, помещенному непосредственно за контролируемым блоком (Это чуть-чуть похоже на оператор goto). Так как в данной программе обработчик исключений локализован в теле функции, то ему доступны значения ее параметров (х, у). Поэтому при возникновении каждого исключения в поток вывода сообщений об ошибках сегг выводится символьная строка с информацией о характере ошибки (нулевые параметры или отрицательные значения параметров) и значения параметров, приведшие к возникновению особой ситуации и к генерации исключения. Здесь же в составном операторе обработчика исключений выполняется оператор return 0; Тем самым при ошибках возвращается необычное нулевое значение наибольшего общего делителя. При естественном окончании выполнения функции GCM(). когда становятся равными значения х и у, функция возвращает значение х.
Так как по умолчанию и выходной поток cout, и поток сеrr связываются с экраном дисплея, то результаты как правильного, так и ошибочного выполнения функции GCM () выводятся на один экран. Обратите внимание, что исключение (const char *) одного типа посылаются в ответ на разные ситуации, возникающие в функции.
Хотя разобранная программа демонстрирует особенности механизма особых ситуаций (исключений), но в ней нет никаких преимуществ перед стандартными средствами анализа данных и возврата из функций. Все действия при возникновении особой ситуации (при неверных данных) запланированы автором функции и реализованы в ее теле. Использование механизма обработки исключений полезнее в тех случаях, когда автор функции только констатирует наличие особых ситуаций и предлагает программисту (использующему функцию) самостоятельно решать вопрос о выборе правил обработки исключений в вызывающей программе.
В следующей программе используется другой вариант функции для определения наибольшего общего делителя (НОД) двух целых чисел, передаваемых в качестве аргументов. В теле указанной функции GCM_ NЕW () нет контролируемого блока и обработчика исключений, но cохранены "генераторы" исключений. Контролируемый блок и обработчик исключений перенесены в функцию main(). Все вызовы функции (верный и с ошибками в параметрах) помещены в контролируемый блок.
//Р12-02.СРР - функция с генерацией, но без контроля
// исключений
#include < iostream.h >
int GCM_NEW(int х, int у) // Определение функции
{ if (х == 0 | | у == 0) throw "\nZERO!";
if (х < 0) throw "\nNegative parameter 1.";
if (у < 0) throw "\nNegative parameter 2.";
while (х != y)
{ if (х > у) х = х - y;
else у = у - х;
}
return х;
} // Контроль и обработка исключений в вызывающей программе
void main ()
{ try // Контролируемый блок
{ cout << "\nGCM_NEW(66,44) = " << GCM_NEW(66,44);
cout << "\nGCM_NEW(0,7) = " << GСМ_NЕW(0,7);
cout << "\nGCM_NEW(-12,8) = " << GCM_NEW(-12,8);
}
catch (const char *report) // Обработчик исключений
{ cerr << report;}
}
Результаты выполнения программы:
GCM_NEW(66,44) = 22
ZERO!
Как видно из результатов, программа прекращает работу npи втором вызове функции после обработки первого исключения. Taк как обработка исключения выполняется вне тела функции, то в обработчике исключений недоступны параметры функции и тем самым утрачивается возможность наблюдения за их значениями, приведшими к особой ситуации.
В целом программа демонстрирует возможность формировать исключения в функции, а наблюдать за этими исключениями в вызывающей программе. По сравнению с предыдущей программой в точке обработки исключения недоступны переменные х, у, локализованные в теле функции. Это снижение информативности можно устранить, введя специальный тип для исключения, т.е. генерируя исключение как информационно богатый объект введенного программистом класса.
В следующей программе определен класс DATA с компонентами, позволяющими отображать в объекте-исключении, как целочисленные параметры функции, так и указатель на строку с сообщением (о смысле события, при наступлении которого сформировано исключение).
//Р12-ОЗ.ОРР - исключения глобального пользовательского
// типа
#include < iostream.h >
struct DATA // Глобальный класс объектов-исключений
{ int n, m;
char *s;
DATA(int x, int y, char *c) // Конструктор класса DATA
{ n = х; m = у; s = с;}
};
int GCM_ONE(int х, int у) // Определение функции
{ if (х == 0 | | y == 0) throw DATA(x,y,"\nZERO!");
if (x < 0) throw DATA(x,y,"\nNegative parameter 1.");
if (у < 0) throw DATA(x,y,"\nNegative parameter 2.');
while (x != y)
{ if (x > y) x = x - y;
else y = y - x;
}
return x;
}
void main()
{ try
{ cout << "\nGCM_ONE(66,44) = " << GCM_ONE(66,44);
cout << "\nGCM_ONE(0,7) = " << GCM_ONE (0,7);
cout << "\nGCM_ONE(-l2,8) = " << GCM_ONE (-12,8);
}
catch (DATA d)
{ cerr << d.s << " x=" << d.n << ", y=" << d.m; }
}
Результат выполнения программы:
GCM_ONE(66,44) = 22
ZERO! x = 0, у = 7
Отметим, что объект класса DATA формируется в теле функции при выполнении конструктора класса. Если бы этот объект не был исключением, он был бы локализован в теле функции и недоступен в точке ее вызова. Но по определению исключений они создаются как временные статические объекты. В данном примере исключения как безымянные объекты класса DATA формируются в теле функции, вызываемой из контролируемого блока. В блоке обработчика исключений безымянный объект типа DATA инициализирует переменную (параметр) DATA d и тем самым информация из исключения становится доступной в теле обработчика исключений, что демонстрирует результат.
Итак, чтобы исключение было достаточно информативным, оно должно быть объектом класса, причем класс обычно определяется специально. В приведенной программе класс для исключений определен как глобальный, т.е. он доступен как в функции GCM_ONE(), где формируются исключения, так и в основной программе, где выполняется контроль за ними и, при необходимости, их обработка. Внешне исключение выглядит как локальный объект той функции, где оно формируется. Однако исключение не локализуется в блоке, где использован оператор его генерации. Исключение как объект возникает в точке генерации, распознается в контролируемом блоке и передается в обработчик исключений. Только после обработки оно может исчезнуть. Нет необходимости в глобальном определении класса объектов-исключений. Основное требование к нему - известность в точке формирования (throw) и в точке обработки (catch). Следующий пример иллюстрирует сказанное. Класс (структура) DATA определен отдельно как внутри функции GCM_TWO(), так и в основной программе. Никаких утверждений относительно адекватности этих определении явно не делается. Но передача исключении проходит вполне корректно, что демонстрируют результаты.
//Р12-04.СРР - локализация определений типов (классов)
// исключений
#include < iostream.h >
int GCM_ONE(int x, int у)
{ struct DATA // Определение типа локализовано в функции
{ int n, m;
char *s;
DATA(int x, int у, char *c) // Конструктор класса DATA
{ n = x; m = у; s = с;}
};
if (x == 0 || у == 0)
throw DATA(x,y,"\nZERO!");
if (x < 0) throw DATA(x,y,"\nNegative parameter 1.");
if (y < 0) throw DATA(x,y,"\nNegative parameter 2.");
while (x != y)
{ if (x > y) x = x - y;
else y = y - x;
}
return x;
}
void main ()
{ struct DATA // Определение типа локализовано в main()
{ int n, m;
char *s;
DATA(int x, int y, char *c) // Конструктор класса DATA
{ n = x; m = y; s = c;}
};
try
{ cout <<"\nGCM_ONE(66,44) = " << GCM_ONE(66,44);
cout <<"\nGCM_ONE(-12,8) = " << GCM_ONE(-12,8);
cout <<"\nGCM ONE(0,7) = " << GCM_ONE(0,7);
}
catch (DATA d)
{ cerr << d.s <<" x = " << d.n <<", y = " << d.m;}
}
Результат выполнения программы:
GCM_TWO(66,44) = 22
Negative parameter 1.
x = -12, у = 8
Если проанализировать приведенные выше программы, то окажется, что в большинстве из них механизм генерации и обработки исключений можно имитировать "старыми" средствами. В этом случае, определив некоторое состояние программы как особое, ее автор предусматривает анализ результатов выполнения оператора, в котором это состояние может быть достигнуто, либо проверяет исходные данные, использование которых в операторе может привести к возникновению указанного состояния. Далее выявленное состояние обрабатывается. Чаще всего при обработке выводится сообщение о достигнутом состоянии и либо завершается выполнение программы, либо выполняются заранее предусмотренные коррекции. Описанная схема имитации механизма обработки особых ситуаций неудобна в тех случаях, когда существует "временной разрыв" между написанием частей программы, где возникает (выявляется) ситуация и где она обрабатывается. Например, это типично при разработке библиотечных функций, когда реакции на необычные состояния в функциях должен определять не автор функций, а программист, применяющий их в своих программах. При возникновении аварийной (особой) ситуации в библиотечной (или просто заранее написанной) функции желательно передать управление и информацию о характере ситуации вызывающей программе, где программист может по своему предусмотреть обработку возникшего состояния. Именно такую возможность в языке Си++ обеспечивает механизм обработки исключений.
Итак, исключения введены в язык в основном для того, чтобы дать возможность программисту динамически (run-lime) проводить обработку возникающих ситуаций, с которыми не может справиться исполняемая функция. Основная идея состоит в том, что "функция, сталкивающаяся с неразрешимой проблемой, формирует исключение в надежде на то, что вызывающая ее (прямо или косвенно) функция сможет обработать проблему" [26].
Механизм исключений позволяет переносить анализ и обработку ситуации из точки ее возникновения (throw point), в другое место программы, специально предназначенное для ее обработки. Кроме того, из точки возникновения ситуации в место ее обработки (в список обработчиков исключений) может быть передано любое количество необходимой информации, например, сведения о том, какие данные и действия привели к возникновению такой ситуации.
Таким образом, механизм обработки исключений позволяет регистрировать исключительные ситуации и определять процедуры их обработки, которые будут выполняться перед дальнейшим продолжением или завершением программы.
Необходимо лишь помнить, что механизм исключений предназначен только для синхронных событий, то есть таких, которые порождаются в результате работы самой программы (к примеру, попытка прерывания программы нажатием Ctrl+C во время ее выполнения не является синхронным событием).
Как уже объяснялось, применение механизма обработки исключений предусматривает выделение в тексте программы двух размещенных последовательно обязательных участков - контролируемого блока, в котором могут формироваться исключения, и последовательности обработчиков исключений. Контролируемый блок идентифицируется ключевым словом try. Каждый обработчик исключений начинается со служебного слова catch. Общая схема размещения указанных блоков:
try
{ операторы контролируемого блока}
catch (спецификация исключения)
{ операторы обработчика исключений}
...
...
catch (спецификация исключения)
{ операторы обработчика исключений}
В приведенных выше программах использовалось по одному обработчику исключений. Это объясняется "однотипностью" формируемых исключений (только типа const char * или только типа DATA). В общем случае в контролируемом блоке могут формироваться исключения разных типов и обработчиков может быть несколько, Размещаются они подряд, последовательно друг за другом и каждый обработчик "настроен" на исключение конкретного типа. Спецификация исключения, размещенная в скобках после служебного слова catch, имеет три формы:
catch (тип имя) { ... }
catch (тип) { ... }
catch (...) { ... }
Первый вариант подобен спецификации формального параметра в определении функции. Имя этого параметра используется в операторах обработки исключения. С его помощью к ним передается информация из обрабатываемого исключения.
Второй вариант не предполагает использования значения исключения. Для обработчика важен только его тип и факт его получения.
В третьем случае (многоточие) обработчик реагирует на любое исключение независимо от его типа. Так как сравнение "посланного" исключения со спецификациями обработчиков выполняется последовательно, то обработчик с многоточием в качестве спецификации следует помещать только в конце списка обработчиков. В противном случае все возникающие исключения "перехватит" обработчик с многоточием в качестве спецификации. В случае если описать его не последним обработчиком, компилятор Borland C++, к примеру, выдаст сообщение об ошибке:
The ' ... ' handler must be last in function ...( )
(Обработчик '...' должен идти последним в функции ...()).
Продемонстрируем некоторые из перечисленных особенностей обработки исключений еще одной программой:
//Р12-05.СРР - исключения без передачи информации
#include < iostream.h >
class ZeroDivide { }; // Класс без компонентов
class Overflow { }; // Класс без компонентов
// Определение функции с генерацией исключений:
float div(float n, float d)
{if (d == 0.0)
throw ZeroDivide(); // Вызов конструктора
double b = n/d;
if (b > le+30) throw Overflow(); // Вызов конструктора
return b;
}
float x = le-20, у = 5.5, z = le+20, w = 0.0;
// Вызываемая функция с выявлением и обработкой исключений:
void RR(void)
{ // Контролируемый блок:
try (у = div(4.4,w);
z = div(z,x);
}
// Последовательность обработчиков исключений:
catch (overflow)
{ cerr <<"\n0verflow"; z = le30; }
catch (zeroDivide)
{ cerr <<"\nZeroDivide"; w = 1.0; }
}
void main()
{ // Вызов функции div() с нулевым делителем w:
RR();
// Вызов функции div() с арифметическим переполнением:
RR();
cout << "\nResult: у = " << у;
cout << "\nResuit: z = " << z;
}
Результат выполнения программы:
ZeroDivide
Overflow
Result: у = 4.4
Result: z = le+ЗО
В программе в качестве типов для исключений используются классы без явно определенных компонентов. Конструктор ZeroDivide () вызывается и формирует безымянный объект (исключение) при попытке деления на нуль. Конструктор Overflow () используется для создания исключений, когда значение результата деления превысит величину le+ЗО. Исключения указанных типов не передают содержательной информации. Эта информация не предусмотрена и в соответствующих обработчиках исключений. При первом обращении к функции RR() значение глобальной переменной у не изменяется, так как управление передается обработчику исключений
catch (ZeroDivide)
При его выполнении выводится сообщение, и делитель w (глобальная переменная) устанавливается равным 1.0. После обработчика исключения завершается функция RR() , и вновь в основной программе вызывается функция RR() , но уже с измененным значением w. При этом обращение div(4.4,w) обрабатывается безошибочно, а вызов div(z,x) приводит к формированию исключения типа overflow. Его обработка в RR() предусматривает печать предупреждающего сообщения и изменение значения глобальной переменной z. Обработчик catch (ZeroDivide) в этом случае пропускается. После выхода из RR() основная программа выполняется обычным образом, и печатаются значения результатов "деления", осуществленного с помощью функции div ().
Продолжим рассмотрение правил обработки исключений. Если при выполнении операторов контролируемого блока исключений не возникло, то ни один из обработчиков исключений не используется, и управление передается в точку непосредственно после них.
Если в контролируемом блоке формируется исключение, то делается попытка найти среди последующих обработчиков соответствующий исключению обработчик и передать ему управление. После обработки исключения управление передается в точку окончания последовательности обработчиков. Возврата в контролируемый блок не происходит. Если исключение создано, однако соответствующий ему блок обработки отсутствует, то автоматически вызывается специальная библиотечная функция terminate (). Выполнение функции terminate () завершает выполнение программы.
При поиске обработчика, пригодного для "обслуживания" исключения, оно последовательно сравнивается по типу со спецификациями исключений, помещенными в скобках после служебных слов catch. Спецификации исключений подобны спецификациям формальных параметров функций, а набор обработчиков исключений подобен совокупности перегруженных функций. Если обработчик исключений (процедура обработки) имеет вид:
catch (Т х) { действия_ обработчика }
где T - некоторый тип, то обработчик предназначен для исключений в виде объектов типа т.
Однако сравнение по типам в обработчиках имеет более широкий смысл. Если исключение имеет тип const T, const T& или T&, то процедура также пригодна для обработки исключения. Исключение "захватывается" (воспринимается) обработчиком и в том случае, если тип исключения может быть стандартным образом приведен к типу формального параметра обработчика. Кроме того, если исключение есть объект некоторого класса т и у этого класса т есть доступный в точке порождения исключения базовый класс в, то обработчик
catch (В х) { действия_обработчика }
также соответствует этому исключению.
Генерация исключений. Выражение, формирующее исключение, может иметь две формы:
throw выражение_ генерации_исключения;
throw;
Первая из указанных форм уже продемонстрирована в приведенных программах. Важно отметить, что исключение в ней формируется как статический объект, значение которого определяется выражением генерации. Несмотря на то, что исключение формируется внутри функции как локальный объект, копия этого объекта передается за пределы контролируемого блока и инциализирует переменную, использованную в спецификации исключения обработчика. Копия объекта, сформированного при генерации исключения, существует, пока исключение не будет полностью обработано.
В некоторых случаях используется вложение контролируемых блоков, и не всегда исключение, возникшее в самом внутреннем контролируемом блоке, может быть сразу же правильно обработано. В этом случае в обработчике можно использовать сокращенную форму оператора:
throw;
Этот оператор, не содержащий выражения после служебного слова, "ретранслирует" уже существующее исключение, т.е. передает его из процедуры обработки и из контролируемого блока, в который входит эта процедура, в процедуру обработки следующего (более высокого) уровня. Естественно, что ретрансляция возможна только для уже созданного исключения. Поэтому оператор throw; может использоваться только внутри процедуры обработки исключений и разумен только при вложении контролируемых блоков. В качестве иллюстрации сказанного приведем следующую программу с функцией compare (), анализирующей четность (или нечетность) значения целого параметра. Для четного (even) значения параметра функция формирует исключение типа const char *. Для нечетного (odd) значения создается исключение типа int, равное значению параметра. В вызывающей функции GG() - два вложенных контролируемых блока. Во внутреннем - два обработчика исключений. Обработчик catch (int n), приняв исключение, выводит в поток cout сообщение и ретранслирует исключение, т.е. передает его во внешний контролируемый блок. Обработка исключения во внешнем блоке не имеет каких - либо особенностей. Текст программы:
//Р12-06.СРР - вложение контролируемых блоков
// и ретрансляция исключений
#include < iostream.h >
void compare(int k) // Функция, генерирующая исключения
{ if (k%2 != 0) throw k; // Нечетное значение (odd)
else
throw "even"; // Четное значение (even)
}
// Функция с контролем и обработкой исключений:
void GG(int j)
{ try
{ try { compare( j.); } // Вложеный контролируемый блок
catch (int n)
{ cout << "\n0dd";
throw; // Ретрансляция исключения
}
catch (const char *)
{ cout " "\nEven"; }
} // Конец внешнего контролируемого блока
// Обработка ретранслированного исключения:
catch (int i) { cout << "\nResult = " << i; }
} // Конец функции GG()
void main()
{ GG(4);
GG(7);
}
Результат работы программы:
Even
Odd
Result = 7
В основной программе функция GG() вызывается дважды - с четным и нечетным параметрами. Для четного параметра 4 функция после печати сообщения "Even" завершается без выхода из внутреннего контролируемого блока. Для нечетного параметра выполняются две процедуры обработки исключений из двух вложенных контролируемых блоков. Первая из них печатает cообщение "odd" и ретранслирует исключение. Вторая печатает значение нечетного параметра, снабдив его пояснительным текстом: "Result = 7".
Если оператор throw; использовать вне контролируемого блока, то вызывается специальная функция terminate (), завершающая выполнение программы.
При вложении контролируемых блоков исключение, возникшее во внутреннем блоке, последовательно "просматривает" обработчики, переходя от внутреннего (вложенного) блока к внешнему до тех пор, пока не будет найдена подходящая процедура обработки. (Иногда действия по установлению соответствия между процедурой обработки и исключением объясняют в обратном порядке. Говорят,что не исключение просматривает заголовок процедуры обработки, а обработчики анализируют исключение, посланное из контролируемого блока и последовательно проходящее через заголовки обработчиков. Однако это не меняет существа механизма.) Если во всей совокупности обработчиков не будет найден подходящий, то выполняется аварийное завершение программы с выдачей, например, такого сообщения: "Program Aborted". Аналогичная ситуация может возникнуть и при ретрансляции исключения, когда во внешних контролируемых блоках не окажется соответствующей исключению процедуры обработки.
Используя следующие ниже синтаксические конструкции, можно указывать исключения, которые будет формировать конкретная функция:
void my_func1() throw(А,В)
{ // Тело функции }
void my func2() throw()
{ // Тело функции }
В первом случае указан список исключений (А и в - это имена некоторых типов), которые может порождать функция my_funcl (). Если в функции my_funcl () создано исключение, отличное по типу от А и в, это будет соответствовать порождению неопределенного исключения и управление будет передано специальной функции unexpected () (см.п. 12.4). По умолчанию функция unexpected () заканчивается вызовом библиотечной функции abort (), которая завершает программу.
Во втором случае утверждается, что функция mу_funс2() не может порождать никаких исключений. Точнее говоря, "внешний мир" не должен ожидать от функции никаких исключений. Если некоторые другие функции в теле функции mу_funс2 () породили исключение, то оно должно быть обработано в теле самой функции my_func2(). В противном случае такое исключение, вышедшее за пределы функции mу_funс2(), считается неопределенным исключением, и управление передается функции unexpected ().
Обработка исключений. Как уже было сказано, процедура обработки исключений определяется ключевым словом catch, вслед за которым в скобках помещена спецификация исключения, а затем в фигурных скобках следует блок обработки исключения. Эта процедура должна быть помещена непосредственно после контролируемого блока. Каждая процедура может обрабатывать только одно исключение заданного или преобразуемого к заданному типа, который указан в спецификации ее параметра. Рассмотрим возможные преобразования при отождествлении исключения с процедурой обработки исключений. Стандартная схема:
try { /* Произвольный код, порождающий исключения Х */ }
catch (Т х)
{ /* Некоторые действия, возможно с х */ }
Здесь определена процедура обработки для объекта типа T. Как уже говорилось, если исключение х есть объект типа T, T&, const T или const T&, то процедура соответствует этому объекту х. Кроме того, соответствие между исключением х и процедурой обработки устанавливается в тех случаях, когда тих- одного типа; T -доступный в точке порождения исключения базовый класс для х; T -тип "указатель" их- типа "указатель", причем х можно преобразовать к типу т путем стандартных преобразований указателя в точке порождения исключения.
Просмотр процедур обработки исключений производится в соответствии с порядком их размещения в программе. Исключение обрабатывается некоторой процедурой в случае, если его тип совпадает или может быть преобразован к типу, обозначенному в спецификации исключения. При этом необходимо обратить внимание, что если один класс (например, ALPHA) является базовым для другого класса (например, BETA), то обработчик исключения BETA должен размещаться раньше обработчика ALPHA, в противном случае обработчик исключения BETA не будет вызван никогда. Рассмотрим такую схему программы:
class ALPHA {};
class BETA: public ALPHA { };
...
void f1()
{ ...
try { ... }
catch (BETA) // Правильно
{ ... }
catch (ALPHA)
{ ... }
...
}
...
void f2()
{ ...
try { ... }
catch (ALPHA) // Всегда будет обработан и объект класса
{ ... // BETA, т.к. "захватываются" исключения
... // классов ALPHA и всех порожденных
} // от него
catch (BЕTA) // Неправильно: заход в обработчик
// невозможен!
{...}
...
}
Если из контролируемого блока будет послано исключение типа BETA, то во втором случае, т.е. в f2 (), оно всегда будет захвачено обработчиком ALPHA, так как ALPHA является доступным базовым классом для BETA. При этом компилятор Borland C++ выдает предупреждение:
Handler for 'BETA' hidden
by previous handler for 'ALPHA' in function f2()
(Обработчик 'BETA' "не виден" за размещенным перед ним обработчиком 'ALPHA' в функции f 2()).
Заметим, что для явного выхода из процедуры обработки исключения или контролируемого блока можно также использовать оператор goto для передачи управления операторам, находящимся вне этой процедуры или вне контролируемого блока, однако оператором go to нельзя воспользоваться для передачи управления обратно - в процедуру обработки исключений или в контролируемый блок.
После выполнения процедуры обработки программа продолжает выполнение с точки, расположенной после последней процедуры обработки исключений данного контролируемого блока. Другие процедуры обработки исключений для текущего исключения не выполняются.
try { // Тело контролируемого блока }
catch (спецификация_исключения)
{ // Тело обработчика исключений}
catch (спецификация_исключения)
{ // Тело обработчика исключений}
// После выполнения любого обработчика
// исполнение программы будет продолжено отсюда
Как уже показано выше, язык C++ позволяет описывать набор исключений, которые может порождать функция. Это описание исключений помещается в качестве суффикса в определении функции или в ее прототипе. Синтаксис такого описания исключений следующий:
throw (список_идентификаторов типов)
где список_идентификаторов_типов - это один идентификатор типа или последовательность разделенных запятыми идентификаторов типов. Указанный суффикс, определяющий генерируемые функцией исключения, не входит в тип функции. Поэтому при описании указателей на функцию этот суффикс не используется. При описании указателя на функцию задают лишь возвращаемое функцией значение и типы аргументов.
Примеры прототипов функций с указанием генерируемых исключений:
void f 2 (void) throw (); // функция, не порождающая
// исключений
void f3(void) throw(BETA); // Функция может порождать
// только исключение типа BETA
void (*fptr) (); // Указатель на функцию, возвращающую void
fptr = f2; // Корректное присваивание
fptr = f3; // Корректное присваивание
В следующих примерах описываются еще некоторые функции с перечислением исключений:
void fl(); // Может порождать любые исключения
void f2() throw (); //Не порождает никаких исключений
void f3() throw(А,В*); // Может порождать исключения в виде
// объектов классов, порожденных из
// А или указателей на объекты
// классов, наследственно
// порожденных из В
Если функция порождает исключение, не указанное в списке, программа вызывает функцию unexpected (). Это происходит во время выполнения программы и не может быть выяснено на стадии ее компиляции. Поэтому необходимо внимательно описывать процедуры обработки тех исключений, которые порождаются функциями, вызываемыми изнутри (из тела рассматриваемой) функции.
Особое внимание необходимо обратить на перегрузку виртуальных функций тех классов, к которым относятся исключения. Речь идет о следующем. Пусть классы ALPHA и BETA определены следующим образом:
class ALPHA // Базовый класс для BETA
{ public: virtual void print(void)
{ cout << "print: Класс ALPHA"; }
};
class BETA: public ALPHA
{ public: virtual void print(void)
{ cout << "print: Класс BETA";}
};
BETA b; // Создан объект класса BETA
Теперь рассмотрим три ситуации:
1)...
try
{ throw (b); // Исключение в виде объекта класса BETA
}
catch (ALPHA d)
{ d.print();}
...
2)...
try
{ throw(d); // Исключение в виде объекта класса BETA
}
catch (ALPHA & d)
{ d.print(); }
...
3) ...
try
{ throw(b); // Исключение в виде объекта класса BETA
}
catch (BETA d)
{ d.print(); }
...
В первом случае при входе в обработчик фактический параметр, соответствующий формальному параметру ALPHA C1, воспринимается как объект типа ALPHA, даже если исключение создано как объект класса BETA. Поэтому при обработке доступны только компоненты класса ALPHA. Результатом выполнения этого фрагмента будет печать строки:
print: Класс ALPHA
Во втором случае во избежание потери информации использована передача значения по ссылке. В этом случае будет вызвана компонентная функция print () класса BETA, и результат будет таким:
print: Класс BETA
Попутно отметим, что функция print () класса BETA будет вызываться и в том случае, если она будет являться защищенным (protected) или собственным (private) компонентом класса BETA. Так, если в описании класса BETA вместо ключевого слова public поставить protected или private, то результат не изменится. В этом нет ничего удивительного, так как "права доступа к виртуальной функции определяются ее определением и не заменяются на права доступа к функциям, которые позднее переопределяют ее" [2]. Поэтому и в данном случае права доступа к функции print определяются правами, заданными в классе ALPHA (см. п. 10.3).
Конечно, можно непосредственно "отлавливать" исключение в виде объекта класса BETA, как показано в третьем примере. Однако в этом случае если функция print () будет входить в число защищенных или собственных компонентов класса BETA, такой вызов функции print () окажется невозможным, и при компиляции будет выдано сообщение об ошибке.
Конкретные реализации компиляторов языка Си++ обеспечивают программиста некоторыми дополнительными возможностями для работы с исключениями. Здесь следует отметить предопределенные исключения, а также типы, переменные и функции, специально предназначенные для расширения возможностей механизма исключений.
Достаточно распространенной особой ситуацией, требующей специальных действий на этапе выполнения программы, является невозможность выделить нужный участок памяти при ее динамическом распределении. Стандартное средство для такого запроса памяти - это операция new или перегруженные операции, вводимые с помощью операций-функций operator new () и operator new[] (). По умолчанию, если операция new не может выделить требуемое количество памяти, то она возвращает нулевое значение (NULL) и одновременно формирует исключение типа xalloc. Кроме того, в реализацию ВС++ включена специальная глобальная переменная _new_handler, значением которой служит указатель на функцию, которая запускается на выполнение при неудачном завершении операции-функции operator new (). По умолчанию функция, адресуемая указателем _new_handler, завершает выполнение программы.
Функция set_new_handler() позволяет программисту назначить собственную функцию, которая будет автоматически вызываться при невозможности выполнить операцию new. Функция set_new_handler() описана в заголовочном файле new.h следующим образом:
new handler set_new handler (new handler my_handler);
Функция set_new_handler() принимает в качестве параметра указатель my_handler на ту функцию, которая должна автоматически вызываться при неудачном выделении памяти операцией new.
Параметр my_handler специфицирован как имеющий тип new_handler, определенный в заголовочном файле new.h таким образом:
typedef void (new * new_handler)() throw (xalloc);
В соответствии с приведенным форматом new_handler - это указатель на функцию без параметров, не возвращающую значения (void) и, возможно, порождающую исключение типа xalloc. Тип xalloc -это класс, определенный в заголовочном файле except. h.
Объект класса xalloc, созданный как исключение, передает информацию об ошибке при обработке запроса на выделение памяти. Класс xalloc создан на базе класса хmsg, который выдает сообщение, определяющее сформированное исключение. Определение хmsg в заголовочном файле except. h выглядит так:
class xmsg
{ public:
хmsg(const string &msg);
хmsg(const хmsg &msg);
~хmsg();
const string & why () const;
void raise() throw(xmsg);
xmsg& operator=(const хmsg &src);
private:
string _FAR *str;
};
Класс хmsg не имеет конструктора по умолчанию. Общедоступный (public) конструктор: хmsg(string msg) предполагает, что с каждым хmsg-объектом должно быть связано конкретное явно заданное сообщение типа string. Тип string определен в заголовочном файле cstring.h.
Общедоступные (public) компонентные функции класса:
void raise() throw(xmsg);
вызов raise () приводит к порождению исключения xmsg. В частности, порождается *this.
inline const string _FAR & xmsg::why() const
{ return *str; };
выдает строку, использованную в качестве параметра конструктором класса xasg. Поскольку каждый экземпляр (объект) класса хmsg обязан иметь собственное сообщение, все его копии должны иметь уникальные сообщения.
Вернемся к классу xalloc. Он описан в заголовочном файле except. h следующим образом:
class xalloc : public xmsg
{ public:
xalloc(const string &msg, size_t size); // Конструктор
size_ t requested() const;
void raise() throw(xalloc);
private:
size_t siz;
};
Класс xalloc не имеет конструктора по умолчанию, поэтому каждое определение объекта xalloc должно включать сообщение, которое выдается в случае, если не может быть выделено size байт памяти. Тип string определен в заголовочном файле cstring.h.
Общедоступные (public) компонентные функции класса xalloc:
void xalloc::raise() throw(xalloc);
Вызов raise () приводит к порождению исключения типа xalloc. В частности, порождается *this.
inline size_t xalloc::requested() const { return siz; }
Функция возвращает количество запрошенной для выделения памяти.
Итак, поговорив о классах xalloc и xmsg, возвратимся к функции set_new_handler (). Ее можно вызывать многократно, устанавливая разные функции для реакции на нехватку памяти. В качестве возвращаемого значения функция set_new_handler() возвращает указатель (тип new_handler) на функцию, которая была установлена с помощью set_new_handler() при ее предыдущем выполнении. Чтобы восстановить традиционную схему обработки ситуации нехватки памяти для операции new, следует вызвать функцию в виде set_new_handler(0). В этом случае все обработчики "отключаются" и накладывается запрет на генерацию исключений.
Итак, если операция new не может выделить требуемого количества памяти, вызывается последняя из функций, установленных с помощью set_new_handler(). Если не было установлено ни одной такой функции, new возвращает значение 0. Функция my_handler () должна описывать действия, которые необходимо выполнить, если new не может удовлетворить требуемый запрос.
Определяемая программистом функция my_handler() должна выполнить одно из следующих действий:
Рассмотрим особенности перечисленных вариантов. Вызов функции abort () демонстрирует следующая программа:
//Р12-07.СРР - завершение программы в функции my_handler()
#include < iostream.h > // Описание потоков ввода/вывода
#include < new.h > // Описание функции set_new_handler()
#include < stdlib.h > // Описание функции abort()
// Функция для обработки ошибок при выполнении
// операции new:
void new_new_handler()
{ cerr << "Ошибка при выделении памяти! ";
abort(); // Если память выделить невозможно, вызываем
// функцию abort(), которая завершает программу
// с выдачей сообщения "Abnormal program
// termination"
}
void main(void)
{ // Устанавливаем собственный обработчик ошибок:
set_new_hahdler(new_new_handler);
// Цикл с ограничением количества попыток выделения
// памяти:
for (int n = 1; n <= 1000; n++)
{ cout << n << ": ";
new char[61440U]; // Пытаемся выделить 60 Кбайт
cout << "Успех!" << end1;
}
}
Несмотря на простоту программы, результаты ее работы будут сильно зависеть от того, откуда она будет запускаться на выполнение. Так, если ее запустить под DOS из интегрированной среды компилятора Borland C++ 3.1 (начиная с версии Borland C++ 4.0, интегрированная среда разработки (IDE - Integrated Development Environment) под DOS отсутствует), то, скорее всего, неудачей закончится уже первый запрос на выделение памяти, и результат будет таким:
1: Ошибка при выделении памяти!
Если запустить эту же программу непосредственно из командной строки DOS (выйдя из интегрированной среды), то несколько первых попыток выделения памяти будут удачными, и результат будет примерно таким:
1: Успех!
2: Успех!
3: Успех!
4: Успех!
5: Успех!
6: Успех!
7: Ошибка при выделении памяти!
Если же откомпилировать эту же программу под Microsoft Windows в режиме EasyWin компиляторов Borland C++ версий 4.0 или 4.5, то для программы будет отводиться не только вся физически доступная оперативная, но также и вся виртуальная память, определенная в этот момент в среде Windows. (Для справки: режим EasyWin дает возможность компилировать DOS-приложения, которые будут осуществлять ввод-вывод в стандартное окно Windows.)
Так, на компьютере с оперативной памятью 8 Мбайт и виртуальной памятью 15 Мбайт при свободных 16.401 Кбайте памяти (согласно информации из окна About оболочки Program Manager) работа программы, скомпилированной с помощью Borland C++ 4.5, проходила следующим образом: сначала очень быстро было выделено 225 блоков по 60 Кбайт (что составляет в сумме 13,5 Мбайт!), после чего начался активный обмен данными с диском и было выделено еще 22 блока. Затем выделение каждого очередного блока происходило с частотой примерно 1 раз в 2-3 с, а при попытке выделить 257-й блок было получено сообщение об ошибке. Всего, таким образом, было выделено 256 * 60Кбайт = 15.360 Кбайт или 15 Мбайт памяти!
Для демонстрации передачи управления исходному обработчику рассмотрим следующую программу (модификация программы Р12-07.СРР):
//Р12-08.СРР
#include < iostream.h > // Описание потоков ввода/вывода
#include < new.h > // Описание функции set_new_ handler()
#include < stdlib.h > // Описание функции abort ()
// Прототип функции - старого обработчика ошибок
// распределения памяти:
void (*old_new_handler) ();
void new_new_handler() // Функция для обработки ошибок
{ сеrr << "Ошибка при выделении памяти! ";
if (old_new_handler) (*old_new_handler)();
abort(); // Если память выделить невозможно, вызываем
// функцию abort(), которая завершает программу
// с выдачей сообщения "Abnormal program
// termination"
}
void main(void)
{ // Устанавливаем собственный обработчик ошибок:
old_new_handler = set_new_handler(new_new_handler);
// Цикл с ограничением количества попыток выделения
// памяти:
for (int n = 1; n <= 1000; n++)
{ cout << n << ": ";
new char[61440U]; // Пытаемся выделить 60 Кбайт
cout << "Успех!" << end1;
}
}
Ее отличие от предыдущей программы (Р12-07.СРР) состоит в том, что при установке собственного обработчика ошибок адрес старого (стандартного) обработчика сохраняется как значение указателя old_new_handler. Этот сохраненный адрес используется затем в функции для обработки ошибок new_new_handler. С его помощью вместо библиотечной функции abort () вызывается "старый" обработчик. Результаты выполнения программы при тех же условиях (в среде Windows):
1: Успех!
2: Успех!
...
244: Успех! Ошибка при выделении памяти!
и затем сообщение в окне: "Program Aborted" (рис. 12.1)
Если mу handler возвращает управление программе, new пытается снова выделить требуемое количество памяти. Наилучшим выходом из ситуации нехватки памяти будет, очевидно, освобождение внутри функции my_handler требуемого количества памяти и возвращение управления программе. В этом случае new сможет удовлетворить запрос, и выполнение программы будет продолжено.
Рис. 12.1. Окно, выдаваемое функцией abort () при компиляции программы в режиме Easy Win
В следующем примере при нехватке памяти освобождаются блоки памяти, выделенной ранее, и управление возвращается программе. Для этого в программе определена глобальная переменная-указатель на блок (массив) символов (char *ptr;).
//Р09.СРР - освобождение памяти в функции my_handler()
#include < iostream.h > // Описание потоков ввода-вывода
#include < new.h > // Описание функции set_new_handler
char *ptr; // Указатель на блок (массив) символов
// Функция для обработки ошибок при выполнении
// операции new:
void new_ new_handler()
{ cerr << "Ошибка при выделении памяти! ";
dalete ptr; // Если память выделить невозможно, удаляем
// последний выделенный блок и возвращаем
// управление программе
}
void main(void)
{ // Устанавливаем собственный обработчик ошибок:
set_new_handler(new_new_handler);
// Цикл с ограничением количества попыток выделения
// памяти:
for (int n = 1; n <= 1000; n++)
{ cout << n << ": ";
// Пытаемся выделить 60 Кбайт:
ptr = new char[61440U];
cout << "Успех!" << end1;
}
set_ new_ handler(0); // Отключаем все обработчики
}
Результат выполнения этой программы будет следующим (при запуске из командной строки DOS):
1: Успех!
2: Успех!
3: Успех!
4: Успех!
5: Успех!
6: Успех!
7: Ошибка при выделении памяти! Успех!
8: Ошибка при выделении памяти! Успех!
9: Ошибка при выделении памяти! Успех!
10: Ошибка при выделении памяти! Успех!
...
Действительно, результаты работы подтверждают сказанное: после неудачного выделения памяти освобождается последний выделенный блок, после чего операции new удается выделить очередной блок памяти такого же размера. Эта последовательность действий прерывается только по достижении конца цикла.
В случае компиляции в режиме EasyWin была отмечена и такая комбинация сообщений:
...
251: Успех!
252: Ошибка при выделении памяти! Успех!
253: Ошибка при выделении памяти! Успех!
254: Успех!
255: Успех!
256: Ошибка при выделении памяти! Успех!
257: Ошибка при выделении памяти! Успех!
...
Это означает, что если программа, выполняемая под Windows, не может получить требуемое количество памяти, то имеет смысл повторить попытки через некоторое время.
Если показанное в последнем примере освобождение памяти невозможно, функция my_handler() обязана либо вызвать исключение, либо завершить программу. В противном случае программа, очевидно, зациклится (после возврата из new_new_handler() попытка new выделить память опять окончится неудачей, снова будет вызвана new_new handler (), которая, не очистив память, вновь вернет управление программе и т.д.)
Так, если в первом примере Р12-07.СРР из функции new_new_handler() убрать вызов функции abort (), получившаяся программа зациклится, и результат ее запуска будет примерно таким:
1: Успех!
2: Успех!
3: Успех!
4: Успех!
5: Успех!
6: Успех!
7: Ошибка при выделении памяти! Ошибка при выделении памяти!
Ошибка при выделении памяти! Ошибка при выделении памяти!
Ошибка при выделении памяти! Ошибка при выделении памяти!
Ошибка при выделении памяти! Ошибка при выделении памяти!
Ошибка при выделении памяти! Ошибка при вы...
Последняя из перечисленных задач, решаемых функцией, назначенной для обработки неудачного завершения операции new, предусматривает генерацию исключения xalloc. Это исключение формирует и функция, которая по умолчанию обрабатывает неудачное завершение операции new. Рассмотрим на примере, какую информацию передает исключение типа xalloc и как эту информацию можно использовать.
//Р12-10.СРР - обработка исключения типа xalloc
#include < except.h > // Описание класса xalloc
#include < iostream.h > // Описание потоков ввода/вывода
#include < cstring.h > // Описание класса string
void main(void)
{try
{ // Цикл с ограничением количества попыток выделения
// памяти:
for (int n = 1; n <= 1000; n++)
{ cout << n << ": ";
new char[61440U];// Пытаемся выделить 60 Кбайт
cout << "Успех!" << end1;
}
}
catch (xalloc X)
{ cout << "При выделении памяти обнаружено ";
cout << "исключение "; << X.why();
}
}
Результат выполнения программы (из командной строки DOS):
1: Успех!
2: Успех!
3: Успех!
4: Успех!
5: Успех!
6: Успех!
7: При выделен0ии памяти обнаружено исключение Out of memory
К сожалению, стандартный обработчик ошибок выделения памяти не заносит количество не хватившей памяти в компоненте siz класса xalloc (на что, между прочим, намекается в документации), поэтому даже если в тело обработчика исключений в последнем примере вставить дополнительно вызов функции requested, возвращающей siz, т.е.:
cout << "Обнаружено исключение " << X.why();
cout << " при выделении ";
cout << X. request () << " байт памяти";
то результат и в этом случае будет не очень информативным:
... 7: Обнаружено исключение Out of memory при выделении 0 байт памяти.
Самым радикальным способом устранения этой некорректности реализации будет, вероятно, перегрузка операции new. Впрочем, эту возможность предоставим читателю, а сейчас покажем, как можно реализовать обработку ошибок операции new с помощью установки своей функции, которая будет порождать исключение xalloc с соответствующими значениями компонентов.
//Р12-11.СРР - "своя" функция обработки ошибок при
// выполнении операции new и генерации xalloc
#include < except.h > // Описание класса xalloc
#include < iostream.h > // Описание потоков ввода/вывода
#include < new.h > // Описание функции set_new_handler
#include < cstring.h > // Описание класса string
#define SIZE 61440U
// Функция для обработки ошибок при выполнении
// операции new:
void new_new_handler() throw(xalloc)
{ // Если память выделить не удалось, формируем исключение
// xalloc с соответствующими компонентами:
throw{xalloc(string("Memory full"),SIZE));
}
void main(void)
{ // Устанавливаем собственный обработчик ошибок:
set_new_handler(new_new_handler);
try // Контролируемый блок
{ for (int n = 1; n <= 1000; n++)
{cout << n << ": ";
new char[SIZE]; // Пытаемся выделить 60 Кбайт
cout << "Успех!" << end1;
}
}
catch (xalloc X) // Обработчик исключений
{cout << "Обнаружено исключение " << X.why();
cout << " при выделении ";
cout << X.requested() << " байт памяти.";
}
}
Результат выполнения программы (из командной строки DOS):
1: Успех!
2: Успех!
3: Успех!
4: Успех!
5: Успех!
6: Успех!
7: Обнаружено исключение Memory full при выделении 61440 байт памяти.
По-видимому, так по смыслу и должен работать встроенный обработчик ошибок выделения памяти. Кроме вышеописанных вариантов, также может использоваться перегрузка операций new() и new[] () для определения каких-либо дополнительных проверок (о перегрузке см. п.9.7).
Функция обработки неопознанного исключения. Функция void terminate () вызывается в случае, когда отсутствует процедура для обработки некоторого сформированного исключения. По умолчанию terminate () вызывает библиотечную функцию abort (), что влечет выдачу сообщения "Abnormal program termination" (или, при компиляции программы в режиме EasyWin в среде Windows, вывод окна, аналогичного изображенному на рис. 12.1) и завершение программы. Если такая последовательность действий программиста не устраивает, он может написать собственную функцию (terminate function) и зарегистрировать ее с помощью функции set_terminate(). В этом случае terminate () будет вызывать эту новую функцию вместо функции abort ().
Функция set_terminate() позволяет установить функцию, определяющую реакцию программы на исключение, для обработки которого нет специальной процедуры. Эти действия определяются в функции, поименованной ниже как terminate_func(). Указанная функция специфицируется как функция типа terminate_function. Такой тип в свою очередь определен в файле except. h как указатель на функцию без параметров, не возвращающую значения:
typedef void (*terminate_function) ();
terminate_function set_terminate(terminate function
terminate_func);
Функция set_terminate() возвращает указатель на функцию, которая была установлена с помощью set_ terminate () ранее.
Следующая программа демонстрирует общую схему применения собственной функции для обработки неопознанного исключения:
//Р12-12.СРР - замена стандартной функции terminate()
#include < stdlib.h > // Для функции abort()
#include < except.h > // Для функции поддержки исключений
#include < iostream.h > // Для потоков ввода-вывода
// Указатель на предыдущую функцию terminate:
void (*old_terminate)();
// Новая функция обработки неопознанного исключения:
void new_tezminate()
{ cout << "\nВызвана функция new_terminate()";
// ... Действия, которые необходимо выполнить
// ... до завершения программы
abort(); // Завершение программы
}
void main()
{ // Установка своей функции обработки:
old_terminate = set_terminatе(new_ terminate);
// Генерация исключения вне контролируемого блока:
throw(25);
}
Результат выполнения программы:
Вызвана функция new_terminate()
Вслед за этим программа завершается и выводит в окно сообщение: "Program Aborted!" (рис. 12.1)
Вводимая программистом функция для обработки неопознанного исключения, во-первых, не должна формировать новых исключений, во-вторых, эта функция должна завершать программу и не возвращать управление вызвавшей ее функции terminate (). Попытка такого возврата приведет к неопределенным результатам.
Функция void unexpected () вызывается, когда некоторая функция порождает исключение, отсутствующее в списке ее исключений. В свою очередь функция unexpected () по умолчанию вызывает функцию, зарегистрированную пользователем с помощью set_ unexpected (). Если такая функция отсутствует, unexpected () вызывает terminate (). Функция unexpected () не возвращает значения, однако может сама порождать исключения.
Функция set_unexpected() позволяет установить функцию, определяющую реакцию программы на неизвестное исключение. Эти действия определяются в функции, которая ниже поименована как unexpectad_func ().
Указанная функция специфицируется как функция типа unexpected_function. Этот тип определен в файле except. h как указатель на функцию без параметров, не возвращающую значения:
typedef void (*unexpected_function)();
unexpected_ function set_unexpected(unexpected_function
unexpected_func);
По умолчанию, неожиданное (неизвестное для функции) исключение вызывает функцию unexpected (), которая, в свою очередь вызывает либо unexpected_func() (если она определена), либо terminate () (в противном случае).
Функция set_unexpected() возвращает указатель на функцию, которая была установлена с помощью set_unexpected() ранее. Устанавливаемая функция (unexpected__func) обработки неизвестного исключения не должна возвращать управление вызвавшей ее функции unexpected (). Попытка возврата приведет к неопределенным результатам.
Кроме всего прочего, unexpected_func() может вызывать функции abort (), exit () и terminate ().
Глобальные переменные, относящиеся к исключениям:
_ _throwExceptionName
содержит имя типа (класса) последнего исключения, порожденного программой;
_ _throwFileName
содержит имя файла с исходным текстом программы, в котором было порождено последнее исключение;
_ _throwLineNumber
содержит номер строки в исходном файле, в которой создано порождение исключения.
Эти переменные определяются в файле except. h следующим образом:
extern char *_ _throwExceptionName;
extern char *_ _throwFileName;
extern unsigned _ _throwLineNumber;
Следующая программа демонстрирует возможности применения перечисленных глобальных переменных:
//Р12-13.СРР - Использование глобальных переменных
// _ _throwExceptionName, _ _throwFileName
// и _ _throwLineNumber
#include < except.h > // Описание переменных _ _ throwXXXX
#include < iostream.h > // Описание потоков ввода-вывода
class A // Определяем класс А
{ public:
void print() // Функция печати сведений об исключении
{ cout << "Обнаружено исключение ";
cout << _ _throwExceptionName;
cout << " в строке " << _ _throwLineNumber;
cout << " файла ' " << _ _throwFileName << " ' "
cout << end1;
}
};
class B: public A {}; // Класс В порождается из А
class С: public A {}; // Класс С порождается из А
С _с; // Создан объект класса С
void f() // Функция может порождать любые исключения
{ try
{ // Формируем исключение (объект класса С):
throw(_c) ;
}
catch (В X) // Здесь обрабатываются исключения типа В
{ X.print();}
}
void main()
{ try { f(); } // Контролируемый блок
// Обрабатываются исключения типа А
// (и порожденных от него) :
catch (A X)
{ X.print();}; // Обнаружено исключение
}
Результат выполнения программы:
Обнаружено исключение С в строке 22 файла 'Р12-13.СРР'
Комментарии в тексте программы достаточно подробно описывают ее особенности. В выводимом на экран результате используются значения глобальных переменных. (Обратите внимание, что указанные результаты получаются при компиляции программы с заранее установленными опциями, о которых кратко сказано ниже в п. 12.7.
Когда выполнение программы прерывается возникшим исключением, происходит вызов деструкторов для всех автоматических объектов, появившихся с начала входа в контролируемый блок. Если исключение было порождено во время исполнения конструктора некоторого объекта, деструкторы вызываются лишь для успешно построенных объектов. Например, если исключение возникло при построении массива объектов, деструкторы будут вызваны только для полностью построенных объектов.
Приведем пример законченной программы, иллюстрирующий поведение деструкторов при обработке исключений.
//Р12-14.СРР - вызовы деструкторов при исключении
#include < iostream.h > // Описание потоков ввода-вывода
#include < new.h > // Описание функции set_ new_ handler
#include < cstring.h > // Описание класса cstring
// Определение класса "блок памяти" class Memory
{char *ptr;
public:
Memory() // Конструктор выделяет 60 Кбайт памяти
{ ptr = new char [61440U];}
~Memory() // Деструктор очищает выделенную память
{delete ptr; }
};
// Определение класса "Набор блоков памяти":
class BigMemory
{ static int nCopy; // Счетчик экземпляров класса + 1
Memory *MemPtr; // Указатель на класс Memory
public:
// Конструктор с параметром по умолчание BigMemory(int n = 3)
{cout << end1 << nCopy << ": ";
MemPtr = new Memory [n];
cout << "Успех!"; // Если память выделена успешно, ++nСору; // увеличиваем счетчик числа экземпляров
}
~BigMemory() // Деструктор очищает выделенную память
{ cout << end1 << --nCopy << ": Вызов деструктора";
delete [] MamPtr;
}
};
// Инициализация статического элемента:
int BigMemory: : nCopy = 1;
// Указатель на старый обработчик для new:
void (*old_new_handler)();
// Новый обработчик ошибок:
void new_new_handler() throw(xalloc)
{ // Печатаем сообщение...
cout << "Ошибка при выделении памяти!";
// ... и передаем управление старому обработчику
(*old_new handler)();
}
void main(void)
{// Устанавливаем новый обработчик:
old__new_handler = set_new_handler(new_new_handler);
try // Контролируемый блок
{ // Запрашиваем 100 блоков по 60 Кбайт:
BigMemory Request!(100);
// Запрашиваем 100 блоков no 60 Кбайт:
BigMemory Requesfc2(100);
// Запрашиваем 100 блоков по 60 Кбайт:
BigMemory Request3(100);
}
catch (xmsg& X) // Передача объекта по ссылке
{ cout << "\n0бнаружено исключение " << X.why();
cout << " класса " << _ _throwExceptionName;
} set_new_ handler (old_new_handler);
}
Результат выполнения программы:
1: Успех!
2: Успех!
3: Ошибка при выделении памяти!
2: Вызов деструктора
1: Вызов деструктора
Обнаружено исключение Out of Меmоrу класса xalloc
Заметим, что обычно вызовы деструкторов происходят по умолчанию. Однако эти умалчиваемые вызовы можно отменить с помощью опции компилятора -xd (см. ниже п. 12.7). В этом случае результат выполнения программы будет таким:
1: Успех!
2: Успех!
3: Обнаружено исключение Out of Memory класса xalloc
Недавнее добавление в проект стандарта языка Си++ механизма динамической идентификации типов (RTTI - Run-Time Type Identification) расширяет язык набором средств, позволяющих идентифицировать конкретные типы объектов во время выполнения программы, даже если известны только указатель или только ссылка на интересующий вас объект. (Отметим, что для использования средств динамического определения типов к программе необходимо подключить файл typeinfo. h.)
Механизм динамического определения (идентификации) типов позволяет проверить, является ли некоторый объект объектом заданного типа, а также сравнивать типы двух данных объектов . Для этого используется операция typeid, которая определяет тип аргумента и возвращает ссылку на объект типа const typeinfo, описывающий этот тип. В качестве аргумента typeid можно также использовать и имя некоторого типа. В этом случае typeid вернет ссылку на объект const typeinfo этого типа. Класс typeinfo содержит операции-функции operator== и operator!=, которые используются для сравнении типов объектов. Класс typeinfo также содержит компонентную функцию name (), возвращающую указатель на строку, содержащую имя типа аргумента.
Операция typeid имеет две формы:
typeid (выражение),
typeid (имя_типа)
Эта операция используется для динамического получения информации о типах данных typeid возвращает ссылку на объект типа const typeinfo. Возвращаемый объект представляет тип операнда оператора typeid.
При динамической идентификации типов важным свойством типа является его полиморфность
Напомним, что понятие полиморфности введено для обозначения классов, имеющих, по крайней мере, одну виртуальную или чисто виртуальную функцию.
Если операнд операции typeid есть разыменованный указатель или ссылка на полиморфный тип, typeid возвращает динамический тип объекта, получаемого по указателю или ссылке на него. Если операнд не полиморфный, typeid вернет объект, представляемый статическим типом. При этом вы можете использовать операцию typeid как с основными типами данных, так и с производными.
Если операнд операции typeid суть разыменованный нулевой указатель, порождается исключение Bad_typeid.
Класс typeinfo описан в файле typeinfo.h (если не считать некоторых опущенных нами модификаторов, не влияющих на существо дела) следующим образом:
class _ _rtti typeinfo
{ public:
tpid * tpp;
private:
typeinfo(const typeinfo &);
typeinfo & operator=(const typeinfo &);
public:
virtual ~typeinfo ();
int operator== (const typeinfo &) const;
int operator!=(const typeinfo &) const;
int before(const typeinfo &) const;
const char * name() const;
};
Общедоступная компонентная функция name () возвращает указатель на строку, определяющую тип операнда typeid. Конкретная длина возвращаемой строки может изменяться при каждом новом вызове функции.
Компонентная функция before () производит лексикографическое сравнение названий (имен) типов двух заданных объектов. Для этого необходимо использовать следующую конструкцию:
typeid(T1).before(typaid(T2)};
Если имя типа T1 "предшествует" имени типа Т2, то функция возвращает 1, в противном случае возвращается 0. Рассмотрим следующий вариант программы:
//Р12-15.СРР - идентификация и сравнение типов объектов
#include < typeinfo.h >
#include < iostream.h >
void main()
{char szString[10];
float floatVar;
cout << typeid (szString) .name() << ", " << typeid(floatVar).name();
cout << end1 <<
typeid,szString).before(typeid(floatVar));
}
Результат выполнения программы:
char *, float
1
В первой строке результата в текстовом виде напечатаны названия типов переменных szString и floatVar, а во второй строке - результат выполнения функции before (). Он равен 1. Это означает, что строка "char *" лексикографически предшествует строке "float", что соответствует латинскому алфавиту.
Операции-функции operator== () и operator!=() обеспечивают сравнение двух типов данных.
Пусть szString и floatVar описаны так, как в предыдущем примере, тогда
... typeid (szString) ==typeid (floatVar) ... // Условие
// НЕ выполнено
...typeid(szString)!=typeid(floatVar)... // Условие
// выполнено
Ключевое слово __rtti и опция -RT. По умолчанию в Borland C++ динамическое определение типов (RTTI) всегда разрешено. Однако его можно разрешать или запрещать, используя ключ -RT (подробнее см. ниже п. 12.7). Если RTTI запрещено или аргументом typeid является указатель или ссылка на класс без виртуальных функций, typeid возвращает ссылку на объект const typeinfo, описывающий объявленный тип указателя или ссылки, но не реальный объект (т.е. указатель или ссылку на него).
Если при компиляции использовать ключ -RT- (то есть отключить динамическое определение типов) и некоторый базовый класс объявлен _ _rtty, то все полиморфные базовые классы также должны быть объявлены как _ _rtti.
struct _ _rtti ( virtual e1(); ); // Полиморфизм
struct _ _rtti ( virtual s2(); ); // Полиморфизм struct X: S1, S2 { };
Если отключен механизм RTTI (использован ключ -RT-), то динамическое определение типов в порожденных классах может оказаться недоступным. При множественном наследовании способность к динамической идентификации типов порожденного класса зависит от типов и порядка следования базовых классов. Порядок следования оказывается важным в случае, если новый класс порождается как классами без виртуальных функций, так и классами с виртуальными функциями.
Если компилировать указанные ниже строки с ключом -RT-, вы должны объявить х с модификатором _ _rtti. В противном случае изменение порядка следования базовых классов для х вызовет ошибку при компиляции:
Can't inherit nоn RTTI class from RTTI base 'S1'
(Невозможно наследовать не-РТП-класс из базового RTTI-класса ' S1').
struct _ _rtti S1
{ virtual func();}; // Полиморфный класс struct S2
{ }; // Не полиморфный класс struct _ _rtti X: S1, S2
{ };
В данном случае класс х явно объявлен как _ _rtti. Это сделано как раз для того, чтобы не зависеть от порядка следования базовых классов при описании х.
В следующем примере класс х порождается из не полиморфных классов, поэтому его не требуется объявлять как _ _rtti.
struct _ _rtti S1 { }; // Не полиморфные классы
struct S2 { } ;
struct X: S2, S2 { }; // Порядок следования базовых
// классов несущественен
Обратите внимание на то, что применение _ _rtti или ключа -RT не преобразует статический класс в полиморфный класс.
//Р12-16.СРР - динамическая идентификация типов для
// полиморфных классов
#include < iostream.h >
#include < typeinfo.h >
class _ _rtti Alpha
{ // Alpha и порожденные из него - RTTI-классы virtual
void func() {}; // Виртуальная функция делает
};
// класс Alpha полиморфным
class В : public Alpha {}; // В - тоже RTTI
int main(void)
{ В Binst; // Копия класса В
В *Bptr; // Указатель на класс В
Bptr = &Binst; // Инициализация указателя
try
{ // Какого типа *Bptr?
if (typeid(*Bptr) == typeid(B))
cout << "тип " << typeid(*Bptr).nаmе() << ".";
if (typeid(*Bptr) != typeid(Alpha))
cout << "\nУказатель не типа Alpha.";
return 0;
}
catch (Bad typeid)
{cout << "Ошибка функции typeid().";
return 1;
}
}
Результат работы программы:
Тип В.
Указатель не типа Alpha.
Доступ к установке перечисленных ниже опции можно получить либо с помощью задания ключей компилятору в командной строке либо из интегрированной среды разработки (IDE) Borland C++ 4.0/4.5, из меню
Options | Project | C++ Options | Exception Handling/RTTI
Если вы используете компилятор командной строки, то для того чтобы установить некоторую опцию, необходимо задать соответствующий ключ, а для того, чтобы ее отменить - тот же ключ, но с последующим символом ' - '. Например, для задания разрешения поддержки исключений (см. ниже) необходимо запустить компилятор с ключом -х, а для того, чтобы такую поддержку запретить - с ключом -х-.
-х Enable Exceptions - Разрешить поддержку исключений
(По умолчанию = ON, т.е. опция включена)
Попытка использования исключений и процедур их обработки без этой опции вызовет появление ошибки при компиляции: "Exception handling not enabled in function ...". Выключение этой опции может быть полезно, например, если вы хотите удалить обработку исключений из вашей программы (скажем, для переноса ее на другую платформу или перехода на другой компилятор).
-хр Enable Exception Location Information - Разрешить получение информации о месте порождения исключения
(По умолчанию = OFF, т.е. опция отключена)
Эта опция предоставляет возможность получения информации о месте возникновения исключительной ситуации (название файла и номер строки) во время выполнении программы (см. функции _ _throwFileName и _ _throwLineNumber).
-xd Enable Destructor Cleanup - Разрешить вызов деструкторов
(По умолчанию = ON, т.е. опция включена)
Если эта опция установлена, то в случае порождения исключения для всех объектов, созданных между областями видимости операторов catch и throw, происходит вызов соответствующих деструкторов. Заметим, что для динамических объектов автоматического вызова деструкторов не происходит и динамические объекты автоматически не освобождаются.
-xf Enable Fast Exception Prologs - Разрешить генерацию "быстрого" начального кода
(По умолчанию == OFF, т.е. опция отключена)
Эта опция указывает компилятору, что необходимо осуществить подстановку inlinе-выражений во все функции обработки исключений. Это повышает надежность программы, но увеличивает ее размер (размер получающегося .ЕХE-модуля). Заметим, что выбор этой опции возможен лишь при отключенной опции Enable Compatible Exception.
-хс Enable Compatible Exceptions - Разрешить совместимость исключений
(По умолчанию = OFF, т.е. опция отключена)
Эта опция позволяет создавать с помощью Borland C++ .ЕХЕ и .DLL модули, совместимые с .ЕXE-модулями, созданными другими программами. Если вы установите эту опцию, то в исполняемый (.ЕХЕ) файл будет включена некоторая информация, касающаяся обработки исключений (даже при выключенной опции Enable Exceptions).
-RT Enable Runtime Type Information - Разрешить динамическое получение информации о типах объектов
(По умолчанию = ON, т.е. опция включена)
Указание этой опции сообщает компилятору о необходимости построения кода, позволяющего производить определение типов объектов во время исполнения программы. Если вы используете ключ -xd, то вам необходимо, как правило, указывать и эту опцию, поскольку в противном случае вы не сможете удалить указатель на класс с виртуальным деструктором. Поэтому обычно эта опция используется одновременно с опцией -xd (Enable Destructor Cleanup).
ПРИЛОЖЕНИЕ 1
Таблица П 1.1 Коды управляющих символов (0 + 31)
Символ | Код 10 | Код 08 | Код 16 | Клавиши | Значение |
nul | 0 | 0 | 00 | ^$ | Нуль |
soh | 1 | 1 | 01 | ^А | Начало заголовка |
stx | 2 | 2 | 02 | ^B | Начало текста |
etx | 3 | 3 | 03 | ^C | Конец текста |
eot | 4 | 4 | 04 | ^D | Конец передачи |
enq | 5 | 5 | 05 | ^Е | Запрос |
ack | 6 | 6 | 06 | ^F | Подтверждение |
bel | 7 | 7 | 07 | ^G | Сигнал (звонок) |
bs | 8 | 10 | 08 | ^H | Забой (шаг назад) |
ht | 9 | 11 | 09 | ^I | Горизонтальная табуляция |
lf | 10 | 12 | ОА | ^J | Перевод строки |
vt | 11 | 13 | ОВ | ^К | Вертикальная табуляция |
ff | 12 | 14 | ОС | ^L | Новая страница |
cr | 13 | 15 | OD | ^M | Возврат каретки |
so | 14 | 16 | ОЕ | ^N | Выключить сдвиг |
si | 15 | 17 | OF | ^O | Включить сдвиг |
dle | 16 | 20 | 10 | ^P | Ключ связи данных |
dcl | 17 | 21 | 11 | ^Q | Управление устройством 1 |
dc2 | 18 | 22 | 12 | ^R | Управление устройством 2 |
dc3 | 19 | 23 | 13 | ^S | Управление устройством 3 |
dc4 | 20 | 24 | 14 | ^T | Управление устройством 4 |
nak | 21 | 25 | 15 | ^U | Отрицательное подтверждение |
syn | 22 | 26 | 16 | ^V | Синхронизация |
etb | 23 | 27 | 17 | ^N | Конец передаваемого блока |
can | 24 | 30 | 18 | ^X | Отказ |
em | 25 | 31 | 19 | ^Y | Конец среды |
sub | 26 | 32 | 1А | ^Z | Замена |
esc | 27 | 33 | 1В | ^[ | Ключ |
fs | 28 | 34 | IС | ^\ | Разделитель файлов |
gs | 29 | 35 | ID | ^] | Разделитель группы |
rs | 30 | 36 | IЕ | ^^ | Разделитель записей |
us | 31 | 37 | IF | ^_ | Разделитель модулей |
В графе "клавиши" обозначение ^ соответствует нажатию клавиши Ctrl, вместе с которой нажимается соответствующая "буквенная" клавиша, формируя код символа.
Таблица П1.2 Символы с кодами 32 +127
Символ | Код 10 | Код 08 | Код 16 | Символ | Код 10 | Код 08 | Код 16 |
пробел | 32 | 40 | 20 | I | 73 | 111 | 49 |
! | 33 | 41 | 21 | J | 74 | 112 | 4A |
" | 34 | 42 | 22 | K | 75 | 113 | 4B |
# | 35 | 43 | 23 | L | 76 | 114 | 4C |
$ | 36 | 44 | 24 | M | 77 | 115 | 4D |
% | 37 | 45 | 25 | N | 78 | 116 | 4E |
& | 38 | 46 | 26 | O | 79 | 117 | 4F |
' | 39 | 47 | 27 | P | 80 | 120 | 50 |
( | 40 | 50 | 28 | Q | 81 | 121 | 51 |
) | 41 | 51 | 29 | R | 82 | 122 | 52 |
* | 42 | 52 | 2А | S | 83 | 123 | 53 |
+ | 43 | 53 | 2B | T | 84 | 124 | 54 |
, | 44 | 54 | 2C | U | 85 | 125 | 55 |
- | 45 | 55 | 2D | V | 86 | 126 | 56 |
. | 46 | 56 | 2E | W | 87 | 127 | 57 |
/ | 47 | 57 | 2F | 88 | 130 | 58 | |
0 | 48 | 60 | 30 | Y | 89 | 131 | 59 |
1 | 49 | 61 | 31 | Z | 90 | 132 | 5A |
2 | 50 | 62 | 32 | [ | 91 | 133 | 5B |
3 | 51 | 63 | 33 | \ | 92 | 134 | 5C |
4 | 52 | 64 | 34 | ] | 93 | 135 | 5D |
5 | 53 | 65 | 35 | ^ | 94 | 136 | 5E |
6 | 54 | 66 | 36 | _ | 95 | 137 | 5F |
7 | 55 | 67 | 37 | . | 96 | 140 | 60 |
8 | 56 | 70 | 38 | a | 97 | 141 | 61 |
9 | 57 | 71 | 39 | b | 98 | 142 | 62 |
: | 58 | 72 | 3A | c | 99 | 143 | 63 |
; | 59 | 73 | 3B | d | 100 | 144 | 64 |
< | 60 | 74 | 3C | e | 101 | 145 | 65 |
= | 61 | 75 | 3D | f | 102 | 146 | 66 |
> | 62 | 76 | 3E | g | 103 | 147 | 67 |
? | 63 | 77 | 3F | h | 104 | 150 | 68 |
$ | 64 | 100 | 40 | i | 105 | 151 | 69 |
A | 65 | 101 | 41 | j | 106 | 152 | 6A |
B | 66 | 102 | 42 | k | 107 | 153 | 6B |
C | 67 | 103 | 43 | l | 108 | 154 | 5C |
D | 68 | 104 | 44 | m | 109 | 155 | 6D |
E | 69 | 105 | 45 | n | 110 | 156 | 6E |
F | 70 | 106 | 46 | o | 111 | 157 | 6F |
G | 71 | 107 | 47 | p | 112 | 160 | 70 |
H | 72 | 110 | 48 | q | 113 | 161 | 71 |
r | 114 | 162 | 72 | y | 121 | 171 | 79 |
s | 115 | 163 | 73 | z | 122 | 172 | 7A |
t | 116 | 164 | 74 | { | 123 | 173 | 7B |
u | 117 | 165 | 75 | | | 124 | 174 | 7C |
v | 118 | 166 | 76 | } | 125 | 175 | 7D |
w | 119 | 167 | 77 | ~ | 126 | 176 | 7E |
x | 120 | 170 | 78 | del | 127 | 177 | 7F |