Условная компиляция

Download Report

Transcript Условная компиляция

Основы информатики
Лекция.
Директивы. Исключения
Заикин Олег Сергеевич
[email protected]
Препроцессор С/С++
Препроцессор С/С++ — программный инструмент,
изменяющий код программы для последующей
компиляции и сборки, используемый в языках
программирования Си и его потомка - C++.
Этот препроцессор обеспечивает использование
стандартного набора возможностей:
• Включение файла — #include
• Макроподстановки — #define
• Условная компиляция — #if, #ifdef, #elif, #else,
#endif
Препроцессор С/С++
Препроцессор языка Си — низкоуровневый,
лексический препроцессор, потому что он требует
только лексического анализа, то есть он
обрабатывает только исходный текст перед
компиляцией, выполняя простую замену лексем и
специальных символов заданными
последовательностями символов, в соответствии с
правилами, установленными пользователями.
Директивы препроцессора
Директивой препроцессора называется строка в
исходном коде, которая начинается с символа # и
следующего за ним ключевого слова препроцессора.
Cписок ключевых слов:
define — задаёт макроопределение (макрос) или
символическую константу
undef — отменяет предыдущее определение
include — вставляет текст из указанного файла
if — осуществляет условную компиляцию при
истинности константного выражения
Директивы препроцессора
ifdef — осуществляет условную компиляцию при
определённости символической константы
выражения
ifndef — осуществляет условную компиляцию при
неопределённости символической константы
else — ветка условной компиляции при ложности
выражения
elif — ветка условной компиляции, образуемая
слиянием else и if
endif — конец ветки условной компиляции
Директива #define, макросы
Макросы в языке Си преимущественно используются
для определения небольших фрагментов кода.
Во время обработки кода препроцессором, каждый
макрос заменяется соответствующим ему
определением.
Например
#define _WIN32
определяет макрос _WIN32
Теперь проверка #ifdef _WIN32 вернет истину
Директива #define, макросы
Если макрос имеет параметры, то они указываются в
теле макроса; таким образом, макросы языка Си
могут походить на Си-функции.
Распространенная причина использования —
избежание накладных расходов при вызове функции в
простейших случаях, когда небольшого кода,
вызываемого функцией, достаточно для ощутимого
снижения производительности.
Директива #define, макросы
Например
#define max(a,b) ((a) > (b) ? (a) : (b))
определяет макрос max, использующий два аргумента
a и b.
Этот макрос можно вызывать как любую Си-функцию,
используя схожий синтаксис. То есть, после обработки
препроцессором,
z = max(x,y);
становится
z = ((x) > (y) ? (x) : (y));
Предопределенные макросы
Кроме пользовательских макросов препроцессор
имеет встроенные макросы которые не требуют
объявления:
__LINE__ - заменяется на номер строки, может быть
переопределен директивой #line, чаще всего
используется для создания отладочной информации
__FILE__ - заменяется на имя файла, так же может
быть переопределено с помощью директивы #line
__DATE__ - заменяется на текущую дату (на момент
компиляции, а точнее обработки препроцессором)
__TIME__ - заменяется на текущее время
Директива #include
Препроцессор Си, встречая следующие директивы:
#include "..."
или
#include <...>
полностью копирует содержимое указанного файла в
файл, в котором указана эта директива, в месте
вызова директивы.
Эти файлы обычно содержат классы и типы данных,
которые должны быть подключены перед их
использованием; таким образом, директива #include
обычно указывается в начале (заголовке) файла. По
этой причине подключаемые файлы и называются
заголовочными.
Условная компиляция
Препроцессор языка Си предоставляет возможность
компиляции с условиями. Это допускает возможность
существования различных версий одного кода.
Обычно, такой подход используется для настройки
программы под платформу компилятора, состояние
(отлаживаемый код может быть выделен в
результирующем коде), или возможность проверки
подключения файла строго один раз.
Условная компиляция
Допустим мы пишем программу, которая содержит
отладочный вывод в файл.
Этот вывод имеет смысл включать только тогда, когда
разработчик программы занимается её отладкой.
Версия, которую программист отдаёт пользователю, этих
выводов не должна содержать. Для этого, например,
можно в программе завести переменную и ставить все
печати под условие.
Условная компиляция
const int debug = 1;
...
void func (void) {
if (debug)
std::cout<< “entering func“ << std::endl;
...
}
В простых случаях это действительно будет выходом из
ситуации. Если переменную debug установить в 0, то
компилятор, видя условие "if (debug)", а также то, что
переменная имеет модификатор const и равна нулю,
скорее всего вообще удалит вызов cout.
Условная компиляция
Но если программа состоит из нескольких исходных
файлов, то такой фокус не пройдёт, потому как
переменная должна быть определена только в одном
модуле, а остальные модули не будут видеть значения
переменной, а потому не смогут удалить мёртвый код.
При этом вызов cout в коде программы останется, хоть и
будет стоять под условием, которое никогда не будет
равно true. По большому счёту и это тоже терпимо, т.к.
десяток или сотня вызовов cout принципиально размер
бинарного фала не увеличат (т.е. увеличение будет
составлять единицы процентов, но не в разы).
Хуже обстоит дело, когда мы вызваем не cout, а какую-то
"нашу" функцию, которая нужна только для
отладочных печатей.
Условная компиляция
Чтобы избежать этих проблем, ненужный код надо
физически вырезать из программы. И для этих
целей удобно использовать директиву условной
компиляции #if
#define DEBUG 1
#if DEBUG == 1
void debug_print (...)
{ ... }
#endif
void func (void) {
#if DEBUG == 1
debug_print (...);
#endif
...
}
Условная компиляция
Если DEBUG == 1, то после препроцессора будет такой код
void debug_print (...)
{ ... }
void func (void) {
debug_print (...);
...
}
Теперь если мы значение макроса DEBUG поменяем на 0, то
ненужный нам текст физически вырежется и в компиляцию в
принципе не попадает
void func (void) {
...
}
Условная компиляция
В случае работы с несколькими файлами определение макроса
DEBUG следует поместить в один из файлов *.h, который
подключается во всех исходных файлах.
Работа с макросом в данном случае (да и не только в данном)
требует аккуратности. Что важно в данном случае управление наличием или отсутствием кода осуществляется
путём замены единственного символа и не требует каких-то
постоянных усилий по закомментированию и
раскомментированию кода.
В MS Visual Studio есть макрос
_DEBUG
Стражи включения
В каждый заголовочный файл нужно добавлять стражи
включения, чтобы содержимое файла не добавлялось
более одного раза.
Пример. fname.h:
#ifndef FNAME_H
#define FNAME_H
… // содержимое заголовочного файла
#endif
Стражи включения
// file1.cpp
#include “header.h”
#include “fname.h”
…
// header.h
#include “fname.h”
…
В файле file1.cpp после первого подключения файла
fname.h станет определена константа FNAME_H, и
второе включение не произойдет.
Обработка исключений
Исключительная ситуация, или исключение — это
возникновение непредвиденного или аварийного
события, которое может порождаться некорректным
использованием аппаратуры.
Синхронные исключения могут возникнуть только в
определённых, заранее известных точках программы.
Примеры: ошибка чтения файла, попытка выделения
памяти при ее нехватке.
Обработка исключений
Асинхронные исключения могут возникать в любой
момент времени.
Примеры: аварийный отказ питания, поступление
новых данных.
Исключения позволяют логически разделить
вычислительный процесс на две части:
• обнаружение аварийной ситуации;
•
ее обработка.
Возможные действия при ошибке

прервать выполнение программы;

возвратить значение, означающее «ошибка»;

вывести сообщение об ошибке в поток cerr и
вернуть вызывающей программе некоторое
приемлемое значение, которое позволит ей
продолжать работу.

обработать исключение
Поток cerr
Выходной поток cerr нужен для отправки сообщений
на стандартное устройство ошибок.
Если вывод сообщений программы перенаправлен в
файл, то сообщения потока cerr все равно будут
показаны в консоли.
Пример.
if (x < 0) { cerr << “Incorrect value”; x = 0; }
cout-сообщения выводятся в файл
a.exe > out.txt
Механизм обработки исключений

Функция, в которой возникла ошибка,
генерирует исключение (используется
ключевое слово throw с параметром константой, переменной или объектом)

Отыскивается соответствующий обработчик
и ему передается управление

Если обработчик не найден, вызывается
стандартная функция terminate, которая
вызывает функцию abort
Синтаксис исключений
try (пытаться) - начало блока исключений.
throw (бросить) - ключевое слово, "создающее"
исключение.
catch (поймать) - начало блока, "ловящего" исключение.
Синтаксис исключений
Контролируемый блок:
try{
...
}
Генерация исключения:
throw [ выражение ];
Обработчики исключений:
catch(тип имя){ ... }
catch(тип){ ... }
catch(...){ ... }
Перехват исключений
Когда с помощью throw генерируется исключение,
функции исполнительной библиотеки:
1. создают копию параметра throw в виде
статического объекта, который существует до тех
пор, пока исключение не будет обработано;
2. в поисках подходящего обработчика раскручивают
стек, вызывая деструкторы локальных объектов,
выходящих из области действия;
3. передают объект и управление обработчику,
имеющему параметр, совместимый по типу с этим
объектом.
Подходящий обработчик:



тот же, что и в параметре catch;
является производным от указанного в
параметре catch (если наследование
производилось с ключом доступа public);
является указателем, который может быть
преобразован по стандартным правилам
преобразования указателей к типу указателя в
параметре catch.
Пример 1
class A {
public:
A() { cout << "Constructor of A\n"; }
~A() { cout << "Destructor of A\n"; }
};
class Error {};
class ErrorOfA : public Error {};
void foo() {
A a;
throw 1;
cout << "This message is never printed" << endl;
}
int main() {
try {
foo();
throw ErrorOfA();
}
catch(int) { cerr << "Catch of int\n"; }
catch(ErrorOfA) { cerr << "Catch of ErrorOfA\n"; }
catch(Error) { cerr << "Catch of Error\n"; }
return 0;
©Павловская Т.А. (СПбГУ ИТМО)
}
результат:
Constructor of A
Destructor of A
Catch of int
Пример 2
#include <fstream>
class Hello{
public: Hello(){cout << "Hello!" << endl;}
~Hello(){cout << "Bye!" << endl;}
};
void f1(){
ifstream ifs("\\INVALID\\FILE\\NAME");
if (!ifs){
cout << "Генерируем исключение" << endl;
throw "Ошибка при открытии файла";
}
}
void f2(){
Hello H;
f1();
}
int main() {
try{ cout << "Входим в try-блок" << endl;
f2();
cout << "Выходим из try-блока" << endl;
}
catch(int i){
cerr << “Oбработчик int - " << i << endl;
return –1;
}
catch(const char * p){
cerr << “Oбработчик const char* - " << p;
return –1;
}
catch(...){cerr << “Oбработчик всех" << endl;
return –1;
}
return 0;
}
Результаты выполнения программы:
Входим в try-блок
Hello!
Генерируем исключение
Bye!
Обработчик const char * - Ошибка при открытии файла
Неперехваченные исключения

Если исключение сгенерировано, но не перехвачено,
вызывается стандартная функция std::terminate().

Функция terminate() будет также вызвана, если механизм
обработки исключения обнаружит, что стек разрушен, или
если деструктор, вызванный во время раскрутки стека,
пытается завершить свою работу при помощи исключения.

По умолчанию terminate() вызывает функцию abort().
Стандартные исключения





bad_alloc — ошибка при динамическом
распределении памяти с помощью new;
bad_cast — неправильное
использование оператора dynamic_cast
bad_typeid — операция typeid не может
определить тип операнда;
bad_exception — при вызове функции
произошло неожидаемое исключение;
length_error — попытка создания
объекта, большего, чем максимальный
размер для данного типа;
Стандартные исключения






domain_error — нарушение внутренних
условий перед выполнением действия;
out_of_range — попытка вызова
функции с параметром, не входящим в
допустимые значения;
invalid_argument — попытка вызова
функции с неверным параметром;
range_error — неправильный результат
вычислений при выполнении;
overflow_error — арифметическое
переполнение;
underflow_error — исчезновение
порядка.