Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Практикум по программированию на языке Си

..pdf
Скачиваний:
25
Добавлен:
12.11.2023
Размер:
3.53 Mб
Скачать

#define QUADR(A,B,C,X) (A)*(X)*(X) + (B)*(X) + (C) #define DISPLAY(EXPRESSION, FORMAT) \

printf(#EXPRESSION"="#FORMAT,EXPRESSION) int main()

{

DISPLAY(QUADR1(2,1,3,5-5),%d\n);

DISPLAY(QUADR(2,1,3,5-5),%d\n); return 0;

}

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

.....

int main()

{

printf("QUADR1(2,1,3,5-5)""=""%d\n",

2 * 5-5 * 5-5 + 1 * 5-5 + 3 ) ; printf("QUADR(2,1,3,5-5)""=""%d\n",

( 2 )*( 5-5 )*( 5-5 ) + ( 1 )*( 5-5 ) + ( 3 ) ); return 0;

}

Результаты выполнения программы:

QUADR1(2,1,3,5-5)=-17

QUADR(2,1,3,5-5)=3

10.5.Расширение языка Си

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

431

(программирования) его задач. В качестве яркого примера следует обратить внимание на первые версии языка Си++. Его автор Бьерн Страуструп начал разработку Си++ с расширения языка Си, введя препроцессорные средства, "позволяющие выразить концепции модульности и параллельности" [17].

Рассмотрение многочисленных аспектов использования макросов для расширения произвольного языка программирования выходит за рамки нашего практикума, поэтому остановимся на основных возможностях препроцессора, применяемых для расширения языка Си. Точнее говоря, продолжим изучение этой тематики, так как рассмотренное выше применение макросов в качестве заменителей функций уже следует рассматривать как расширение языка программирования.

Известно, что язык Си неудобен для представления и обработки многомерных массивов с переменными размерами. Например, при необходимости выполнять матричные вычисления это в ряде случаев создает существенные затруднения, преодолеть которые можно разными способами. Одно из решений – разработка макросредств для удобного представления матриц и доступа к их элементам с использованием индексов, изменяющихся от 1, а не от 0, как это принято для массивов языка Си.

ЗАДАЧА 10-16. Напишите программу, формирующую и печатающую по строкам верхнюю треугольную целочисленную матрицу, каждый ненулевой элемент которой равен сумме номеров его строки и столбца. Размер матрицы nRow вводите с клавиатуры, ограничив его препроцессорной константой N_MAX.

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

// 10_16.c – макросредства для матричных вычислений

#include <stdio.h> #define N_MAX 10

/* Макрос для представления элемента матрицы */ #define MATR(I,J) _ARRAY[N_MAX*(I-1)+(J-1)]

/* Макрос для ввода значения с "приглашением" */ #define READ(VARIABLE,FORMAT) \

printf(#VARIABLE"="); scanf(#FORMAT,#VARIABLE);

432

// Макрос для "печати" элементов квадратной матрицы

#define PRINT(NAME,SIZE)

\

{ int

i, j;

\

for

(i = 1; i <= SIZE; i++)

\

{

printf("\n");

\

for

(j = 1; j <= SIZE; j++)

\

printf(#NAME"(%d,%d)=%d\t",i,j,NAME(i,j)); \

}}

 

 

int main()

 

{

MATR(N_MAX,N_MAX+1);

 

int

 

int

nRow;

 

int

i, j;

 

do {

printf("\nInput size (0 < nRow < %d):\n",N_MAX); printf("Input size (0 < nRow <= %d):\n",N_MAX); READ(nRow,%d); }

while (0 >= nRow || nRow > N_MAX); for (i = 1; i <= nRow; i++)

for (j = 1; j <= nRow; j++) MATR(i,j) = i>j ? 0 : (i+j);

PRINT(MATR,nRow);

return;

}

Результаты выполнения программы:

Input size (0 < nRow <= 10): nRow=-6<ENTER>

Input size (0 < nRow <= 10): nRow=12<ENTER>

Input size (0 < nRow <= 10): nRow=4<ENTER>

MATR(1,1)=2

MATR(1,2)=3

MATR(1,3)=4

MATR(1,4)=5

MATR(2,1)=0

MATR(2,2)=4

MATR(2,3)=5

MATR(2,4)=6

MATR(3,1)=0

MATR(3,2)=0

MATR(3,3)=6

MATR(3,4)=7

MATR(4,1)=0

MATR(4,2)=0

MATR(4,3)=0

MATR(4,4)=8

В программе три макроса. MATR() служит для представления матрицы. Реально матрица реализована с помощью одномерного

433

массива _ARRAY[] с фиксированным именем. Макровызовы MATR() используются как при определении массива, так и при обращении к его элементам. Тип элементов матрицы (и соответствующего одномерного массива) заранее не фиксирован и задается в определении матрицы. В нашей задаче выбран тип int, что учтено в макросе PRINT(). Обращение к элементу матрицы реализуется с помощью макровызова MATR(i,j), причем нумерация строк и столбцов с 1 и до nRow. Имя одномерного массива _ARRAY "накрепко" связано с макросом. Поэтому при необходимости использования в программе нескольких матриц нужно вводить для каждой матрицы свое макроопределение.

Макроопределение PRINT() "настроено" на печать только целочисленных элементов квадратных матриц, представленных в виде имя_матрицы(i,j), т.е. из макроопределения PRINT() выполняется обращение к макроопределению MATR(), когда вместо NAME подставлен фактический параметр MATP.

В первой строке тела функции main() макрообращение MATR(N_MAX,N_MAX+1) использовано в определении массива с элементами типа int. При подстановке формируется строка:

int _ARRAY[N_MAX*(N_MAX-1)+(N_MAX+1-1);

определяющая одномерный массив с N_MAX*N_MAX элементами, пригодный для представления квадратных матриц с размерами от 1 на 1 до N_MAX на N_MAX.

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

ЗАДАНИЕ. Выполните препроцессорную обработку программы 10_16.c и проанализируйте полученный текст. Проследите за последовательностью препроцессорных подстановок.

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

434

ЗАДАЧА 10-17. Определите в виде макроса шаблон функций для суммирования значений элементов одномерного массива. Параметр макроопределения должен определять тип элементов суммируемого массива и, естественно, тип возвращаемого функцией значения суммы. Параметры формируемой макроопределением функции – одномерный массив и количество суммируемых элементов этого массива. Используя в макрообращениях имена разных типов, получите формируемые препроцессором тексты определений функций.

/*

10_17.c - макрос-шаблон функций */

#define template_summa(type) type

\

summa_##type(type array[], int n)

\

{

type res=0;

\

 

int i;

\

 

for (i=0; i <n; i++)

\

 

res+=array[i];

\

}

return res;

\

 

 

/* Обращения к макросу (макровызовы): */ template_summa(double) template_summa(long)

Выполним препроцессорную обработку, помещая результат в текстовый файл "pre":

>gcc –E 10_17.c>pre<ENTER>

Результат препроцессорной обработки (изменено размещение текста по строкам):

# 1 "10_17.c"

double summa_double ( double array[], int n)

{double res=0; int i; for (i=0; i <n; i++) res+=array[i];

return res;

}

long summa_long ( long array[], int n)

435

{long res=0; int i; for (i=0; i <n; i++)

res+=array[i]; return res;

}

Обратите внимание, как макровызов template_summa(double) формирует имя функции summa_double, т.е. название типа включается в имя формируемой функции.

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

ЗАДАЧА 10-18. Определите и инициализируйте массивы с элементами типа double и long. Используя функции, созданные с помощью макроса template_summa(), вычислите суммы элементов массивов.

/*

10_18.c – макрогенерация текстов функций */

/*

Макрос-шаблон функций */

\

#define template_summa(type) type

summa_##type(type array[], int n)

\

{

type res=0;

\

 

int i;

\

 

for (i=0; i <n; i++)

\

 

res+=array[i];

\

}

return res;

\

 

 

#include <stdio.h>

/* Обращения к макросу (макровызовы): */ template_summa(double) template_summa(long)

int main()

{

double dar[]={1.0, 2.0, 3.0, 4.0, 5.0, 6.0};

436

long lar[]={1,3,5,7,9,2,4,6,8,10}; double sumD;

long sumL; sumD=summa_double(dar,sizeof(dar)/sizeof(dar[0])); sumL=summa_long(lar,sizeof(lar)/sizeof(lar[0])); printf("\nsumma_double=%e",sumD); printf("\nsumma_long=%ld",sumL);

return 0;

}

Результат выполнения программы:

summa_double=2.100000e+01 summa_long=55

ЗАДАЧА 10-19. Напишите макроопределение шаблона функций для перестановки (обмена) значений переменных, адресуемых фактическими параметрами создаваемой функции. Тип параметров создаваемой шаблоном функции задается параметром макроса. Используя в макровызовах имена разных типов, получите формируемые препроцессором тексты определений функций.

/* 10_19.c - шаблон функций для перестановки значений переменных */

#define

template_swap(type) void

\

swap_##type(type * a, type * b)

\

{

temp;

\

type

\

temp

= *a;

\

*a

= *b;

\

*b

= temp;

\

}

 

 

/* Обращения к макросу (макровызовы): */

template_swap(double)

template_swap(long)

Выполним препроцессорную обработку, помещая результат в текстовый файл "pre":

437

>gcc –E 10_19.c>pre<ENTER>

Результат препроцессорной обработки (изменено размещение текста по строкам):

# 1 "10_19.c"

void swap_double ( double * a, double * b)

{double temp; temp = *a;

*a

=

*b;

*b

=

temp;

}

void swap_long ( long * a, long * b)

{long temp; temp = *a;

*a

=

*b;

*b

=

temp;

}

ЗАДАЧА 10-20. Для иллюстрации возможностей предложенного макроопределения шаблона функций для обмена значений переменных определите переменные типа double и переменные типа long и обменяйте их значения с помощью обращения к соответствующим функциям, сформированным макроопределением.

/*

10_20.c - перестановка значений переменных */

/*

шаблон

функций для перестановки значений

 

переменных */

\

#define

template_swap(type) void

swap_##type(type * a, type * b)

\

{

type

temp;

\

 

\

 

temp

=

*a;

\

 

*a

=

*b;

\

}

*b

=

temp;

\

 

 

 

 

/* Обращения к макросу (макровызовы): */ template_swap(double)

438

template_swap(long)

#include <stdio.h> int main()

{

double a=3.1415, b=2.7182; long aa=12345, zz=98765; swap_double(&a, &b); swap_long(&aa, &zz); printf("\na=%e b=%e",a,b);

printf("\naa=%ld zz=%ld",aa,zz); return 0;

}

Результат выполнения программы:

a=2.718200e+00

b=3.141500e+00

aa=98765

zz=12345

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

Коротко о важном

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

!Любой идентификатор является неопределенным для препроцессора, пока он не использован в директиве #define (10_01.с).

!Текст, включаемый директивой #include, в свою очередь может содержать препроцессорные директивы (10_01.с, 10_02.с и

др.).

!Текст, включаемый директивой #include, может содержать такую же директиву #include, что позволяет выполнять рекурсивную обработку на уровне препроцессора (dom.txt, domPrint.txt,

10_02.с – 10_04.с).

!Препроцессор позволяет "фильтровать" текстовую информацию, выбирая специальным образом отмеченные фрагменты

(10_01.с, 10_02.с, 10_03.с).

439

!Логические выражения в препроцессорных директивах позволяют управлять количеством использований фрагментов исходного текста (10_01.с – 10_03.с).

!Препроцессорные директивы могут использоваться для обработки произвольного текста, а не только текста программ на языке Си (10_02.с, 10_03.с, 10_04.с).

!Изменение препроцессорных идентификаторов, входящих в проверяемые препроцессором логические выражения, позволяют гибко регулировать количество включений фрагментов в результирующий текст (10_02.с, 10_03.с).

!При рекурсивном применении директивы #include должен быть предусмотрен механизм завершения рекурсии (dom.txt,

10_03.с, Fable.txt, 10_04.с).

!Препроцессорная настройка программы может быть изменена только при ее повторной компиляции (10_05.с, 10_06.с и др.).

!Нельзя определить параметр функции как многомерный массив с изменяемыми размерами. Препроцессорные подстановки позволяют настраивать функцию на обработку многомерного массивапараметра с нужными размерами (10_06.с, 10_07.с).

!Препроцессорная операция #, использованная перед именем параметра в строке замещения макроопределения, предусматривает заключение в кавычки аргумента при макроподстановке

(10_08.с, 10_09.с, 10_10.с и др.).

!Препроцессорная операция ##, использованная в строке замещения макроса между лексемами, обеспечивает их конкатенацию при макроподстановке (10_10.с, 10_17.с, 10_19.с).

!Если строка замещения содержит несколько операторов языка Си, то их нужно обрамлять фигурными скобками (10_10.с –

10_14.с и др.).

!Имена параметров макроса в строке замещения заключайте в круглые скобки (10_15.с).

!Строка замещения препроцессорного макроопределения может содержать весьма сложную последовательность операторов язы-

ка Си (10_11.с – 10_14.с, 10_16.с – 10_20.с).

!Значения препроцессорных логических выражений используются в директивах #if (10_01.txt, dom.txt и др.).

!Препроцессорные директивы #ifdef и #ifndef проверяют только препроцессорную определенность и, соответственно, неопреде

440