Зачем создавать исключения как объекты? Не проще ли записать код устранения ошибки?
Объекты более гибки и универсальны в использовании, чем обычные программные блоки. С объектами можно передать больше информации и снабдить конструктор и деструктор класса исключения функциями устранения возникшей ошибки.
Почему бы не использовать исключения не только для отслеживания исключительных ситуаций, но и для выполнения рутинных процессов? Разве не удобно использовать исключения для быстрого и безопасного возвращения по стеку вызовов к исходному состоянию программы?
Безусловно, и многие программисты на C++ используют исключения именно в этих целях. Но следует помнить, что прохождение исключения по стеку вызовов может оказаться не таким уж безопасным. Так, если объект был создан в области динамического обмена, а потом удален в стеке вызовов, это может привести к утечке памяти. Впрочем, при тщательном анализе программы и использовании современного компилятора эту проблему можно предупредить.
Кроме того, многие программисты считают, что использование исключений не по прямому назначению делает программу слишком запутанной и нелогичной.
Всегда ли следует перехватывать исключения сразу за блоком try, генерирующим это исключение?
Нет, в стеке вызовов перехват исключения может осуществляться в любом месте, после чего стек вызовов будет пройден то того места, где происходит обработка исключения.
Зачем использовать утилиту отладки, если те же функции можно осуществлять прямо во время компиляции с помощью объекта cout и условного выражения #ifdef debug?
В действительности утилита отладки предоставляет значительно больше средств и возможностей, таких как пошаговое выполнение программы, установка точек останова и анализ текущих значений переменных. При этом вам не приходится перегружать свой код многочисленными командами препроцессора и выражениями, которые никак не связаны с основным назначением программы.
Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний, а также ряд упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов.
Контрольные вопросы
1. Что такое исключение?
2. Для чего нужен блок try?
3. Для чего используется оператор catch?
4. Какую информацию может содержать исключение?
5. Когда создается объект исключения?
6. Следует ли передавать исключения как значения или как ссылки?
7. Будет ли оператор catch перехватывать производные исключения, если он настроен на базовый класс исключения?
8. Если используются два оператора catch, один из которых настроен на базовое сообщение, а второй — на производное, то в каком порядке их следует расположить?
9. Что означает оператор catch(...)?
10. Что такое точка останова?
Упражнения
1. Запишите блок try и оператор catch для отслеживания и обработки простого исключения.
2. Добавьте в исключение, полученное в упражнении 1, переменную-член и метод доступа и используйте их в блоке оператора catch.
3. Унаследуйте новое исключение от исключения, полученного в упражнении 2. Измените блок оператора catch таким образом, чтобы в нем происходила обработка как производного, так и базового исключений.
4. Измените код упражнения 3, чтобы получить трехуровневый вызов функции.
5. Жучки: что не правильно в следующем коде?
#include "string" //класс строк
сlass xOutOfMemory
{
public:
xOutOfMemory(){ theMsg = new сhar[20];
strcpy(theMsg, "trror in momory");}
~xOutOfMemory(){ delete [] theMsg;
cout << "Memory restored, " << endl; }
char * Message() { return theMsg; }
private:
char >> theMsg;
};
main()
{
try
{
char * var = new char;
if ( var == 0 )
{
xOutOfMemory * px = new xOutOfMemory;
throw px;
}
}
catch( xOutOfMemory * theException )
{
cout << theException->Message() <<endl;
delete theException;
}
return 0;
}
6. Данный пример содержит потенциальную ошибку, подобную возникающей при попытке выделить память для показа сообщения об ошибке в случае обнаружения нехватки свободной памяти. Вы можете протестировать эту программу, изменив строку if (var == 0) на if (1), которая вызовет создание исключения.
День 21-й. Что дальше
Примите наши поздравления! Вы почти завершили изучение полного трехнедельного интенсивного курса введения в C++. К этому моменту у вас должно быть ясное понимание языка C++, но в современном программировании всегда найдутся еще не изученные области. В этой главе будут рассмотрены некоторые опущенные выше подробности, а затем намечен курс для дальнейшего освоения C++.
Большая часть кода файлов источника представлена командами на языке C++. Компилятор превращает этот код в программу на машинном языке. Однако перед запуском компилятора запускается препроцессор, благодаря чему можно воспользоваться возможностями условной компиляции. Итак, сегодня вы узнаете:
• Что представляет собой условная компиляция и как с ней обращаться
• Как записывать макросы препроцессора
• Как использовать препроцессор для обнаружения ошибок
• Как управлять значениями отдельных битов и использовать их в качестве флагов
• Какие шаги следует предпринять для дальнейшего эффективного изучения C++
Процессор и компилятор
При каждом запуске компилятора сначала запускается препроцессор, который ищет команды препроцессора, начинающиеся с символа фунта (#). При выполнении любой из этих команд в текст исходного кода вносятся некоторые изменения, в результате чего создается новый файл исходного кода. Этот новый файл является временным, и вы обычно его не видите, но можете дать команду компилятору сохранить его для последующего просмотра и использования.
Компилятор читает не исходный файл источника, а результат работы препроцессора и компилирует его в исполняемый файл программы. Вам уже приходилось встречаться с директивой препроцессора #include: она предписывает найти файл, имя которого следует за ней, и вставить текст этого файла по месту вызова. Этот эффект подобен следующему: вы полностью вводите данный файл прямо в свою исходную программу, причем к тому времени, когда компилятор получит исходный код, файл будет уже на месте.
Просмотр промежуточного файла
Почти каждый компилятор имеет ключ, который можно устанавливать или в интегрированной среде разработки, или в командной строке. С помощью этого ключа можно сообщить компилятору о том, что вы хотите сохранить промежуточный файл. Если вас действительно интересует содержимое этого файла, обратитесь к руководству по использованию компилятора, чтобы узнать, какие ключи можно для него устанавливать.
Использование директивы #define
Команда #define определяет строку подстановки. Строка
#define BIG 512
означает, что вы предписываете препроцессору заменять лексему BIG строкой 512 в любом месте программы. Эта запись не является командой языка C++. Строка 512 вставляются в исходную программу везде, где встречается лексема BIG. Лексема — это строка символов, которую можно применить там, где может использоваться любая строка, константа или какой-нибудь другой набор символов. Таким образом, при записи строк
#define BIG 512 int myArray[BIG];
промежуточный файл, создаваемый препроцессором, будет иметь такой вид:
int myArray[512];
Обратите внимание, что в коде исчезла команда #define. Из промежуточного файла все директивы препроцессора удаляются, поэтому они отсутствуют в конечном варианте кода источника.
Использование директивы #define для создания констант
Один вариант использования директивы #define - это создание констант. Однако этим не стоит злоупотреблять, поскольку директива #define просто выполняет замену строки и не осуществляет никакого контроля за соответствием типов. Как пояснялось на занятии, посвященном константам, гораздо безопаснее вместо директивы #define использовать ключевое слово const.
Использование директивы #define для тестирования
Второй способ использования директивы #define состоит в простом объявлении того, что данная лексема определена в программе. Например, можно записать следующее: