Условная компиляция
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 — исчезновение
порядка.