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

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

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

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

/* newHash.c – функция начинает хэш-цепочку и вводит переводы */

#include "readWrite.c"

void newHash(char * term, long hashValue)

{

long pos;

/* Выбрать свободную позицию для записи статьи: */ fseek(artiFile, 0L, SEEK_END);

pos = ftell(artiFile);

/* Записать в хэш-файл адрес статьи */ fseek(hashFile, hashValue, SEEK_SET); fprintf(hashFile, FORMAT, pos);

/* Ввести переводы термина и распечатать */ readWrite(term, pos);

return;

}

Комментарии в тексте функции с достаточной полнотой поясняют назначение ее операторов. Используются библиотечные функции для работы с файлами. Функция fseek() устанавливает позицию чтения/записи в конец файла articles (смещение 0L относительно SEEK_END). Функция ftell() позволяет определить в файле установленное текущее значение позиции чтения/записи. Полученное значение (переменная pos) затем сохраняется в соответствующей записи файла hash. Для этого функция fseek() перемещает позицию чтения/записи в нужное место (заданное значением hashValue) файла hash. После чего функция fprintf() записывает туда значение pos, используя константу FORMAT, определенную вне функции newHash(). Завершает выполнение вызов функции readWrite(), текст которой на препроцессорном уровне включается в текст newHash().

// readWrite.c – функция читает и сохраняет переводы

#ifndef READ_WRITE #define READ_WRITE

char rusTerm[LINE_MAX];

void readWrite(char * engTerm, long position)

561

{

/* position - позиция для записи статьи: */ fseek(artiFile, position, SEEK_SET); fprintf(artiFile, "%s\n", engTerm); printf("Input the translations or <ENTER>:\n");

/* Цикл для чтения и записи переводов*/ for (;;)

{ gets(rusTerm);

if (strlen(rusTerm) == 0) break; /* запись переводного эквивалента :*/

fprintf(artiFile, "%s\n", rusTerm);

}

fprintf(artiFile, FORMAT"\n", 0L); return;

}

#endif

Так как функция readWrite() используется в двух функциях "более высокого уровня" (см. рис 12.4), то ее текст защищен от повторных включений директивами #ifndef, #endif. Флажком для проверки включения служит препроцессорный идентификатор READ_WRITE. Через аппарат параметров в функцию передаются английский термин engTerm и позиция в файле articles, в которой нужно начинать записывать статью. После записи термина (используются уже рассмотренные библиотечные функции) функция printf() выводит "приглашение" пользователю на ввод переводов термина. После ввода каждого переводного эквивалента определяется его длина и если она равна нулю (нажата клавиша ENTER без ввода значащих символов), цикл чтения-записи переводов завершается. В противном случае функция fprintf() записывает очередной перевод в файл articles. Запись идет по строкам. После окончания ввода словарной статьи (всех переводов термина) в одну (последнюю) строку записывается значение 0L. При его записи используется препроцессорная константа FORMAT. Тем самым для нулевой константы 0L отводится 5 (в данном случае) позиций. Это позволяет в дальнейшем записать в эту строку (в этот участок файла articles) любой принятый в словаре адрес в диапазоне от 0 до 99999.

При наличии в файле articles хэш-цепочки статей, адресуемых найденным для английского термина хэш-значением, вызывается следующая функция:

562

// search.c – функция поиска термина в хэш-цепочке long search(char * engTerm, long address)

{

/* address - позиция начала хэш-цепочки */ long pos=address, k;

char line[LINE_MAX]; char *pLF=NULL; while (1)

{fseek(artiFile, pos, SEEK_SET); fgets(line, LINE_MAX, artiFile); pLF=strchr(line,'\n');

if (pLF != NULL) *pLF='\0';

if (strcmp(engTerm, line) == 0)

/* вернем начало словарной статьи: */ return pos;

while(1)

{pos = ftell(artiFile); fgets(line, LINE_MAX, artiFile);

if (strpbrk(line, "1234567890") != 0)

{k = atol(line);

if (k == 0L) /* вернем конец цепочки: */

return (-pos); pos = k;

break;

}

}/* end of while */ } /* end of while */

}

Параметры функции – английский термин и адрес начала хэшцепочки в файле articles. В теле два вложенных цикла. Во внешнем "перебираются" в файле articles словарные статьи из хэшцепочки. Для этого в каждой итерации позиция чтения/записи функцией fseek() устанавливается на начало статьи. Функция fgets() прочитывает в символьный массив line[] английский термин. Затем указатель pLF "настраивается" функцией strchr() на символ \n, а присваивание *pLF=\0 записывает в эту позицию терминальный символ. Необходимость этих действий уже объяснялась, она связана с различием между функциями fgets() и gets(). Функция gets() читает из входного потока только значащие символы и дополняет их при-

563

знаком конца строки '\0'. Функция fgets() считывает из файла значащие символы и признак '\n' и только после него ставит признак '\0'. Таким образом, термин из входного стандартного потока, прочитанный функцией gets(), и тот же термин из файла, прочитанный функцией fgets(), при сравнении библиотечной функцией strcmp() оказываются не равными, если не "укоротить" строку, прочитанную из файла.

Если функция strcmp() возвращает нулевое значение, то в хэшцепочке найдена соответствующая словарная статья и адрес ее начала (pos) возвращается как результат выполнения функции

search().

В противном случае в следующем (вложенном) цикле разыскивается либо начало следующей статьи, либо конец хэш-цепочки статей. Для этого из файла articles последовательно считываются строки с переводами до тех пор, пока в одной из них встретятся цифры. Числовое значение в строке может быть либо 0L – конец хэш-цепочки, либо адрес следующей статьи в хэш-цепочке. Проверку наличия цифр выполняет библиотечная функция strbrk(). Преобразование символьной записи числа (адреса) в числовое значение типа long осуществляет функция atol(). Если найден 0L (конец цепочки), функция search() возвращает адрес позиции (в конце словарной статьи), в которую нужно будет записать адрес начала новой словарной статьи. Если найден адрес продолжения хэш-цепочки, то оператором break прерывается только внутренний цикл (просмотр переводов), и во внешнем цикле начинается обработка следующей статьи.

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

/* display.c – функция вывода переводов термина */ void display(long position)

{

/* position - позиция начала словарной статьи */ long pos=position;

int i;

char line[LINE_MAX]; fseek(artiFile, pos, SEEK_SET); for(i=0; ; i++)

{fgets(line, LINE_MAX, artiFile);

if (strpbrk(line, "1234567890") != 0) return;

564

if ( i != 0) /* пропустить английский термин */ printf("\t%s",line);

}/* end of cycle */

}

Алгоритм функции display() схож с внутренним циклом из функции search(). Библиотечная функция fseek() устанавливает позицию чтения/записи в файле articles. Затем в цикле из файла по строкам считывается информация в символьный массив line[]. Если в очередной строке окажутся цифры, – цикл и вся программа завершаются. Так как в первой строке словарной статьи находится английский термин, то при печати он пропускается, выводятся только переводы – строки, при чтении которых параметр цикла i>0.

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

/* newArt.c – функция вводит переводы и дополняет хэш-цепочку */

#include "readWrite.c"

void newArt(char * term, long position)

{

long pos;

/* Перейти в конец файла статей: */ fseek(artiFile, 0L, SEEK_END);

/* Свободная позиция (адрес) для записи статьи: */ pos = ftell(artiFile);

/* Записать адрес в последнюю статью цепочки */ fseek(artiFile, position, SEEK_SET); fprintf(artiFile, FORMAT, pos);

/* Прочитать и записать в файл переводы: */ readWrite(term, pos);

}

Текст этой функции лишь немногим отличается от текста функции newHash(). Отличие состоит только в том, что запись позиции начала новой статьи осуществляется не в файл hash, а в файл articles. Учитывая столь малое расхождение, можно было бы объединить задачи функций newArt() и newHash() и разработать

565

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

ЗАДАНИЕ. Измените хэш-функцию hashFun() таким образом, чтобы первой буквой анализируемых терминов могла быть не только строчная, но и прописная английские буквы.

ЗАДАНИЕ. Используйте в предложенном проекте словаря более совершенную функцию хэширования, т.е. существенно уве-

личьте HASH_LEN.

Убедившись, что англо-русский словарь построен и может использоваться, следует обратить внимание на принципиальные недостатки нашего проекта.

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

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

Для исправления указанных недостатков следует изменить правила хранения информации в файлах словаря. Но тут мы приходим к необходимости серьезно изучить принципы и механизмы управления данными, что выходит за рамки нашего пособия, посвященного методам и средствам программирования на языке Си. Заинтересованным читателям можно рекомендовать классические и современные работы по файловым системам и обработке данных, например [12,

18, 19,20].

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

566

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

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

!FILE – это имя предопределенного структурного типа. Структура этого типа содержит такие сведения о файле, как режим обменов с файлом, размер буфера и т.д.

!Стандартные потоки ввода-вывода представлены в программе зарезервированными именами stdin, stdout, stderr, каждое из которых является указателем типа FILE * (12_01.c).

!"Открыть файл" – это значит поставить в соответствие файлу с заданным и известным операционной системе именем конкретный указатель типа FILE * (12_02.с).

!Для обработки данных из файла в программе необходимо определить указатель типа FILE * и "связать" его с именем файла

(12_2.с, 12_03.с).

!Если файл и исполняемая программа находятся в разных каталогах, то при открытии файла в программе нужно указывать полный путь к файлу (12_02_1.с).

!Имя файла – это обычная строка в стиле Си. Оно может быть задано строковой константой (12_02.с, 12_05.с), аргументом в командной строке при запуске программы (12_03.с), введено пользователем с клавиатуры (12_04.с), сформировано при исполнении программы (12_03.с) и т.д.

!При записи в тексте программы полного имени файла в виде строковой константы не забудьте об "удвоении" каждого символа '\', разделяющего имена каталогов в их иерархии (12_02_1.с).

!При выводе информации на экран "порциями" можно применить для получения от пользователя сигнала на вывод следующей

"порции" функцию getchar() (12_02.c, 12_03.c).

!При текстовом чтении файла количество введенных в программу символов меньше реального размера файла на количество строк, сохраняемых в файле (12_04.с).

!Файл, открываемый для записи в режиме "w", каждый раз созда-

ется заново (12_04.с, 12_04_1.с).

!При чтении и записи файла в бинарном режиме (12_04_1.с) признаки окончания строк обрабатываются наряду с остальными

567

символами потока. Количество введенных в программу символов равно размеру файла.

!Чтобы изменить режим обработки файла (например, чтобы начать читать данные из начала файла, открытого ранее для записи), необходимо его закрыть и открыть заново с указанием другого режима (12_05.с).

!Проверить, достигнут ли конец файла, можно с помощью библиотечной функции feof() (12_05.c).

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

!При открытии потока (файла) в режимах "r" и "w" указатель чтения/записи устанавливается на начало потока (12_06.с).

!При открытии потока (файла) в режиме "a" указатель чтения/записи устанавливается в конец потока (12_06.с).

!Функция fgets, обнаружив в файле код символа новой строки ('\n') переносит его в формируемую строку, а после него добавляет туда признак конца строки '\0' (функция openPrograms() из программы 12_06.с, функция search() – из программы

12_11.с).

!При автономной компиляции функций программы приходится снабжать каждую функцию описаниями всех глобальных имен, используемых в ее теле (модули m0.c m4.c из задачи 12-06).

!Для прямого доступа к данным файла в произвольном порядке необходимо при каждом обращении установить позицию чтения/записи в нужное положение, используя функцию fseek() (12_07.с, 12_08.с, 12_09.с).

!Два режима, позволяющие изменять файл: "r+", – изменение данных без увеличения размеров файла; "a+" – изменение данных и размеров файла.

!Позиции (смещения) в файлах обозначаются числовыми значе-

ниями типа long (12_07.с, 12_08.с, 12_09.с).

!При смещениях по файлу в качестве точки отсчета выбирают: "начало файла" (SEEK_SET), "конец файла" (SEEK_END), "теку-

щую позицию" (SEEK_CUR) (12_07.с, 12_08.с, 12_09.с).

ПРИЛОЖЕНИЕ

Свободно

распространяемый компилятор DJGPP

Для программирования на языках Си и Си++ на компьютерах с Intel’овской архитектурой удобен свободно распространяемый 32разрядный компилятор DJGPP (автор D.J.Delorie, см., например, http://www.gnu.org/software/gcc/). Его название представляет собой аб-

бревиатуру, первые буквы которой DJ являются инициалами автора – инициатора проекта. Пакет DJGPP – это не только компилятор, но и инструментальные средства для разработки программ на языках Си и Си++.

DJGPP при обработке программы на языке Си обычно выполняет: препроцессорную обработку, собственно компиляцию, ассемблирование и компоновку.

Следует отметить, что DJGPP вначале разрабатывался для операционной системы Unix, поэтому синтаксис его команд очень похож на синтаксис команд, принятый в Unix. (С подготовкой программ в системе Unix читатель может познакомиться в разд. 9.1 пособия [3].) Для работы с DJGPP необходимо знать принятые в нем соглашения об обозначениях файлов и команды для управления процессом их обработки.

Список учитываемых расширений имен файлов:

.c (строчная буква) – текст на стандартном языке Си (требуется обработка препроцессором, потом компилятором);

.h – заголовочный файл (текстовый);

.i – Си-файл (текстовый), уже обработанный препроцессором и требующий обработки компилятором;

.s – текстовый файл на языке ассемблера (направляется на ассемблирование, минуя при этом все предыдущие этапы);

.S (прописная буква) – ассемблерный файл, требующий перед компиляцией обработки препроцессором (может включать директивы

#include, #define);

.cc, .cxx, .cpp, .c++, .C – программа (исходный текст) на Си++;

.ii – текст программы на Си++, обработанный препроцессором (направляется компилятору);

569

.o – объектный файл, сформированный после компиляции и ассемблирования;

.a – библиотечный файл.

Команды для работы с компилятором. Как принято в системе

Unix, в командах большую роль играют ключи. Каждый ключ вводится символом "-" (дефис). Основными из них для наших целей являются:

-v – сообщить версию и дату создания компилятора или другого вызываемого компонента;

--help – выдать справочную информацию о компоненте;

-E – выполнить только препроцессорную обработку (не компилировать) – создать модуль с именем, имеющим расширение .i (для Сипрограмм) или .ii (для программ на Си++);

-S – выполнить только компиляцию и не ассемблировать полученный модуль с именем, имеющим расширение .s;

-c – выполнить компиляцию и ассемблирование (не выполнять компоновку); полученный объектный модуль должен иметь имя с расширением .o;

-o имя_файла – поместить результат обработки в файл с заданным после ключа именем.

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

>gcc имя_С-файла.c -o имя_исполнимого_файла.exe

Сокращенный вариант этой команды:

>gcc имя_С-файла.c

В этом случае создается исполнимый модуль транслируемой программы, всегда имеющий одно и то же имя: a.exe.

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

>gcc –c С-файл_1.c >gcc –c С-файл_2.c >gcc –c С-файл_3.c

>gcc -o файл.exe С-файл_1.o С-файл_2.o С-файл_3.o

Чтобы получить текст с результатом препроцессорной обработки программы, используется команда

>gcc –Е -o файл.i С-файл.c

570