Макроподстановки средствами препроцессора

Лекция №26. Препроцессорная обработка

План:

1. Стадии и директивы препроцессорной обработки;

1. Замены в тексте;

2. Включение текстов из файлов;

3. Макроподстановки средствами препроцессора.

 

Стадии и директивы препроцессорной обработки

 

В интегрированную среду подготовки программ на С++ или в компилятор языка как обязательный компонент входит препроцессор. Назначение препроцессора – обработка исходного текста программы до её компиляции.

Препроцессорная обработка в соответствии с требованиями стандарта языка С++ включает несколько стадий, выполняемых последовательно. Конкретная реализация транслятора может объединять несколько стадий, но результат должен быть таким, как если бы они выполнялись последовательно:

- все системно-зависимые обозначения (например, системно-зависимый индикатор конца строки) перекодируются в стандартные коды;

- каждая пара символов ‘\’ и “конец строки” убираются, и тем самым следующая строка исходного файла присоединяется к строке, в которой находилась эта пара символов;

- в тексте распознаются директивы препроцессора, а каждый комментарий заменяется одним символом пустого промежутка;

- выполняются директивы препроцессора и производятся макроподстановки;

- ESC-последовательности в символьных константах и символьных строках, например, ‘\n’ заменяются на их эквиваленты (на соответствующие числовые коды);

- Смежные символьные строки конкатенируются, т.е. соединяются в одну строку.

Знакомство с перечисленными задачами препроцессорной обработки объясняет некоторые соглашения синтаксиса языка. Например, становится понятным смысл утверждений: каждая символьная строка может быть перенесена в файле на следующую строку, если использовать символ ‘\’ или “две символьные строки, записанные рядом, воспринимаются как одна строка”.

Рассмотрим подробно стадию обработки директив препроцессора. При её выполнении возможны следующие действия:

· Замена идентификаторов (обозначений) заранее подготовленными последовательностями символов;

· Включение в программу текстов из указанных файлов;

· Исключение из программы отдельных частей её текста (условная компиляция);

· Макроподстановка, то есть замена обозначения параметризованным текстом, формируемым препроцессором с учётом конкретных параметров (аргументов).

Для управление препроцессором, т.е. для задания нужных действий, используются команды (директивы) препроцессора, каждая из которых помещается на отдельной строке и начинается с символа #. Определены следующие препроцессорные директивы:

à Директива # define имеет несколько модификаций. Они предусматривают определение макросов или препроцессорных идентификаторов, каждому из которых ставится в соответствие некоторая символьная последовательность. В последующем тексте программы препроцессорные идентификаторы заменяются на заранее запланированные последовательности символов.

à Директива # include позволяет включать в текст программы текст из выбранного файла.

à Директива # undef отменяет действие команды # define, которая определила до этого имя препроцессорного идентификатора.

à Директива # if и её модификации # ifdef , # ifndef совместно с директивами # else , # endif , # elif позволяют организовать условную обработку текста программы. Условность состоит в том, компилируется не весь текст, а только те его части, которые так или иначе выделены с помощью перечисленных директив.

à Директива # line позволяет выполнять нумерацией строк в файле с программой. Имя файла и начальный номер строки указываются непосредственно в директиве # line.

à Директива # error позволяет задать текст диагностического сообщения, которое выводится при возникновении ошибок.

à Директива # pragma вызывает действия, зависящие от реализации.

 

Замены в тексте

 

Для замены идентификатора заранее подготовленной последовательностью символов используется директива:

# define идентификатор строка_замещения

Директива может размещаться в любом месте обрабатываемого текста, а её действие в обычном случае от точки размещения до конца текста. Директива, во-первых, определяет идентификатор как препроцессорный. В результате обработки все вхождения определённого командой # define идентификатора в текст программы заменяются строкой замещения, окончанием которой обычно служит признак конца той строки, где размещена команда # define. Символы пробелов, помещённые в начале и конце строки замещения, в подстановке не используются. Например:

Исходный текст

#define begin {

#define end }

void main ()

begin

операторы

end

Результат препроцессорной обработки

 

Void main ()

{

операторы

 }

В данном случае программист решил использовать в качестве операторных скобок идентификаторы begin, end. компилятор языка С++ не может обрабатывать таких скобок, и поэтому до компиляции препроцессор заменяет все вхождения этих идентификаторов стандартными скобками { и }. Соответствующие указания программист дал препроцессору с помощью директив #define.

С помощью команды #define удобно выполнять настройку программы. Например, если в программе требуется работать с массивами, то из размеры можно явно определять н на этапе препроцессорной обработки:

 

Исходный текст

#define K 40

void main()

{ int M[K] [K];

fload A[K], B[K] [K];

Результат препроцессорной обработки

Void main()

{ int M[40] [40];

fload A[40], B[40] [40];

Замены в тексте можно отменять с помощью директивы:

# undef идентификатор

После выполнения такой команды идентификатор для препроцессора становится неопределённым и его можно определять повторно.

 

Включение текстов из файлов

 

Для включения текста из файла используется команда # include, имеющая две формы записи:

#include <имя_файла> // имя в угловых скобках

#include “имя_файла” // имя в кавычках

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

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

#include <iostream.h>

Выполняя эту директиву, препроцессор включает в программу средства связи с библиотекой ввода-вывода. Поиск файла iostream.h ведётся в стандартных системных каталогах.

По принятому соглашению префикс .h приписывается тем файлам, которые нужно помещать в заголовке программы, т.е. до исполняемых операторов. Кроме такого в некоторой степени стандартного файла, каким является iostream.h, в заголовок программы могут быть включены любые другие файлы (стандартные или подготовленные специально).

Заголовочные файлы оказываются весьма эффективным средством при модульной разработке крупных программ, когда связь между модулями, размещёнными в разных файлах, реализуется не только с помощью параметров, но и через внешние объекты, глобальные для нескольких или всех модулей. Описания таких внешних объектов (переменных, массивов, структур и т.п.) помещаются в одном файле, который с помощью директив #include включается во все модули, где необходимы внешние объекты. В тот же файл можно включить и директиву подключения библиотеки функций ввода-вывода.

Заголовочный файл может быть, например, таким:

#include <iostream.h> // включение средств обмена

extern int ii, jj, ll;         // целые внешние переменные

extern fload aa, bb;      // вещественные внешние переменные

В практике программирования на С++ обычна в некотором смысле обратная ситуация. Если в программе используется несколько функций, то иногда удобно текст каждой из них хранить в отдельном файле. При подготовке программы пользователь включает в неё тексты используемых функций с помощью команды #include.

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

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

#include <iostrem.h>

#include “invert.cpp” // функция инвертирования строк

#include “conc.cpp” // функция соединения строк

void main()

{ char slovo[81], sp[81], c = ‘ ‘, *ptr = slovo;

sp[0] = ‘\0’;           // очистка массива для нового предложения

cout << “\nВведите предложение с точкой на конце:\n”;

do

    { cin >> slovo; // читается слово из входного потока  

       invert (slovo); //инвертировать слово

       c = slovo[0];  

// убрать точку в начале последнего слова:

       if (c == ‘.’) ptr = &slovo[1];

       if (sp[0] != ‘\0’) conc(sp,” \0”); // пробел перед словом

       conc(sp, ptr);     // добавить слово в предложение

     } while (c != ‘.’);    // конец цикла чтения

conc (sp, “.\0”);            // точка в конце предложения

cout << “\n” << sp;

}

В файле invert.cpp текст функции:

Void invert(char *e)

{ char s;

for (int m = 0; e[m] != ‘\0’; m++);

for (int i = 0, j = m-1; i < j; i++, j--)

    { s = e[i]; e[i] = e[j]; e[j] = s; }

}

В файле conc.cpp текст функции:

Void conc (char *c1, char *c2)

{ for (int m = 0; c1[m] != ‘\0’; m++);

// m – длина первой строки без символа ‘\0’

for (int i = 0; c2[i] != ‘\0’; i++) c1[m+1] = c2[i];

c1[m+i] = ‘\0’;

}

Возможный результат выполнения:

Введите предложение с точкой в конце:

А ШОРОХ ХОРОШ. <ENTER>

Вывод: А ХОРОШ ШОРОХ.

В программе в символьный массив slovo считывается из входного потока (с клавиатуры) очередное слово; sp – формируемое предложение, в конец которого всегда добавляется точка. Переменная char с – первый символ каждого инвертированного слова. Для последнего слова предложения этот символ равен точке. При добавлении этого слова к новому предложению точка отбрасывается с помощью изменения значения указателя ptr.

Использованы директивы #include, включающие в программу средства ввода-вывода и тексты функций инвертирования строки invert() и конкатенации строк conc(). Обратите внимание, что длина массива – первого из параметров функции conc() должна быть достаточно велика, чтобы разместить результирующую строку.

 

Макроподстановки средствами препроцессора

 

Макрос, по определению, есть средство замены одной последовательности символов другой. Для выполнения замен должны быть заданы соответствующие макроопределения. Простейшее макроопределение можно записать так:

#define идентификатор строка_замещения

Такая директива удобна, однако она имеет существенный недостаток – строка замещения фиксирована. Большими возможностями обладает следующее макроопределение с параметрами:

#define имя(список параметров) строка_замещения

Здесь имя – имя макроса (идентификатор), список_параметров – список разделённых запятыми идентификаторов. Между именем макроса и списком параметров не должно быть пробелов.

Классический пример макроопределение:

#define max(a,b) (a < b ? b : a)

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

max(X, Y)

заменяется выражением  

(X < Y ? Y : X)

а использование max ( Z , 4) приведёт к формированию ( Z < 4 ? 4 : Z )

Выводы:

При препроцессорной обработке исходного текста программы каждая строка обрабатывается отдельно. Напомню, что возможно “соединение” строк: если в конце строки стоит символ ‘\’, а за ним – символ перехода на новую строку ‘\n’, то эта пара символов исключается и следующая строка непосредственно присоединяется к текущей строке. Анализируя полученные строки, препроцессор распознаёт лексемы. Лексемами для препроцессора являются:

- лексемы языка С++;

- имена файлов;

- символы, не определённые иным способом.

  Аргументы вызова макроса – лексемы, разделённые запятыми. В последовательности лексем, образующих строку замещения, предусматривается использование двух операций – ‘#’ и ‘##’, первая из которых помещается перед параметром, а вторая – между любыми двумя лексемами. Операция ‘#’ требует, чтобы текст, замещающий данный параметр в формируемой строке, заключался в двойные кавычки. Например, для определения:

#define sm(zip) cout << #zip

обращение (макровызов) sm (сумма); приведёт к формированию оператора cout << “сумма”;

Операция ‘##’, допускаемая только между лексемами строки замещения, позволяет выполнять конкатенацию лексем, включаемых в строку замещения. Определение:

#define abc(a, b, c, d) a##(##b##c##d)

позволит сформировать выражение sin(x+y), если использовать макровызов abc ( sin , x ,+, y ).

 


Дата добавления: 2020-12-12; просмотров: 113; Мы поможем в написании вашей работы!

Поделиться с друзьями:




Мы поможем в написании ваших работ!