
- •18.Классы для работы с векторами и матрицами.
- •1.1 Описание программы, матрицы
- •1.3 Microsoft Visual Studio Express
- •1.4 Стандартная библиотека шаблонов (stl)
- •1.5 Vector
- •1.6 Перегрузка операторов
- •2. Проектирование и этапы разработки
- •2.1 Постановка задачи
- •Int lbound;
- •Int ubound;
- •1) Создается и запоминается копия объекта (переменной). Это означает, что
- •2) Разматывает стек, вызывая деструкторы локальных объектов, выходящих
- •3) Передается управление ближайшему catch-обработчику, совместимому с
- •Void f1(void) {
- •Void f2( Vector& V ) {
- •Void f() {
- •Void g() {
- •Void use_file( const char* filename ) {
- •Void use_file( const char* filename ) {
- •Void get_resources( ) {
- •Void use_file( const char* filename )
- •X( const char* aa, const char* bb )
- •Init(), то выделенная память не будет освобождена, поскольку объект полностью
- •X( int s ) : ip( s ) { init( ); }
- •Void f( int a ) throw( Range, Size, int, char* )
- •Void f( int a ) {
- •Int g( void ) throw( ); // функция не заявляет каких-либо исключений
- •Void f( void ) throw( int ){ throw “This error message has type char* ”; }
- •Void rethrow( ) { throw; }
- •Void network_g( )
Int lbound;
Int ubound;
unsigned max_size;
public:
// . . .
T& operator[]( int ) {
if( i < lbound || ubound < i ) throw "Index out of range.";
return v[i-lbound];
};
// . . .
}; // class Vector
Здесь генерируется исключение типа const char*, который соответствует
постоянной строке "Index out of range". Ключевое слово throw отвечает
оператору языка С++, «выбрасывающему» исключение. Функция,
предполагающая контроль индексов, должна содержать программный код,
использующий векторный тип в так называемом try-блоке:
void f( Vector<int>& w )
{
// . . .
try {
do_something(w);
}
catch ( const char* s ) {
// здесь пользователь имеет возможность
// обработать исключительную ситуацию
// например, так
cerr << “Произошла ошибка: ” << s << “\n”;
exit(1);
}
// Здесь продолжается выполнение при нормальном
// завершении функции do_something()
}
5
Слова try и catch являются ключевыми словами языка С++. Конструкция
catch ( . . . . . ) { . . . . . }
называется обработчиком исключения и должна размещаться сразу за try-
блоком, либо за другим (предшествующим) обработчиком исключения. В
круглых скобках обработчика содержится имя типа, идентифицирующее
исключительную ситуацию и, возможно, имя аргумента, который может
использоваться подобно аргументу функции и передавать обработчику
дополнительную информацию, связанную с исключением – значение
исключения.
Процесс генерации и последующей обработки исключений требует
поиска соответствующего обработчика от точки возникновения исключения
через последовательность стековых фреймов вызовов функций до фрейма
функции, имеющей искомый обработчик. Говорят, что происходит
раскручивание стека вызовов.
ПРОГРАММНЫЙ СТЕК И ЕГО ИЗМЕНЕНИЕ
Для лучшего понимания механизма обработки исключения необходимо
подробнее рассмотреть процесс изменения программного стека во время
выполнения программы.
Стек представляет собой область оперативной памяти компьютера,
которая используется для размещения информации, связанной с вызовом
функций, а также для хранения автоматических локальных переменных. Перед
вызовом функции в стек заносится адрес возврата, кадр состояния (фрейм),
содержащий значения регистров и позволяющий перед возвратом восстановить
состояние вычислительного процесса и, при необходимости, значения
параметров. После входа в программную секцию функции, со стека снимаются
значения параметров и помещаются автоматические локальные переменные.
Перед возвратом из функции со стека снимаются локальные переменные, кадр
состояния и адрес возврата, затем в стек заносится возвращаемое функцией
значение. По кадру состояния восстанавливаются регистры, после чего
управление передается по адресу возврата. Таким образом, в процессе
выполнения программы стек увеличивается и уменьшается.
Раскручиванием программного стека называется процесс удаления
из него значений, в результате которого уничтожаются стековые
фреймы вызванных функций.
Увеличение программного стека происходит в направлении уменьшения
адресов выделенной физической памяти компьютера. Код программы,
напротив, размещается в младших адресах. Далее следует область данных, где
размещаются внешние и статические переменные и константы, а затем
динамически распределяемая память («куча»). При использовании больших
локальных массивов данных или при значительной глубине рекурсии возможно
переполнение стека, т.е. разрастание его до других разделов памяти.
6
Рассмотрим схематический программный код и изменение стека в
процессе его выполнения.
void func2( ) {
int i=256;
}
void func1( ) {
int k=128;
// Когда поток управления достигает
// этого места, то, двигаясь вглубь стека,
// увидим локальную переменную k,
// фрейм функции func1(), адрес возврата
// в main(), переменную j
func2();
}
void main( void ){
int j=64;
func1();
}
void func2( ) {
int i=256;
}
void func1( ) {
int k=128;
func2();
}
void main( void ){
int j=64;
// Когда поток управления
// достигает этого места, в стеке
// размещена переменная j
func1();
}
вершина
стека
Младшие адреса памяти
• Код программы
• Статические данные
• Куча
j=64
Стек
Старшие адреса памяти
вершина
стека
Младшие адреса памяти
• Код программы
• Статические данные
• Куча
j=64
адрес возврата в main()
k=128
фрейм для func1()
Стек
Старшие адреса памяти
7
Имеется несколько механизмов раскручивания программного стека [2]:
• естественное раскручивание в процессе выполнения программы;
• раскручивание, связанное с обработкой исключительных ситуаций;
• раскручивание путем применения функции longjmp();
• и, наконец, путем прямой модификации регистра указателя стека CPU
Важно заметить, что в случае обработки исключений, как и при
естественном разматывании стека, удаление автоматических локальных
объектов сопровождается вызовом для них деструкторов.
ОСОБЕННОСТИ МЕХАНИЗМА ОБРАБОТКИ ИСКЛЮЧЕНИЙ В ЯЗЫКЕ С++
Вышеизложенное объясняет следующие особенности механизма
обработки исключений в языке С++ [1,2]:
1) Обрабатываются только исключительные ситуации, явно генерируемые
некоторой функцией;
2) Поддерживается окончательная модель обработки. Это означает, что
после возникновения исключения невозможно продолжение выполнения
программы с точки исключения;
3) Обработка исключения возможна только в функции, вызванной до его
возникновения и еще не завершившейся. После выбрасывания исключения
void func2( ) {
int i=256;
// Когда поток управления достигает этого
// места, то, двигаясь вглубь стека,
// увидим локальную переменную i, фрейм
// функции func2(), адрес возврата в
// func1(), переменную k, фрейм функции
// func1(), адрес возврата в main(),
// переменную j
}
void func1( ) {
int k=128;
func2();
}
void main( void ){
int j=64;
func1();
}
вершина
стека
Младшие адреса памяти
• Код программы
• Статические данные
• Куча
j=64
адрес возврата в main()
k=128
фрейм для func1()
i=256
Стек
Старшие адреса памяти
адрес возврата в func1()
фрейм для func2()
8
управление должно быть передано некоторому программному блоку,
принадлежащему функции, еще находящейся в стеке вызовов, путем его
разматывания;
4) Если заявлено исключение, для которого нет обработчика в цепочке
вызовов, программа будет завершена. В процессе поиска обработчика
программный стек будет раскручен до конца;
5) Если обработчик «поймал» исключение, то обработка этого же исключения
другими обработчиками, которые могут для него существовать, невозможна.
Другими словами, действует первый подходящий обработчик,
встретившийся в процессе разматывания стека;
6) Если после заявления исключения управление передано catch-блоку, то вне
зависимости от результата последующих действий исключение считается
обработанным;
7) Обработчик, как и обычная функция, может заявить исключение. Более того,
в нем может использоваться оператор throw без параметра, что означает
повторное генерирование исключения, обрабатываемого в данный момент;
СРАВНЕНИЕ ИСКЛЮЧЕНИЙ С ТРАДИЦИОННЫМИ СПОСОБАМИ
ОБРАБОТКИ ОШИБОК
Сравним традиционные подходы к обработке ошибки и концепцию
генерирования исключительных ситуаций на примере обнаружении выхода
индекса в операторе [ ] класса Vector за допустимые границы. Рассмотрим
возможные традиционные варианты обработки ошибки:
1. Завершить программу, выдав сообщение об ошибке.
2. Возвратить условленное значение, обозначающее ошибку.
3. Возвратить значение, как при нормальном завершении, выставив
некоторый (внешний) признак ошибки.
4. Вызвать функцию, предназначенную для вызова в случае ошибки (error
handler functions).
Для обработки исключений случай 1 фактически реализуется по
умолчанию, когда заявленное событие не обрабатывается. Однако во многих
ситуациях при возникновении ошибок можно и нужно поступать более
изобретательно.
Реализовать случай 2 не всегда возможно. Например, в нашем случае нет
приемлемого возвращаемого значения для обозначения ошибки – любое
значение типа T является корректным результатом для оператора [ ]. Кроме
того, этот подход весьма утомителен, т.к. при каждом использовании оператора
[ ] следовало бы проверять возвращенное значение. Поэтому такой подход
редко используется для систематической проверки возникновения всех ошибок.
Оставить программу с обозначенной, но не обработанной ошибкой, что
соответствует случаю 3, опасно, так как вызывающая функция может не
заметить, что в вызываемой функции оказалось не все в порядке. Например,
9
многие функции библиотеки С устанавливают глобальную переменную errno
для индикации ошибки. Поэтому в программах без последовательных проверок
errno будут появляться ошибки, вызванные ошибочными значениями,
возвращаемые предыдущими вызовами. Более того, использование одной
глобальной переменной для различных ошибок недопустимо, если
присутствует параллелизм. (Мы помним, что параллелизм есть одна из
характерных черт объектно-ориентированного программирования).
Из традиционных подходов случай 4 наиболее гибкий и концептуально
целостный. Он часто используется и в определенном смысле близок к
обработке исключений. Недостатком этого подхода является, отсутствие
единого стандарта на реализацию функций-обработчиков (error handler
functions). Пользователю библиотеки с такой дисциплиной обработки ошибок
требуется дополнительные усилия на ее освоение. Заметим, что от автора
библиотеки классов с такой обработкой ошибок также требуются
дополнительные усилия, поскольку необходимо хорошо продумать вопрос
использования функций-обработчиков по умолчанию, применяемых при
отсутствии пользовательских реализаций.
Механизм обработки исключений является альтернативой традиционным
методам во многих случаях, когда последние неприменимы, недостаточны,
чреваты ошибками или некрасивы. Он определяет стандартный способ для
явного отделения «вспомогательного» кода обработки от «обычного» кода
выполнения, делая программу более читабельной и легче контролируемой.
Формальный стиль, устанавливаемый средствами языка, упрощает
взаимодействие между независимо написанными фрагментами программы.
Вместе с тем необходимо понимать, что «обработка исключительных
ситуаций остается сложной задачей и механизм обработки ситуаций – хотя и
больше формализован, чем заменяемые им средства – все еще остается
относительно неструктурированным по сравнению со средствами языка для
локального управления выполнением в программе» [1].
ГЕНЕРИРОВАНИЕ И РАСПОЗНАВАНИЕ ИСКЛЮЧЕНИЙ.
Исключение выступает одновременно как переменная и как тип данных
(или как объект и как класс). Оператор throw выбрасывает объект, а catch-
обработчик ловит класс (или throw выбрасывает переменную, а catch ловит ее
тип). При генерации исключения функции библиотеки исполняющей системы
осуществляют следующие действия: