Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Курс ПЯВУ 2 сем / Лекции 2 сем / Л№30.Другие директивы препроцессора / Лекция №30 Другие директивы препроцессора.odt
Скачиваний:
11
Добавлен:
17.04.2015
Размер:
27.21 Кб
Скачать

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), как автоматически отслеживать, какие из файлов требуется перетранслировать, как собирать несколько объектных файлов в библиотеку, и так далее. Но времени на это у нас нет, да и к нашему курсу это имеет слабое отношение. Так что остановимся на этом.