книги / Практикум по программированию на языке Си
..pdfА это веселая птица-синица,
Которая |
часто ворует пшеницу, |
|
Которая |
в темном |
чулане хранится |
В доме, |
построил |
Джек. |
Который |
||
Вот кот, |
пугает и |
ловит синицу, |
Который |
||
Которая |
часто ворует пшеницу, |
|
Которая |
в темном |
чулане хранится |
В доме, |
построил |
Джек. |
Который |
ЗАДАНИЕ. Выпишите на листе бумаги последовательность строк, формируемых при препроцессорной обработке файла domPrint.txt. Начало текста:
puts("\t\tМаршак…); puts("Вот дом,\n"
"который построил Джек.\n");
Решающую роль при рекурсивном использовании директивы
#include (как и во всех рекурсиях) играет наличие условия завершения рекурсии. В нашем примере решения задачи 10-03 используется проверка значения препроцессорного идентификатора _T. Начиная с фиксированного значения _Т (равного 5 в нашем примере), текст и сама директива #include не должны больше включаться, и цикл препроцессорной обработки прерывается.
ЗАДАЧА 10-04. Чтобы оценить размеры бедствия, которое грозит произойти при отсутствии условия завершения рекурсии, оформите в виде препроцессорного текста леденящую душу историю о попе и его собаке. (Надеемся, что читатель знаком с этим фольклорным шедевром.)
Файл с решением задачи:
// Fable.txt - бесконечная препроцессорная рекурсия У попа была собака, поп ее любил.
411
Она украла кусок мяса, поп ее убил.
И в землю закопал, и на могиле написал: #include " Fable.txt"
Тестовая программа:
/* 10_04.c - обработка некорректного текста */ #include " Fable.txt"
Директива для препроцессорной обработки:
>gcc -E 10_04.c>pre
Часть результатов из сформированного препроцессором файла
"pre":
#1 "10_04.c"
#1 " Fable.txt" 1
Упопа была собака, поп ее любил. Она украла кусок мяса, поп ее убил.
И в землю закопал, и на могиле написал:
# 1 " Fable.txt" 1
Упопа была собака, поп ее любил.
Она украла кусок мяса, поп ее убил.
................
# 1 " Fable.txt" 1
У попа была собака, поп ее любил. Она украла кусок мяса, поп ее убил.
И в землю закопал, и на могиле написал:
#6 " Fable.txt" 2
..................
#6 " Fable.txt" 2
#2 "10_04.c" 2
Теоретически бесконечный цикл при реальном исполнении программы завершился – компилятор исчерпал свои ресурсы – создал текстовый файл pre (препроцессор из DJGPP создал файл размером 66267 байт) и прекратил циклическое обращение к файлу
Fable.txt.
412
10.3.Препроцессорная "настройка" программ
Директива #define широко используется для внесения в программу заранее запланированных достаточно разнообразных изменений. Например, можно настраивать программу, выполняющую численный расчет, на использование разных типов арифметических (вещественных) данных.
ЗАДАЧА 10-05. Вычислите приближенное значение числа e по формуле Тейлора:
ex = 1+ |
|
x |
+ |
x2 |
+ |
x3 |
+ L+ |
xn |
+ L, |
|
1! |
2! |
3! |
n! |
|||||||
|
|
|
|
|
прекратив вычисления, как только прибавление очередного слагаемого оставит без изменения предыдущее значение суммы (достигнут "машинный нуль"). Проведите вычисления, исполь-
зуя типы float, double, long double.
В нашем случае x=1, т.е. оценивается значение числа "е". Для вычисления с удвоенной точностью (long double) задачу решает следующая программа:
/* 10_05.c - "настройка" вычислительной программы */ #define REAL long double
#define FORMAT "%27.20Le" #include <stdio.h>
int main()
{
REAL result, member, summa;
int k = 0; /* количество итераций */ member = summa = 1.0;
do { /*цикл с постусловием */ result = summa;
k++;
member/=k;
summa += member;
}
while(summa > result);
printf("Оценка числа е: "FORMAT"\n", result); printf("Число итераций: %d\n", k);
413
printf("Машинный нуль: "FORMAT"\n", member); return 0;
}
Результаты выполнения программы:
Оценка числа е: |
2.71828182845904523543e+00 |
Число итераций: |
21 |
Машинный нуль: |
1.95729410633912612322e-20 |
ЗАДАНИЕ. Посмотрите на результаты препроцессорных подстановок в форматную строку функции printf() значений идентификатора FORMAT. (Используйте команду >gcc –E…).
ЗАДАНИЕ. Изменяя в программе строки замещения для препроцессорных идентификаторов REAL и FORMAT, вычислите оценки числа e для типа float (спецификация %27.20e) и double (%27.20e).
В связи со сложностями реализации в классическом языке Си многомерных массивов с переменными размерами препроцессорные подстановки широко используются для "превращения" переменных размеров массивов в постоянные. Следующая задача иллюстрирует указанную возможность.
ЗАДАЧА 10-06. Напишите функцию для транспонирования квадратной матрицы, передаваемой в функцию через параметры в виде двумерного массива. Размеры матрицы задавайте препроцессорной константой. В основной программе определите и инициализируйте двумерный массив, представляющий матрицу, обратившись к функции, транспонируйте матрицу и распечатайте ее по строкам.
// 10_06.c – задание размеров многомерных массивов
#include <stdio.h> #define N 3
void transp(float d[N][N])
{
float r;
414
int i, j;
for (i = 0; i < N - 1; i++) for (j = i + 1; j < N; j++)
{ r = d[i][j]; d[i][j] = d[j][i]; d[j][i] = r;
}
}
int main()
{
float x[N][N] = { 0, 1, 1, 2, 0, 1, 2, 2, 0 };
int i, j; transp(x);
for (i=0; i < N; i++)
{printf("\nline %d :",i+1); for (j = 0; j < N; j++)
printf("\t%f",x[i][j]);
}
return 0;
}
Результаты выполнения программы:
line 1 |
: |
0.000000 |
2.000000 |
2.000000 |
|
line |
2 |
: |
1.000000 |
0.000000 |
2.000000 |
line |
3 |
: |
1.000000 |
1.000000 |
0.000000 |
ЗАДАЧА 10-07. Определите функцию, присваивающую вещественные псевдослучайные значения из диапазона от 0 до 1 элементам прямоугольной матрицы X={xij}. Напишите функцию печати (вывода на дисплей) значений элементов прямоугольной матрицы. Размеры матрицы (n и m), а также двумерный массив элементов матрицы должны служить параметрами функций. Задав с помощью препроцессорных идентификаторов N и M размеры матрицы, определите в основной программе соответствующий массив ее элементов. Заполните матрицу и распечатайте значения ее элементов, используя вышеназванные функции.
/* 10_07.c – матрица с переменными размерами как параметр */
#include <stdio.h>
415
#include <stdlib.h> /* для rand(void), RAND_MAX */ #define N 3
#define M 4
void matrixPrint(int n, int m, float x[][M])
{
int i, j;
for (i=0; i < n; i++)
{printf("\nline %d :",i+1); for (j = 0; j < m; j++)
printf("\t%5.3f",x[i][j]);
}
}
int main()
{
void elementValues(int, int, float [][M]); float matrix[N][M]; elementValues(N,M,matrix); matrixPrint(N,M,matrix);
return 0;
}
void elementValues(int n, int m, float array[][M])
{
float r; int i, j;
for (i = 0; i < n; i++) for (j = 0; j < m; j++)
{r = (float)(RAND_MAX - rand())/RAND_MAX; array[i][j] = r;
}
}
Результаты выполнения программы:
line 1 |
: |
1.000 |
0.998 |
0.917 |
0.647 |
|
line |
2 |
: |
0.271 |
0.817 |
0.815 |
0.026 |
line |
3 |
: |
0.946 |
0.091 |
0.534 |
0.337 |
В функции elementValues() используется функция стандартной библиотеки int rand(), возвращающая целое значение в диапазоне от 0 до RAND_MAX. Это значение приводится к интервалу [0–1) и присваивается вспомогательной переменной r, а затем оче-
416
редному элементу матрицы. Функция rand() и препроцессорная константа специфицированы в заголовочном файле stdlib.h.
Особую роль препроцессорной настройки программы с многомерными массивами в качестве параметров функций можно продемонстрировать, убрав идентификатор M из заголовков функций, т.е. написав, например, такой заголовок:
void matrixPrint(int n, int m, float x[][])
Компилятор ответит:
10_07.c: In function `matrixPrint':
10_07.c:20: arithmetic on pointer to an incomplete type
Действительно, float X[][M] – это одномерный массив, каждый элемент которого есть массив из M элементов, а float X[][] воспринимается как незавершенный тип, не пригодный для использования.
Попытка заменить в списке параметров обозначение двумерного массива указателем на массив указателей float **x также не удается. Компилятор выдаст предупреждение:
10_04.c: In function `main':
10_04.c:28: warning: passing arg 3 of `matrixPrint' from incompatible pointer type
При исполнении будет выдано сообщение об ошибке (можете проверить).
Утешает лишь возможность применения в новых компиляторах целочисленного параметра функции для указания размера элементов массива-параметра другого параметра функции. Заголовок вида
void matrixPrint(int n, int m, float[][m])...
будет благополучно воспринят транслятором DJGPP, что соответствует современному Стандарту языка Си.
417
10.4.Макросредства препроцессора
ЗАДАЧА 10-08. Определите макрос для вывода на экран дисплея изображения и значения арифметического выражения с возможностью задания спецификации преобразования.
В пособии [3] приведено решение задачи для того случая, когда тип формируемого выражением значения заранее известен и фиксирован:
#defin PRINT(A) printf(#A "=%f",A)
В теме 3 (подразд. 3.4) приведены макросы для вывода изображений выражений и их значений типов int, float, double, long double. Эти макросы мы уже неоднократно использовали в программах из тем 4 – 9.
Более универсальное решение получается в том случае, когда при обращении к макросу можно указывать форматную строку (спецификацию преобразования). Задачу решает макрос в следующей программе:
/* 10_08.c - макроподстановки и операция # */ #include <stdio.h>
#define DISPLAY(EXPRESSION, FORMAT) \ printf(#EXPRESSION"="#FORMAT,EXPRESSION)
int main()
{
DISPLAY(33.22,%7.3f); return 0;
}
Результат препроцессорной обработки:
.....
int main()
{
printf("33.22""=""%7.3f", 33.22 ) ; return 0;
}
418
Результаты выполнения программы:
33.22= 33.220
Отметим еще раз роль препроцессорной операции # перед именем параметра макроса в строке замещения. Аргумент при подстановке помещен в кавычки. Обратите внимание, что обратная косая черта (\) позволяет переносить длинную строку, чем мы воспользовались, разместив строку замещения макроса на следующей строке после DISPLAY().
ЗАДАЧА 10-09. Используя предложенный в предыдущей задаче макрос, выведите на экран предельные значения разных арифметических типов, определенные в заголовочных файлах <limits.h> и <float.h>.
Для нескольких констант из float.h следующая программа решает сформулированную задачу:
// 10_09.c - макроподстановки и предельные значения
#include <stdio.h> #include <float.h>
#define DISPLAY(EXPRESSION, FORMAT) \ printf(#EXPRESSION"="#FORMAT,EXPRESSION)
int main()
{
DISPLAY(FLT_EPSILON,%e\n); DISPLAY(FLT_MIN,%e\n); DISPLAY(FLT_MAX,%e\n); DISPLAY(DBL_EPSILON,%le\n); DISPLAY(DBL_MIN,%le\n); DISPLAY(DBL_MAX,%le\n); return 0;
}
Результаты выполнения программы:
FLT_EPSILON=1.192093e-07
FLT_MIN=1.175494e-38
FLT_MAX=3.402823e+38
DBL_EPSILON=2.220446e-16
DBL_MIN=2.225074e-308
DBL_MAX=1.797693e+308
419
Отметьте особенности задания форматных строк с помощью аргументов макроса. В форматную строку-аргумент, кроме спецификации преобразования, помещен символ перехода на новую строку, изображаемый как '\n'.
Чтобы убедиться в строгости синтаксиса макроопределений, сделайте следующее.
ЭКСПЕРИМЕНТ. Отделите пробелом имя макроса DISPLAY от скобки, открывающей список параметров макроса. Результат лучше изучать, не прибегая к трансляции, а только выполнив препроцессорную обработку:
/* 10_09_1.c - ошибка в макросе */ //#include <stdio.h>
#define DISPLAY (EXPRESSION, FORMAT) \ printf(#EXPRESSION"="#FORMAT,EXPRESSION)
int main()
{
DISPLAY(33.22,%f); return 0;
}
Результат препроцессорной обработки:
.....
int main()
{
(EXPRESSION, FORMAT) printf(#EXPRESSION"="#FORMAT, EXPRESSION) (33.22,%f);
return 0;
}
В программе заключенная в скобки последовательность параметров воспринята как часть замещающей строки. DISPLAY считается препроцессорным идентификатором (не именем макроса). Остальное очевидно из результата препроцессорной обработки. В книге из-за размеров страницы строка, заменившая в результате препроцессор-
420