Декабрь 19, 2019 Просмотры 5 просмотров

[#] Препроцессор [#]

«Ничто не ограничивает полет мысли программиста так жестоко, как компилятор»
Неизвестный программист-мыслитель


Данная статья описывает основы работы с препроцессором Си. Препроцессор – «процессор» макрокоманд, который используется компилятором, чтобы внести некоторые изменения до разбора (обработки) программы. Основная цель препроцессора, это облегчение жизни программиста. Здесь мы рассмотрим GNU Си препроцессор, который имеет дополнительные возможности относительно стандарта ANSI. Поэтому необходимы базовые знания данного языка. Все примеры были выполнены в среде разработке Microsoft Visual C++ 6.0.

До разбора «пользовательских» директив препроцессор выполняет ряд стандартных преобразований. К ним можно отнести:

  • Все комментарии заменяются пробелами.
  • Заранее определенные макросы заменяются определениями.
  • Символы backslash – newline удаляются.
Символы backslash – newline (‘\’) необходимы для деления строк, что позволяет предать более эстетичный вид коду. Сказав вначале, что статья будет описывать основы, я немного слукавил. Думаю, вам будет интересно посмотреть на некоторые ухищрения, которые ну никак нельзя отнести к основам.

Часто процесс изучения языка Си начинается с создания программы, которая выводит строку “Hello world”. Данная программа содержит по крайне мерее одну директиву #include. Это очень распространенная директива, и программисты не представляют жизнь без неё.

Формат записи:
#include <ИМЯ_ФАЙЛА>

Сразу надо отметить, директивы начинаются с символа ‘#’. После и до символа ‘#’ может стоять множество пробелов, но желательно их не использовать. Цель #include , это подключить файл с именем FILE_NAME, который содержит те или иные данные (прототипы функций, объявление макросов, переменных и т.д.). Имя файла должно быть обрамлено либо угловыми скобками, либо кавычками. При использовании кавычек файл ищется в текущем каталоге проекта. А использование угловых скобок дает поиск файла в предопределенных каталогах. Установку предопределенных каталогов смотрите в помощи компилятора. Подключаемый файл может иметь разные расширения. Лучше всего использовать расширение ‘.h’ запомните это. Подключение файла идет с того места, где расположена директива #include <file_name>. Например:

#include <aj\prot.h>

void main(){ aj(); }
void aj(void) { printf("Work function: aj()\n"); }

Внимательный читатель сразу спросит: «А где директива #include , этот код неправильный?» Хочу вас огорчить, но это вполне рабочая программка. Дело в том, что мы убили двух зайцев одним пинком. В файле aj\prot.h хранится не только прототип функции, но и сам файл stdio.h при этом мы не применяли метод «CTRL+C & CTRL+V». Сейчас вы всё сами поймете. Содержимое файла aj\prot.h выглядит так:

void aj(void);
#include <stdio.h>

В этом примере файл подключается до вызова функции aj(), поэтому прототип void aj(void) стоит там, где ему и надо стоять. Но если #include <aj\prot.h> расположить после aj(), например, так:

#include <stdio.h>

void main(){
aj();
#include <aj\prot.h>
}
void aj(void) { printf("Work function: aj()\n"); }

то в этом случае компилятор пошлет нас далеко-далёко, прямо как настоящий сапожник. Как вы заметили, мы вынесли директиву #include <stdio.h> из-за специфической «внутренности» файла stdio.h. Лучше конечно не мешать пользовательские подключаемые файлы (prot.h) с системными файлами (stdio.h). Везде должна быть стройность. Далее для демонстрации полной силы препроцессора рассмотрим вот такой безобидный пример:

#include <stdio.h>

void aj(void);
void main(){
aj
#include <aj\prot.h>
}
void
aj (void){ printf
("Work function: aj()\n"
); }

Сразу скажу, что это рабочая программа. Если проанализировать код, то всё становится на свои места. Выражение aj без (); приведет к ошибкам (в MV C++ 6.0 установите опцию /W4, рекомендую). Ясно, что aj - это имя функции, но не ясно, почему функция хотя и имеет неправильный вызов, но работает. Вся соль заключается в #include <aj\prot.h>, в котором есть одна единственная строка:

();

Директива #include <aj\prot.h> прибавляет к имени aj строку (); в результате получается стандартный вызов функции. Правда, интересно? Но нестандартность кода на этом не заканчивается. Взгляните на тип возвращаемого значения, принимаемых аргументов, вызова функции printf. Все эти извращения достриглись применением newline. Почему компилятор на это не ругнется? Все очень просто. Препроцессор заменяет символ newline на символ пробела. В результате получается void aj (void), пробелы после aj игнорируются, так что все получается правильно. Экспериментируйте и ещё раз экспериментируйте.

Профессиональные программисты часто включают в свои программы директиву #define. Пристально смотрим на #define как на красивую девушку, хотя нет, смотрим ещё пристальней.

Формат записи:
#define ИМЯ_МАКРОСА [(ID)] ТЕЛО_МАКРОСА

Что такое макрос я не буду пояснять, ибо лень, но, думаю, вы сами поймете после примера, что к чему. Итак, макросы подразделяются на простые, «сложные» (макросы с аргументами) и предопределенные макросы. Мы разберем все.
Начнем с простых макросов. Перед использованием макроса мы должны его определить. Для этого, как вы уже догадались, служит директива #define. После этой директивы должно идти имя макроса, а затем значение или тело макроса. Имя макроса пишется заглавными буквами, это не является стандартом, просто хороший тон в программировании, как и отсутствие в заголовочных файлах (*.h) глобальных переменных, тел функций. Имя макроса не может начинаться с цифры. Также имя не может содержать символ пробела, хотя до и после имени пробелы могут стоять. Примеры использования простых макросов:

Вариант I

#include <stdio.h>
#define AJ "Angelina Jolie\n"

void main(){
printf(AJ);
}

Вариант II

#include <stdio.h>
#define AJ "Angelina Jolie\n"
#define PRT printf(AJ);

void main(){
PRT
}
В первом варианте, препроцессор заменяет printf(AJ) на printf("Angelina Jolie\n"). Во втором варианте у нас проявляется одна фишка. Посмотрите внимательно на строку PRT в функции main(), она замениться на printf(AJ);, но это неверный синтаксис, ибо строковая константа должны быть обрамлена кавычками. И еще: после запуска этой программки на экране появится строка Angelina Jolie. Вы догадались, куда я клоню? Если нет, смотрите внимательно первый вариант. При этом если поменять местами #define AJ "Angelina Jolie\n" и #define PRT printf(AJ); ошибки не будет, все будет работать, так как надо.

Сложные макросы немногим отличаются от простых макросов. Сложные макросы имеют в своем составе возможность передачи аргументов. Например:

#include <stdio.h>
#define Ox18(x) {printf("0x%X\n",0x18+x);}

void main() Ox18(0x20)

Сразу в глаза бросается Ox18(0x20), это очень сильно смахивает на вызов функции, но это не так. Ox18(0x20) - всего лишь сложный макрос. После первого этапа работы препроцессора мы получим void main(){printf("0x%X\n",0x18+x);}. Так как мы использовали сложный макрос и в качестве передаваемого аргумента передали значение 0x20, то после второго этапа получим void main(){printf("0x%X\n",0x18+0x20);}. В Ox18(x), икс можно заменить на что-то другое.

Строчку #define Ox18(x) {printf("0x%X\n",0x18+x);} можно поместить в заголовочный файл и затем подключить его. Этот пример не показывает преимущество использования сложных макросов, потому что мы можем заменить строку #define Ox18(x) {printf("0x%X\n",0x18+x);} на #define Ox18 {printf("0x%X\n",0x18+0x20);}, это то же самое, но это статический вариант. Теперь представим, что мы захотим вывести на экран не сумму 0x18+0x20, а скажем 0x18+0x13, тогда нам придется объявлять еще один макрос, скажем Ox1813 с содержимым printf("0x%X\n",0x18+0x13), оно вам надо!?

Вы удивлены тем, что вопреки собственным словам, я использовал в примере имя макроса, начинающееся с цифры? Это, кстати, одна из программерских «рогаток» в исходном коде.

Все знают, что в природе есть баланс, а кто не знает, читайте мануал к природе. То есть существует плюс и минус, черное и белое, так и у нас. Можно не только определять макросы, но и уничтожать их.

Формат записи:
#undef ИМЯ_ДИРЕКТИВЫ

Эта директива очень простая, так что не будем сильно заморачиться. Запомните только, что использовать эту директиву надо крайне осторожно, иначе вы будете часто получать сообщения типа: ['XXX':undeclared identifier]. Ниже идет пример, демонстрирующий работу директивы #undef:

#include <stdio.h>
#define K1 K0

void main(){
int K1 = 10;
printf("K0 = %i\n",K0);
#undef K1
int K1 = 14;
printf("K1 = %i\n",K1);
}

Вывод на консоль после этого примера будет таким: K0 = 10 \n K1 = 14 \n. На первый взгляд, пример может показаться запутанным, точнее неправильным. Сразу в голову лезут сообщения типа: error C2374: 'K1' : redefinition; multiple initialization. Понятно, что int K1 = 14; тут уместна, как беспроводная клавиатура в норе суслика. Нельзя определять дважды в программе переменную того же типа и имени. Но в этом случае препроцессор милостиво соглашается «скушать» код.

И напоследок приведу несколько предопределенных макросов.

  • __DATE__
  • __FILE__
  • __LINE__
  • __TIME__
__DATE__ - строчная константа, дата запуска препроцессора. Макрос __TIME__ похож на __DATE__, только работает со временем. Макросы __FILE__, __LINE__ дают нам имя файл, как строковую константу и номер строки команды, как целое. Эти макросы помогают как для отладки, так и для сопровождения программы.

int div(char cA){
if(!cA) {printf("Error: X / 0\nFile: %s (%d)\n",__FILE__,__LINE__);return -1;}
cM /= cA;
return 0;
}

__FILE__ и __LINE__ имеют явную ценность, когда программа большая, у вас много проектов и т.д. Хочу сразу отметить, что это не все предопределенные макросы, которые существуют, так что не игнорируйте мануалы.

Продолжение следует...


Просмотры 5 просмотров

Статистика просмотров страницы:

  • за текущий месяц (Апрель 2021) - 1;
  • за последние 3 месяца (Январь 2021 - Март 2021) - 3;
  • за последний год (Апрель 2020 - Март 2021) - 4;

Отзывы

Админ
Отлично!
Март 28 Админ

Статьи и обзоры Все статьи

Разработка веб сайтов – одна из самых востребованных услуг в ...
В прошлом году многие впервые попробовали работать удалённо — такой ...
Уже более 70 десятилетий телевизор является наиболее популярным бытовым прибором ...
Комплексное продвижение ресурса включает в себя разные процедуры. Важно найти ...