5 Препроцессорная обработка If-Else и Else-lf
Ранее было показано, как использовать директиву препроцессора #if для проверки, определен ли заданный символ. При использовании директивы #if могут быть случаи, когда требуется, чтобы препроцессором обрабатывался один набор инструкций, если символ определен, и другой набор, если символ не был определен. Используя директиву #else, это можно сделать следующим образом:
#if defined (symbol)
// Операторы
#else
// Операторы
#endif
Дополняя данную обработку еще одним шагом, можно потребовать, чтобы препроцессор проверял состояние (определен/не определен) другого символа, если заданное условие не выполняется. Например, следующие директивы предписывают препроцессору выполнять один набор операторов, если определен символ MY_LIBRARY, другой набор, если MY_LIBRARY не определен, но определен MY_ROUTINES, и третий набор, если не определен ни один из символов:
#if defined (MY_LIBRRARY)
// Операторы
#elif defined (MY_ROUTINES)
// Операторы
#else
// Statements
#endif
Как можно видеть, применение #if и #elif позволяет более гибко использовать в программе механизм условной трансляции.
Другие примеры использования условных директив препроцессора.
1. Этот пример иллюстрирует, кстати, и основное применение первого варианта директив - добиться переносимости программы, включая тот или иной код в зависимости от значения какой-то константы (зачастую "вшитой" прямо в транслятор).
#if !defined(WORDSIZE)
#error WORDSIZE not defined
#elif WORDSIZE==2
/* код для архитектуры с размером слова 2 */
...
#elif WORDSIZE==4
/* код для архитектуры с размером слова 4 */
...
#else
#error Unsupported word size
#endif
2. Второй вариант рассчитан специально на проверку флагов (то есть, проверку наличия или отсутсвия того или иного макроопределения). Например:
#ifdef DEBUG
/*
вызов debug_print() будет вставлен в программу только
если в момент трансляции определено имя DEBUG
*/
debug_print();
#endif
или
/* файл example.h */
#ifndef EXAMPLE_H
/*
Здесь ситуация обратная, код будет включен в программу
только в том случае, если имя EXAMPLE_H не определено
*/
#define EXAMPLE_H
...
#endif
Обратите внимание - в последнем примере мы прямо внутри условного блока трансляции определяем то самое имя, отсутствие которого проверяли в #ifndef. Такой прием вы встретите в любом файле-заголовке стандартной библиотеки. А вскоре, когда мы, наконец, доберемся до С++, вы и сами будете им пользоваться. А нужен он для того, чтобы избежать многократного включения в программу одного и того же файла-заголовка. Теперь, если препроцессору придется обрабатывать такую программу
/* файл example.c */
#include "example.h"
#include "example.h"
то по первой директиве он включит код, содержащийся в условном блоке файла example.h, заодно определив и имя EXAMPLE_H. Когда он доберется до второй директивы #include, ему уже будет известно имя EXAMPLE_H, и повторного включения того-же кода не произойдет.
В СП ТС реализована директива #error. Ее формат:
#error <текст>
Обычно эту директиву записывают среди директив условной компиляции для обнаружения некоторой недопустимой ситуации. По директиве #error препроцессор прерывает компиляцию и выдает следующее сообщение:
Fatal: <имя-файла> <номер-строки> Error directive: <текст>
Fatal — признак фатальной ошибки; <имя-файла> — имя исходного файла; <номер-строки> — текущий номер строки; Error directive — сообщение об ошибке в директиве; <текст> — собственно текст диагностического сообщения.
Например, если именованная константа MYVAL может иметь значение либо 0, либо 1, можно поместить в исходный файл операторы условной компиляции для проверки на некорректное значение MYVAL:
#if (MYVAL != 0 && MYVAL != 1)
#error MYVAL должно иметь значение либо 0, либо 1
#endif
Препроцессор просматривает текст сообщения в директиве #error, и исключает из него комментарии (если они имеются), но именованные константы и макроопределения в тексте не выявляет и макроподстановку не производит.
Пустая директива
Для повышения читабельности программ СП ТС распознает пустую директиву, состоящую из строки, содержащей просто знак #. Эта директива всегда игнорируется.
Указания компилятору языка Си
Синтаксис:
#pragma <последовательность-символов>
Указания компилятору, или прагмы, предназначены для исполнения компилятором в процессе его работы. <Последовательность-символов> задает определенную инструкцию компилятору и, возможно, аргументы.
Набор прагм для каждого компилятора языка Си различен. Для получения подробной информации о прагмах смотрите системную документацию по используемому вами компилятору.
Директивы условной трансляции
Директивы этого типа похожи по смыслу на условные операторы if-else языка C, но управляют не работой программы, а работой препроцессора - позволяют включать в программу тот или иной текст в зависимости от того, какие макроопределения у нас есть. Их два варианта. Первый дает больше возможностей, позволяя (при трансляции) вычислять выражения, сравнивать константы, проверять наличие тех или иных макроопределений. Вынлядит это примерно так:
Этот пример иллюстрирует, кстати, и основное применение первого варианта директив - добиться переносимости программы, включая тот или иной код в зависимости от значения какой-то константы (зачастую "вшитой" прямо в транслятор).
#if !defined(WORDSIZE)
#error WORDSIZE not defined
#elif WORDSIZE==2
/* код для архитектуры с размером слова 2 */
...
#elif WORDSIZE==4
/* код для архитектуры с размером слова 4 */
...
#else
#error Unsupported word size
#endif
Мы сейчас не будем подробно разбираться с этим первым вариантом - если кому понадобится (думаю, что не всем и в любом случае не скоро) прочтете сами.
Второй вариант рассчитан специально на проверку флагов (то есть, проверку наличия или отсутсвия того или иного макроопределения). Например:
#ifdef DEBUG
/*
вызов debug_print() будет вставлен в программу только
если в момент трансляции определено имя DEBUG
*/
debug_print();
#endif
или
/* файл example.h */
#ifndef EXAMPLE_H
/*
Здесь ситуация обратная, код будет включен в программу
только в том случае, если имя EXAMPLE_H не определено
*/
#define EXAMPLE_H
...
#endif
Обратите внимание - в последнем примере мы прямо внутри условного блока трансляции определяем то самое имя, отсутствие которого проверяли в #ifndef. Такой прием вы встретите в любом файле-заголовке стандартной библиотеки. А вскоре, когда мы, наконец, доберемся до С++, вы и сами будете им пользоваться. А нужен он для того, чтобы избежать многократного включения в программу одного и того же файла-заголовка. Теперь, если препроцессору придется обрабатывать такую программу
/* файл example.c */
#include "example.h"
#include "example.h"
то по первой директиве он включит код, содержащийся в условном блоке файла example.h, заодно определив и имя EXAMPLE_H. Когда он доберется до второй директивы #include, ему уже будет известно имя EXAMPLE_H, и повторного включения того-же кода не произойдет.
Вот, пожалуй, и все, что я собирался рассказать вам о первой фазе трансляции - препроцессоре. Давайте теперь поговорим немного о раздельной трансляции.
Раздельная трансляция. extern и static.
Пока программа небольшая ее можно держать в одном файле. И для получения из текста на С готовой к запуску программы достаточно набрать команду
cc prog.c -o prog
которая запустит транслятор, указав ему, что исходный текст находится в файле prog.c, а готовую программу надо записать в файл prog ( -o - это ключ, который говорит транслятору, что в следующем слове командной строки указано имя выходного файла). Однако даже в таком простом случае для сборки программы транслятор использует не только наш файл, но и один из стандартных библиотечных - libc. А библиотека эта тоже когда-то была написана на С и обработана тем же транслятором. Библиотека libc - единственная, которую транслятор подключает автоматически. Если же мы в нашей программе используем, например, функцию вычисления синуса (тоже стандартную, но хранящуюся в другом файле), то нам придется явно сказать об этом транслятору:
cc prog.c -lm -o prog
Здесь -l - тоже ключ транслятора, и этот ключ указывает, что для сборки программы надо использовать дополнительную библиотеку.
Поскольку программы могут использовать функции из многих библиотечных файлов, то и ключ -l можно указывать несколько раз. Например, команда
сс prog.c -lm -lcrypt -o prog
подключит не только математическую библиотеку, но и библиотеку с функциями шифрования. Скажу вкратце (порой полезно бывает знать), как разыскивается нужная библиотека. Транслятор берет то, что стоит после ключа -l и вставляет впереди lib, а сзади добавляет суффикс .so или .a. То, что получилось, и есть имя библиотечного файла. Так, например, ключу -lcrypt соответсвует имя файла libcrypt.so или libcrypt.a. Получив имя файла, транслятор разыскивает его (для этого у него есть список каталогов, где могут находиться файлы библиотек) и подключает к программе.
При необходимости, например, если библиотека лежит в необычном месте, ее можно подключить и без ключа, явным образом. Правда для этого придется указать ее полное имя вместе с путем к каталогу. Например, в команде
сс prog.c /usr/lib/libm.a -lcrypt -o prog
математическая библиотека подключается явным образом, а библиотека шифрования с помощью ключа транслятора.
Ну вот, библиотеки мы подключать научились. Давайте теперь учиться работать с собственными файлами.
Если программа большая, ее текст неудобно держать в одном файле - хотя бы потому, что в редакторе подолгу приходится искать то место, которое мы хотим исправить. Гораздо удобнее разбить текст на части, положив их в разные файлы. Например, держать main() по прежнему в файле prog.c, а все функции вынести в файл func.c. Собрать готовую программу из нескольких файлов по прежнему не составит труда, просто в командной строке надо будет указать несколько файлов:
cc prog.c func.c -o prog
Сама по себе командная строка простая. Однако, чтобы правильно разделить исходный текст на несколько файлов, требуются дополнительные, хотя и не слишком большие усилия. Давайте попробуем это проделать - попытаемся написать программу, которая печатает несколько значений факториала. Причем main() будем держать в файле prog.c, а саму функцию для расчета факториала - в func.c.
Начнем мы с файла prog.c.
/* prog.c */
extern int factorial(int arg);
int main() {
int i;
for (i=0; i<10;i++) {
int result = factorial(i);
printf("i=%d factorial(i)=%d\n", i. result );
}
return 0;
}
Обратите внимание на строку перед main - в ней мы говорим транслятору, что где-то (возможно в другом файле) существует функция factorial(). Напомню - это называется объявлением функции.
Единственное в этом файле, что вам раньше не встречалось - это ключевое слово extern перед объявлением функции factorial. Оно подчеркивает тот факт, что factorial надо искать где-то в другом месте. Для объявления функции это слово не обязательно, а вот до переменной, определенной в другом файле, без него не добраться.
Теперь файл func.c:
/* func.c */
static void print_error(char *s) {
printf("error in factorial: %s\n",s);
}
int factorial(int arg) {
if (arg<0) {
print_error("negative argument");
return -1;
}
else if (arg==0)
return 1;
else
return arg*factorial(arg-1);
}
Здесь у нас есть сама функция вычисления факториала, которую мы используем в main(), и есть еще одна вспомогательная, нужная только факториалу - печатающая сообщение об ошибке.
Причем перед этой вспомогательной функцией стоит уже знакомое вам ключевое слово static. Только смысл у этого слова теперь другой - оно говорит транслятору, что функция print_error() нужна нам только в этом файле, и ее надо сделать недоступной для других, или, как принято говорить, не надо ее экспортировать. Мы таким образом страхуемся от будущих ошибок - если когда-нибудь в файле prog.c появится функция c таким же именем. никакого конфликта не возникнет. Таким же образом с помощью ключевого слова static можно запрещать и экспорт переменных.
По хорошему стоит еще вынести объявление факториала в файл-заголовок, а потом включить этот заголовок и в prog.c, и в func.c:
/* func.h */
extern int factorial(int arg);
/* prog.c */
#include "func.h"
...
/* func.c */
#include "func.h"
...
Вот, собственно, наша программа и разбита на два (точнее даже - на три) файла. И мы можем, наконец оттранслировать ее:
cc prog.c func.c -o prog
Однако и это еще не все. Да, работать с исходным текстом нам теперь будет гораздо удобнее - мы имеем вместо длинного файла несколько коротких. Однако транслятору мы жизнь не только не облегчили, но, наоборот, усложнили. Представьте, что мы написали и отладили один из файлов, допустим, func.c, и он нас вполне устраивает. А вот функцию main() мы постоянно меняем - хотим, чтобы вывод программы покрасивей выглядел. И так много раз - подправили, оттранслировали, запустили - не понравилось. А транслятору при этом каждый раз приходится "за компанию" обрабатывать и второй файл, который мы и не думали менять. Шутки шутками, но длинные программы, случается, часами могут транслироваться. Так что стоит попытаться облегчить (не тарнслятору, себе) жизнь.
Давайте теперь вспомним то, что я говорил в самом начале занятия. Исходный текст при сборке программы претерпевает ряд трансформаций - обрабатывается препроцессором, затем преобразуется в ассемблер, из ассемблера - в объектный файл, и только после этого из объектных файлов и библиотек собирается готовая к запуску программа.
С четвертой, последней фазой, ничего не придумаешь - чтобы собрать программу, нужны все объектные файлы и библиотеки. А вот первые три вполне могут выполняться независимо для каждого файла. Просто надо указать другой ключ транслятору:
сс -с func.c
При этом он сделает нам из исходного файла объектный и положит его в func.o. Вот это, собственно, и есть то, что называют раздельной трансляцией. Теперь, если мы после редактирования prog.c напишем
cc prog.c func.o -o prog
(вместо func.c - func.o), транслятору не придется выполнять лишнюю работу. Можно даже подготовить оба объектных файла:
сс -с prog.c
cc -c func.c
а потом собирать из них программу:
cc prog.o func.o -o prog
при этом, после редактирования любого из файлов достаточно будет перетранслировать только его, а затем заново собрать программу.
Про раздельную трансляцию можно еще много говорить - как писать Makefile (чтобы можно было пользоваться командой make), как автоматически отслеживать, какие из файлов требуется перетранслировать, как собирать несколько объектных файлов в библиотеку, и так далее. Но времени на это у нас нет, да и к нашему курсу это имеет слабое отношение. Так что остановимся на этом.