книги / Практикум по программированию на языке Си
..pdfРезультаты выполнения программы:
Repetitio est mater studiorum!
Похожее по результату вывода на экран решение задачи 02-21 можно получить, используя автоматически выполняемую компилятором конкатенацию строк, разделенных в тексте программы обобщенными символами пробелов.
/* 02_21_1.c - конкатенация строковых констант */ #include <stdio.h>
int main ()
{
puts("Repetitio " "est "
"mater " "studiorum!"
); return 0;
}
Результаты выполнения программы:
Repetitio est mater studiorum!
ЗАДАНИЕ. Обратите внимание на выравнивание слов по правому краю. Если такое выравнивание выполнить в предыдущей программе 02_21.с, то в результирующем тексте появятся лишние пробелы. Попробуйте проделать это на ЭВМ.
ЗАДАЧА 02-22. Чтобы убедиться в правильности понимания роли управляющих символов в теле строк, выведите на экран дисплея таблицу из всех служебных слов языка Си, размещая в каждой строке дисплея не более четырех слов. Используйте в программе только одно обращение к функции puts(). Условие: строки текста программы не должны быть длиннее 60 символов.
/* 02_22.c – редактирующие символы в строке */ #include <stdio.h>
int main ()
{
puts("auto\t\tdouble\t\tint\t\tstruct\n"
61
"break\t\telse\t\tlong\t\tswitch\n"
"case\t\tenum\t\tregister\ttypedef\n"
"char\t\textern\t\treturn\t\tunion\n"
"const\t\tfloat\t\tshort\t\tunsigned\n"
"continue\tfor\t\tsigned\t\tvoid\n"
"default\t\tgoto\t\tsizeof\t\tvolatile\n"
"do\t\tif\t\tstatic\t\twhile\n"
); return 0;
}
Результаты выполнения программы:
auto |
double |
int |
struct |
break |
else |
long |
switch |
case |
enum |
register |
typedef |
char |
extern |
return |
union |
const |
float |
short |
unsigned |
continue |
for |
signed |
void |
default |
goto |
sizeof |
volatile |
do |
if |
static |
while |
Обратите внимание на то, что символы табуляции поставлены с учетом длины предшествующего слова, например, после do перед if их два, а после continue перед for – один.
Коротко о важном
По материалам темы сформулированы следующие утверждения. В конце большинства из них приведены номера программ, подтверждающих справедливость утверждений.
!В отличие от puts() функция printf() не обеспечивает автоматического перехода к новой строке (01_03.с, 01_04_1.с, 02_01.с).
!Несоответствие спецификации преобразования типу выводимого значения приводит к непредсказуемым результатам на этапе выполнения программы (02_01_1.с, 02_01_2.с).
!Если символы форматной строки функции printf() не входят в представление спецификации преобразования, то они выводятся в выходной поток (02_01.с, 02_02.с).
62
!Для символа можно получить (вывести) числовое значение его кода и его изображение (02_03.с).
!Отдельная символьная константа, записанная без префикса L, имеет тип int.
!Арифметическое значение кода символа со значением из диапазона 128–255 воспринимается при выводе по спецификации %d как отрицательное число (02_03_1.с).
!Размеры участков памяти, выделяемой для вещественных констант, зависят только от их типа и не зависят от количества цифр в их записи в тексте программы (02_04.с, 02_04_1.с).
!Независимо от числа знаков в представлении константы число значащих цифр в ее внутреннем коде определяется только ее ти-
пом (02_05.с, 02_05_1.с).
!Спецификации %f и %e без указания точности представления используют точность, предусмотренную умолчанием
(02_05_1.с, 02_05_2.с).
!Применение разных спецификаций преобразования позволяет выводить одно и то же числовое значение с разными основаниями систем счисления (02_06.с).
!Одно и то же целое значение можно представить в тексте программы, используя разные основания систем счисления
(02_07.с).
! При выводе целого значения основание системы счисления определяется спецификацией преобразования (02_06.с,
02_07.с).
!При записи целой константы ей можно приписать тип, используя соответствующий суффикс (02_08.с).
!Предельное значение беззнакового целого в два раза превышает максимальное значение целого того же типа (02_09.с).
!Роль перечислений – ввести осмысленные запоминаемые названия целочисленных констант (02_10.с, 02_11.с).
!Размер памяти, выделяемой для отдельного символа, совпадает с размером числа типа int (02_12.с).
!Для представления особых символов (кавычки, апостроф, пусто, перевод строки) используйте эскейп-последовательности
(02_12_1.с, 02_12_2.с, 02_12_3.с, 02_13.с).
!Для представления неграфических символов используйте эскейппоследовательности (02_14.с).
63
!Набор эскейп-последовательностей для неграфических символов фиксирован и не может быть расширен (02_14_1.с).
!Коды символов '\0' и '0' не совпадают (02_15.с).
!В эскейп-последовательностях с числовыми кодами не применимы десятичные цифры (02_16.с).
!Код мультибайтовой (мультисимвольной) константы – это конкатенация шестнадцатеричных кодов входящих в нее символов
(02_17.с, 02_18.с).
!В мультибайтовую (мультисимвольную) константу не может входить более четырех символов (02_18_1.с).
!Строка из 1-го символа не есть этот символ (02_19.с).
!Строка из k символов типа char занимает в памяти (k+1) байт
(02_20.с).
!Строка из k символов расширенного набора (тип wchart_t) занимает в памяти 4#(k+1) байт (02_20.с).
!В тексте программы разрешены переносы строковой константы с помощью символа '\' (02_21.c).
!Строки, разъединенные в тесте программы обобщенными пробелами, конкатенируются (02_21_1.с).
!Для управления размещением текста на экране (при выводе на печать) в строке можно использовать управляющие символы '\t', '\n' (02_22.c).
Тема 3
Знакомство с препроцессором
Препроцессор не знает Си.
А. Фьюэр. Задачи по языку Си
Основные вопросы темы
!Препроцессорная директива #include.
!Препроцессорная сборка программы из синтаксически "незавершенных" фрагментов.
!Использование директивы #include во включаемых текстах.
!Препроцессорные "подстановки" директивой #define.
!Назначение директивы #undef.
!Предельные значения данных базовых типов.
!Защита текста от повторных включений.
!Управление включением текста в программу.
!Полезные макроопределения.
3.1. Включение текстов из файлов
Чтобы хорошо понять основное назначение препроцессора, недостаточно уметь использовать его директивы и знать, какое влияние оказывает их применение на функциональность создаваемого исполнимого модуля программы. Очень полезно увидеть результаты выполнения препроцессорной обработки программы, включающей директивы препроцессора.
Чтобы препроцессором обработать текстовый файл (например, с именем "testProg.c") и записать результат обработки в другой
65
текстовый файл (например, в файл preText.i), нужно обратиться к компилятору DJGPP со следующей командой:
>gcc -E testProg.c -o preText.i
Здесь ключ –E определяет задачу обработки файла с именем testProg.c. Ключ –o вводит имя файла с результатом препроцессорной обработки.
Используя команду, начнем эксперименты с директивой
#include. Директива #include имеет две формы. Первая форма
#include <имя_файла> /*имя в угловых скобках */
нужна для включения в программу текста файла из стандартного системного каталога. Обычно такой файл, называемый заголовочным, содержит описания библиотечных средств, используемых в программе. Вторая форма директивы
#include "имя_файла" /*имя в кавычках*/
предназначена для включения текста файла из любого каталога, причем поиск этого файла начинается в текущем каталоге.
Следующая задача иллюстрирует независимость препроцессорных средств от смысла того текста, который обрабатывается препроцессором.
ЗАДАЧА 03-01. Подготовьте небольшой текстовый файл и "программу" из одной препроцессорной директивы #include, включающей текст из этого файла. Оцените результат препроцессорной обработки этой "программы".
Разместим в файле 03_01.txt следующий текст:
/* 03_01.txt – текст без Си-операторов */ "Тривиальный алгоритм обычно лучше подходит для иллюстрации нюансов определения языка или структуры программы."
Б. Страуструп
Подготовим такую псевдопрограмму в файле 03_01.с:
/* 03_01.c – включение текста препроцессором */ #include "03_01.txt"
66
Выполним препроцессорную обработку, используя команду:
>gcc -E 03_01.c –o 03_01.i
Вфайле 03_01.i получим следующий текст:
#1 "03_01.c"
#1 "03_01.txt" 1
"Тривиальный алгоритм обычно лучше подходит для иллюстрации нюансов определения языка или структуры программы."
Б.Страуструп
# 2 "03_01.c" 2
Здесь строка «# 1 "03_01.c"» отмечает начало препроцессорной обработки. Строка «# 1 "03_01.txt" 1» содержит имя файла, из которого прочитывается текст. Далее размещен текст из этого файла 03_01.txt, и все завершает последовательность «# 2 "03_01.c" 2» – конец препроцессорной обработки.
Обратите внимание, что препроцессорная директива, вводимая размещенным в начале строки символом #, за которым следует пробел, является пустой директивой и никак не влияет на последующую обработку компилятором полученного текста. Таким образом, "выходом" или результатом препроцессорной обработки нашей псевдопрограммы 03_01.с можно считать именно текст высказывания Б. Страуструпа из файла 03_01.txt. В результат не перенесена и строка комментария (первая строка) из файла 03_01.txt.
ЗАДАЧА 03-02. Разместите текст небольшой программы в двух текстовых файлах "начало" и "конец". Выполните препроцессорную "сборку" программы и убедитесь в ее работоспособности.
Файл первый:
/* 03_02a.txt - начало программы */ #include <stdio.h>
int main()
{puts("Сообщение из начала программы.");
Файл второй:
67
/* 03_02b.txt - конец программы */ puts("Сообщение из окончания программы."); return 0;
}
Тексты обоих файлов являются синтаксически незавершенными, т.е. их невозможно компилировать в отдельности. В третьем файле того же каталога разместим "программу":
/* 03_02.c - программа из частей в двух файлах */ #include "03_02a.txt"
#include "03_02b.txt"
Команда на препроцессорную обработку:
>gcc -E 03_02.c -o 03_02.i
Результаты препроцессорной обработки (в файле 03_02.i):
#1 "03_02.c"
#1 "03_02a.txt" 1
#1 "c:/djgpp/include/stdio.h" 1 3
#1 "c:/djgpp/include/sys/djtypes.h" 1 3
...........................
Текст из файла stdio.h и включаемых в него файлов
...........................
#2 "03_02a.txt" 2
int main()
{puts("Сообщение из начала программы.");
#2 "03_02.c" 2
#1 "03_02b.txt" 1
puts("Сообщение из окончания программы.");
return 0;
}
# 3 "03_02.c" 2
Если не считать "пустых строк", обозначаемых символом # в первой позиции с последующим пробелом, то получен правильный текст программы на языке Си. В нем много "лишнего" для нашей маленькой программы за счет текста из стандартного заголовочного файла stdio.h. Выше мы обозначили этот текст многоточием.
68
Файл 03_02.i можно откомпилировать, используя такую директиву:
>gcc 03_02.i –o test.exe
Предупреждения компилятора:
03_02a.txt: In function `main':
03_02a.txt:5: warning: This file contains more `{'s than `}'s.
03_02b.txt: At top level:
03_02b.txt:5: warning: This file contains more `}'s than `{'s.
Получили предупреждающее сообщение компилятора о том, что каждый файл 03_02a.txt и 03_02b.txt содержит не уравновешенное количество скобок '{' и '}'. (Мы это и сами знаем!) Поэтому запускаем исполнимый модуль test.exe на выполнение:
>test.exe>res
В автоматически формируемом файле результатов (res) получим:
Сообщение из начала программы. Сообщение из окончания программы.
Включаемый препроцессором файл может находиться в любом каталоге файловой системы. В этом случае директива должна содержать сведения о полном имени файла, т.е. имеет вид:
#include "путь к файлу"
ЭКСПЕРИМЕНТ. Поместите файлы 03_02a.txt и 03_02b.txt в произвольные каталоги, отличные от каталога, где размещен текст "собирающей" их программы 03_02.c. Модифицируйте текст 03_02.c, чтобы учесть новое размещение файлов, откомпилируйте и выполните программу.
Вот текст программы для конкретного размещения файлов:
69
/* 03_02_1.c - программа из частей в другом каталоге */
#include "c:\1_c\practicum\programs\03_02a.txt" #include "c:\1_c\practicum\programs\03_02b.txt"
Обратите внимание, что для включаемых файлов из другого каталога указан полный путь, начиная от имени диска.
Включаемый препроцессором файл в свою очередь может содержать директивы #include, включающие другие файлы. Например, из текста файла stdio.h или из результатов препроцессорной обработки программы 03_02.c видно, что файл stdio.h содержит включение файла djtypes.h (см. текст файла 03_02.i на с. 69).
ЗАДАНИЕ. Придумайте содержательный пример с такой же "матрешкой" включений текстов из файлов.
3.2. Замены (подстановки) в тексте
Изучим простейшие возможности препроцессорной директивы
#define, которая позволяет вводить препроцессорные идентификаторы и определяет их значения, т.е. связывает с препроцессорным идентификатором "строку замещения". Формат директивы:
#define имя строка_замещения
Здесь имя – вводимое директивой имя препроцессорного идентификатора, строка_замещения – последовательность символов. Ее окончанием считается код начала новой строки (включаемый при наборе текста программы по нажатию клавиши "ENTER".)
ЗАДАЧА 03-03. Определите с помощью директивы #define
препроцессорный идентификатор PI и свяжите с ним изображение константы 3.1415. Выведите на печать значение константы, используя обозначение PI.
/* 03_03.c - #define – именованные константы */ #include <stdio.h>
#define PI 3.14159 int main()
70