Опубликован: 05.11.2013 | Доступ: свободный | Студентов: 414 / 22 | Длительность: 11:51:00
Лекция 5:

Препроцессор, оформление программы и средства ввода/вывода

< Лекция 4 || Лекция 5: 12345 || Лекция 6 >
Ключевые слова: Си

Языки программирования обеспечивают плодородную почву для спорных идей.

Никлаус Вирт

В программах, написанных на языке Си, вы часто встречаете директивы препроцессора, или, другими словами, символьного макропроцессора. Все эти директивы записываются с новой строки текста и начинаются с символа "#".

4.1. Макроподстановки

Типичным примером подобной директивы является строка

#include <параметры>  
    

или

#include "параметры"  
    

Оба вида означают необходимость подставить в это место текст файла, определяемого параметрами директивы. В случае использования угловых скобок файл ищется в директории библиотеки компилятора, а в случае записи имени файла в кавычках - в текущей директории.

Как правило, подставляемые таким образом файлы в свою очередь содержат дополнительные директивы, определяющие правила генерации подставляемого текста.

Самой простой директивой можно считать строку

#define < левая часть >    < правая  часть >  
    

В ней <левая часть> и <правая часть> - не что иное, как строки символов, разделенные пробелами. Левая часть определяет имя или прототип макроподстановки, а правая - соответствующие подставляемые значения, например:

#define TRUE (1)
#define FALSE (0)
#define NULL (0)
#define FOREVER while(l)  
    

После подобных макроопределений препроцессор Си везде, где встретит указанные имена, будет заменять их на соответствующие значения. Исключение составляют только символьные константы, внутри них замена не производится.

При подстановке могут использоваться параметры прототипа макроопределения. Используя такую директиву, можно определить полезные операции над отдельными битами и группами бит (параметры в правой части заключены в круглые скобки):

/* Manipulation of single bits: */
/* (b) is the bit to be manipulated (0..?, rvalue) */
/* (w) is the value who's bits are to be altered (LVALUE) */
/* (v) is the value who's bits are to be tested (rvalue) */
#define BITSET(b, w) ( (w) |= (1 << (b)) ) /* VOID type */
#define BITCLR(b, w) ( (w) &= ~(1 << (b)) ) /* VOID type */
#define ISBITSET(b, v) ( (v) & (1 << (b)) ) /* BOOL type */
#define ISBITCLR(b, v) (~(v) & (1 << (b)) ) /* BOOL type */
#define GETBITS(b, n, v) ( ((v) >> (b)) & ( (1 << (n)) – 1))
#define PUTBITS(b, n, v, x) ( (((x) & ((1 << (n)) – 1)) << (b) ) \
        | ( (v) & ~(((1 << (n)) - 1) << (b))) )  
    

Директивами #if (#ifdef, #ifndef), #else, #endif можно гибко управлять подстановкой текста, включая или исключая из него требуемые фрагменты. При этом директивы #define и #undef позволяют управлять предысторией процесса, устанавливая или сбрасывая установки объектов макроподстановки.

Препроцессор активно используется в проектировании модулей программной системы. Так, для большинства пользователей модуля не представляет интереса способ реализации экспортируемых им функций, но для работы с ним необходимо иметь прототипы вызовов и определения собственных типов (структур) этого модуля.

4.2. h-файлы и программные модули

Для этой цели в языке Си предусмотрено использование заголовочных файлов (h-file). Такой файл содержит описания заголовков (прототипов) процедур и описания пользовательских типов, использующихся при обращении к этим процедурам. В состав программы пользователя подобный заголовочный (header) файл включается с помощью директивы #include. Имя подобного файла обычно совпадает с именем модуля.

Знание состава h-файла модуля позволяет программистам-пользователям писать свои программы не отвлекаясь на то, как соответствующая функция будет реализована в модуле. Таким образом удается распараллелить между несколькими разработчиками написание программ большой системы. Им оказывается достаточным договориться о прототипах вызовов и поддерживать в актуальном состоянии библиотеку h-файлов системы.

Для средней системы (50-100 тысяч строк программного текста) обычно оказывается достаточно поддерживать в процессе разработки три уровня заголовочных файлов. Нижний уровень соответствует программным модулям. Промежуточный - функциональным областям (ФО). Функциональная область - это группа модулей, отвечающих за реализацию некоторой локальной функции системы, например расчет местоположения, планирование маневра, управление двигателем и т.п. Верхний уровень (обычно не более двух файлов) содержит определения типов и констант, общих для всей системы. В том числе в одном из файлов верхнего уровня могут вводиться принятые в проекте соглашения об именах (см. определения типа BITSET). Во втором - описания типов данных: структуры, перечислимые типы, именные константы, которые являются общими для всего проекта в целом.

В ряде случаев можно порекомендовать структуру, условно называемую 2+2 (два плюс два). Ее идея в том, что на каждую функциональную область создается 2 h-файла (заголовочные файлы отдельных модулей не создаются). Первый - внешний, содержит экспортируемые всей функциональной областью определения типов, констант и заголовков процедур. Второй - внутренний, используется только для локальных для данной функциональной области объектов.

Таким образом, при разработке кода модуля используются два уровня определений: собственные и внешние. Внешние включают общесистемные имена и имена, импортируемые из других функциональных областей. Если к этому добавить еще и соглашения о том, чтобы все импортируемые имена имели префикс, уникальный для каждой ФО, то чтение (анализ) текстов программных кодов существенно упрощается.

< Лекция 4 || Лекция 5: 12345 || Лекция 6 >
Ильдус Кучкаров
Ильдус Кучкаров
Россия, Санкт-Петербург, СПбГУ
Антон Конычев
Антон Конычев
Россия, Москва, МГУПИ, 2013