15. Библиотека языка Си и файлы ввода-вывода

БИБЛИОТЕКА ЯЗЫКА СИ

ФАЙЛЫ В ЯЗЫКЕ СИ

ФУНКЦИИ РАБОТЫ С ФАЙЛОМ

МАКРООПРЕДЕЛЕНИЯ ДЛЯ ПРОВЕРКИ СИМВОЛОВ

ФУНКЦИИ РАСПРЕДЕЛЕНИЯ ПАМЯТИ

Всякий раз, когда нам нужно использовать такие функции, как printf( ), getchar( ) и strlen( ), мы обращаемся в библиотеку языка Си. Она содержит множество функций и макроопределений. Библиотеки меняются от системы к системе, но есть ядро функций (называемое стандартной библиотекой), которое используется чаще всего. В этой главе мы рассмотрим пятнадцать наиболее общих из этих функций, уделяя больше внимания функциям ввода-вывода и использованию файлов.

Однако сначала давайте поговорим о том, как пользоваться библиотекой.

ДОСТУП В БИБЛИОТЕКУ ЯЗЫКА СИ

Получение доступа к библиотеке зависит от системы, поэтому вам нужно посмотреть в своей системе, как применять наиболее распространенные операторы. Во-первых, есть несколько различных мест расположения библиотечных функций. Например, getchar( ) обычно задают как макроопределение в файле stdio.h, в то время как strlen( ) обычно хранится в библиотечном файле. Во-вторых, различные системы имеют разные способы доступа к этим функциям. Вот три из них.

Автоматический доступ

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

Включение файла

Если функция задана как макроопределение, то можно директивой #include включить файл, содержащий ее определение. Часто подобные функции могут быть собраны в соответствующим образом названный заголовочный файл. Например, некоторые системы имеют файл ctype.h, содержащий макроопределения, задающие тип символа: прописная буква, цифра и т. д.

Включение библиотеки

На некотором этапе компиляции или загрузки программы вы можете выбрать библиотеку. В нашей системе, например, есть файл lc.lib, содержащий скомпилированную версию библиотечных функций, и мы предлагаем редактору связей IBM PC использовать эту библиотеку. Даже система, которая автоматически контролирует свою стандартную библиотеку, может иметь другие библиотеки редко применяемых функций, и эти библиотеки следует запрашивать явно, указывая соответствующий признак во время компиляции.

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

БИБЛИОТЕЧНЫЕ ФУНКЦИИ, КОТОРЫЕ МЫ ИСПОЛЬЗОВАЛИ

Пока мы хотим только перечислить эти функции, чтобы напомнить о них.

Сначала приведем функции ввода-вывода:

getchar( ) /* получение символа */

putchar( ) /* печать символа */

gefs( ) /* получение строки */

puts( ) /* печать строки */

scanf( ) /* получение форматированного ввода */

printf( ) /* печать форматированного вывода */

Затем приведем функции, работающие со строками:

strlen( ) /* нахождение длины строки */

strcmp( ) /* сравнение двух строк */

strcpy( ) /* копирование строки */

strcat( ) /* объединение двух строк */

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

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

СВЯЗЬ С ФАЙЛАМИ

Часто нам бывает нужна программа получения информации от файла или размещения результатов в файле. Один способ организации связи программы с файлом заключается в использовании операций переключения < и >. Этот метод прост, но ограничен. Например, предположим, вы хотите написать диалоговую программу, которая спрашивает у вас названия книг (звучит фамильярно?), и вы намерены сохранить весь список в файле. Если вы используете переключение как, например, в

books > bklist

то ваши диалоговые приглашения также будут переключены на bklist. И тогда не только нежелательная чепуха запишется в bklist, но и пользователь будет избавлен от вопросов, на которые он, как предполагалось, должен отвечать.

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

ЧТО ТАКОЕ ФАЙЛ?

Для нас файл является частью памяти, обычно на диске, со своим именем. Мы считаем, например, stdio.h именем файла, содержащего некоторую полезную информацию. Для операционной системы файл более сложен, но это системные проблемы, а не наши. Однако мы должны знать, что означает файл для программы на языке Си. В предлагаемых для обсуждения функциях, работающих с файлами, язык Си "рассматривает" файл как структуру. Действительно, файл stdio.h содержит определение структуры файла. Вот типичный пример, взятый из IBM-версии компилятора Lattice С:

struct _ iobuf

{

char *_ptr; /* текущий указатель буфера */

int_cnt; /* текущий счетчик байтов */

char*_base; /* базовый адрес буфера ввода-вывода*/

char_flag; /* управляющий признак */

char _ file; /* номер файла */

};

#define FILE struct_iobuf /* краткая запись */

Здесь мы не собираемся разбираться детально в этом определении. Главное состоит в том, что файл является структурой, и что краткое наименование шаблона файла - FILE. (Многие системы используют директиву typedef для установления этого соответствия.) Таким образом, программа, имеющая дело с файлами, будет использовать тип структуры FILE, чтобы делать так.

Имея это в виду, мы сможем лучше понять операции над файлами.

ПРОСТЫЕ ПРОГРАММЫ ЧТЕНИЯ ФАЙЛА: fopen( ), fclose( ), getc( ) и putc( )

Чтобы показать элементарные примеры использования файлов, мы составили небольшую программу, которая читает содержимое файла, названного test, и выводит его на экран. Вы найдете наши пояснения сразу после программы.

/* расскажите, что находится в файле "test" */

#include

main( )

{

FILE *in; /* описываю указатель на файл */

int ch;

if((in = fopen("test", "r"))!=NULL)

/* открываю test для чтения, проверяя существует ли он */

/* указатель FILE ссылается теперь на test */

{

while((ch = getc(in) != EOF) /* получаю символ из in */

putc(ch, stdout); /* посылаю на стандартный вывод */

fclose(in); /* закрываю файл */ }

else

printf("я не смогла открыть файл \" test\" .\n");

}

Следует объяснить три основных момента: работу fopen( ), работу fclose( ) и использование функций ввода-вывода файла. Займемся ими.

Открытие файла: fopen( )


Функцией fopen( ) управляют три основных параметра. Первый - имя файла, который следует открыть. Он является и первым аргументом fopen( ); в нашем случае это "test" .

Второй параметр [и второй аргумент fopen( )] описывает, как должен использоваться файл. Вот три основных применения файла:


"r": файл нужно считать
"w": файл нужно записать
"а": файл нужно дополнить

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

Внимание: Если вы используете "w" для существующего файла, то старая версия его стирается, и ваша программа начинает на "чистом месте".

Третий параметр является указателем на файл; это значение возвращается функцией:

FILE *in;

in = fopen("test", "r");

Теперь in является указателем на файл "test". С этого момента программа ссылается на файл при помощи указателя in, а не по имени test.

Если вы очень сообразительны, то теперь можете задать такой вопрос: "Если fopen( ) возвращает указатель на 'FILE' в качестве аргумента, то почему мы не должны объявить fopen( ) как функцию указателя на 'FILE' ?" Хороший вопрос. Ответ заключается в том, что это описание сделал для нас файл stdio.h, который содержит строку

FILE *topen( );

Есть еще один важный момент относительно функции fopen(), которую мы использовали. Если fopen() не способна открыть требуемый файл, она возвращает значение 'NULL' (определенное в stdio.h как 0). Почему она не смогла открыть файл? Вы можете попросить ее считать файл, который не существует. Вот почему мы имеем в программе строку

if((in=fopen("test", "r"))!= NULL)

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

Закрыть файл проще.

Закрытие файла: fclose( )

Наш пример показывает, как закрывать файл:

fclose(in);

Просто используйте функцию fclose( ). Заметим, что аргументом ее является in, указатель на файл, а не test, имя файла.

Для программы, более серьезной, чем эта, следовало бы посмотреть, успешно ли закрыт файл. Функция fclose( ) возвращает значение 0, если файл закрыт успешно, и -1 в противном случае.

Текстовые файлы с буферизацией

Функции fopen() и fclose() работают с текстовыми файлами с "буферизацией". Под буферизацией мы понимаем, что вводимые и выводимые данные запоминаются во временной области памяти, называемой буфером. Если буфер заполнился, содержимое его передается в блок, и процесс буферизации начинается снова. Одна из основных задач fclose( ) заключается в том, чтобы "освободить" любые частично заполненные буфера, если файл закрыт.

Текстовым считается файл, в котором информация запоминается в виде символов в коде ASCII (или аналогичном). Он отличается от двоичного файла, который обычно используется для запоминания кодов машинного языка. Функции ввода-вывода, о которых мы собираемся рассказать, предназначены только для работы с текстовыми файлами.

Ввод-вывод файла: getc( ) и putc( )

Две функции getc( ) и putc( ) работают аналогично функциям getchar( ) и putchar( ). Разница заключается в том, что вы должны сообщить новичкам, какой файл следует использовать. Таким образом, наш "старый дружище"

ch = getchar( );

предназначен для получения символа от стандартного ввода, а

ch = getc(in);

- для получения символа от файла, на который указывает in. Аналогично функция

putc(ch, out);

предназначена для записи символа ch в файл, на который ссылается указатель out типа FILE. В списке аргументов функции putc( ) этот символ стоит первым, а затем указатель файла. В нашем примере мы использовали

putc(ch, stdout);

где stdout является указателем на стандартный вывод. Таким образом, этот оператор эквивалентен

putchar(ch);

Действительно, оператор putchar(ch) определен директивой #define так же как функция putc(ch, stdout) определена в файле stdio.h. Этот ужасный файл к тому же определяет в директиве #define указатели stdout и stdin на стандартный вывод и стандартный ввод системы.

Это выглядит довольно просто? Хорошо, добавим пару полезных новшеств.

ПРОСТАЯ ПРОГРАММА СЖАТИЯ ФАЙЛА

В нашем примере имя файла, который следовало открыть, было записано в программе. Мы нс обязаны считаться с этим ограничением. Используя аргументы командной строки, можно сообщить нашей программе имя файла, который хотим считать. B нашем следующем примере (рис. 15.1) так и происходит. С помощью примитивного приема сжимается содержимое - остается только каждый третий символ. Наконец, сжатая версия размещается в новый файл, имя которого состоит из старого имени с добавкой .red (сокращение слова reduced). Обычно весьма важны первый и последний элементы (аргумент командной строки и добавка к имени файла). Само же сжатие имеет более ограниченное применение, но, как вы увидите, им можно пользоваться.

/* сожмите ваши файлы в 2-3 раза! */

#include

main(argc, argv)

int argc;

char *argv[ ];

{

FILE *in, *out; /* описывает два указателя типа FILE */

int ch;

static char name[20]; /* память для имени выходного файла */

int count = 0;

if(argc < 2) /* проверяет, есть ли входной файл */

printf(" Извините, мне нужно имя файла в качестве аргумента.\n" );

else

{ if((in = fopen(argv[1], "r")) != NULL)

{

strcpy(name, argv[1]; /* копирует имя файла в массив */

strcat(name, ".red"); /* добавляет .red к имени */

out = fopen(name, "w"); /* открывает файл для записи */

while((ch = getc(in)) ! = EOF)

if( count ++ %3 ==0)

putc(ch, out); /* печатает каждый третий символ */

fclose(in);

fclosе(out); }

else

printf(" Я не смогла открыть файл\" %s\" .\n", argv[1]);

} }

РИС. 15.1. Программа сжатия файла.

Мы поместили программу в файл, названный reduce и применили эту программу к файлу, названному eddy, который содержал одну единственную строку

Даже Эдди нас опередил с детским хором.

Была выполнена команда

reduce eddy

и на выходе получен файл, названный eddy.red, который содержит

Дед спел тихо

Какая удача! Наш случайно выбранный файл сделал осмысленное сокращение.

Вот некоторые замечания по программе.

Вспомните, что argc содержит определенное количество аргументов, в число которых входит имя программного файла. Вспомните также, что с согласия операционной системы argv[0] представляет имя программы, т. е. в нашем случае reduce. Вспомните еще, что argv[l] представляет первый аргумент, в нашем случае eddy. Так как сам argv[l] является указателем на строку, он не должен заключаться в двойные кавычки в операторе вызова функции.

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

С целью создания нового имени выходного файла мы используем функцию strcpy( ) для копирования имени eddy в массив nаmе. Затем применяем функцию strcat( ) для объединения этого имени с .red.

Программа требует, чтобы два файла были открыты одновременно, поэтому мы описали два указателя типа 'FILE '. Заметим, что каждый файл должен открываться и закрываться независимо от другого. Существует ограничение на количество файлов, которые вы можете держать открытыми одновременно. Оно зависит от типа системы, но чаще всего находится в пределах от 10 до 20. Можно использовать один и тот же указатель для различных файлов при условии, что они нс открываются в одно и то же время.

Мы не ограничиваемся использованием только функций getc( ) и putc( ) для файлов ввода-вывода. Далее мы рассмотрим некоторые другие возможности.

ВВОД-ВЫВОД ФАЙЛА: fprintf( ), fscanf( ), fgets( ) И fputs( )

Все функции ввода-вывода, которые мы использовали в предыдущих главах, имеют аналоги для ввода-вывода файла. Основное отличие состоит в том, что вам нужно использовать указатель типа FILE, чтобы сообщить новым функциям, с каким файлом им следует работать. Подобно getc( ) и putc( ) эти функции используются после функции fopen( ), открывающей файл, и перед fclose( ), закрывающей его.

Функции fprintf( ) и fscanf( )

Эти функции ввода-вывода работают почти как printf( ) и scanf( ), но им нужен дополнительный аргумент для ссылки на сам файл. Он является первым в списке аргументов. Вот пример, иллюстрирующий обращение к этим функциям:

/* образец использования fprintf( ) и fscanf( ) */

#include

main( )

{

FILE *fi;

int age;

fi = fopen("sam", "r"); /* считывание */

fscanf(fi, "%d", &age); /* fi указывает на sam */

fclose(fi);

fi = fopen("data", "a"); /* дополнение "/

fprintf(fi, "sam is %d. \n", age); /* fi указывает на data */

fclose(fi);

}


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

В отличие от getc( ) и putc( ) эти две функции получают указатель типа FILE в качестве первого аргумента. Две другие, описанные ниже, получают его в качестве последнего аргумента.

Функция fgets( )

Эта функция имеет три аргумента, в то время как gets( ) имеет один. Вот пример ее использования:

/* считывает файл строка за строкой */

#include

#define MAXLIN 80

main( )

{

FILE *f1;

char *string [MAXLIN];

f1 = fopen("story", "r");

while(fgets(string, MAXLIN, f1) != NULL)

puts(string);

}

Первый из трех аргументов функции fgets( ) является указателем на местоположение считываемой строки. Мы располагаем вводимую информацию в символьный массив string.

Второй аргумент содержит предельную длину считываемой строки. Функция прекращает работу после считывания символа новой строки или после считывания символов общим числом MAXLIN - 1 (в зависимости от того, что произойдет раньше). В любом случае нуль-символ ('\0') добавляется в самый конец строки.

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

Разница между gets( ) и fgets( ) заключается в том, что gets( ) заменяет символ новой строки на '\0', в то время как fgets( ) сохраняет символ новой строки.

Подобно gets( ) функция fgets( ) возвращает значение NULL, если встречает символ EOF. Это позволяет вам проверить, как мы и сделали, достигли ли вы конца файла.

Функция fputs( )

Эта функция очень похожа на puts( ). Оператор

fputs(" Вы были правы.", fileptr);

передает строку "Вы были правы." В файл, на который ссылается указатель fileptr типа FILE. Конечно, сначала нужно открыть файл при помощи функции fopen( ). В самом общем виде это выглядит так

status = fputs(yказатель строки, указатель файла);

где status является целым числом, которое устанавливается в EOF, если fputs( ) встречает EOF или ошибку.

Подобно puts( ) эта функция не ставит завершающий символ '\0' в конец копируемой строки. В отличие от puts() функция fputs( ) не добавляет символ новой строки в ее вывод.

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

ПРОИЗВОЛЬНЫЙ ДОСТУП: fseek( )

Функция fseek( ) позволяет нам обрабатывать файл подобно массиву и непосредственно достигать любого определенного байта в файле, открытом функцией fopen( ). Вот простой пример, показывающий, как она работает. Как и в наших предыдущих примерах, функция использует аргумент командной строки для получения имени файла, с которым она работает. Заметим, что fseek() имеет три аргумента и возвращает значение типа int.

/*использование fscek( ) для печати содержимого файла */

#include

main(number, names) /* не следует использовать argc и argv */

int number;

char *namеs[ ];

{

FILE *fp;

long offset = OL; /*обратите внимание, что это тип long */

if(number < 2)

puts("Мне нужно имя файла в качестве аргумента.");

else {

if((fp = fopen(names[1], "r")) == 0)

printf(" Я не могу открыть %s.\n", names[1]);

else {

while(fseek(fp, offset++, 0) == 0)

putchar(getc(fp));

fclose(fp); }

}

}

Первый из трех аргументов функции fseek( ) является указателем типа FILE на файл, в котором ведется поиск. Файл следует открыть, используя функцию fopen( ).

Второй аргумент назван "offset" (вот почему мы выбрали данное имя для переменной). Этот аргумент сообщает, как далеко следует передвинуться от начальной точки (см. ниже); он должен иметь значение типа long, которое может быть положительным (движение вперед) или отрицательным (движение назад).

Третий аргумент является кодом, определяющим начальную точку:


Код Положение в файле
0 начало файла
1 текущая позиция
2 конец файла

Функция fseek( ) возвращает 0, если все хорошо, и -1, если есть ошибка, например попытка перемещаться за границы файла. Теперь мы можем разъяснить наш маленький цикл:

while(fseek(fp, offset++, 0)==0)

putchar(getc(fp));

Поскольку переменная offset инициализирована нулем, при первом прохождении через цикл мы имеем выражение

fseek(fp, OL, 0)

означающее, что мы идем в файл, на который ссылается указатель fp, и находим байт, отстоящий на 0 байт от начала, т.е. первый байт. Затем функция putchar( ) печатает содержимое этого байта. При следующем прохождении через цикл переменная offset увеличивается до 1L, и печатается следующий байт. Посуществу, переменная offset действует подобно индексу для элементов файла. Процесс продолжается до тех пор, пока offset нe попытается попасть в fseek( ) после конца файла. В этом случае fseek( ) возвращает значение - 1 и цикл прекращается.

Этот последний пример чисто учебный. Нам нe нужно использовать fseek( ), потому что getc( ) так или иначе проходит через файл байт за байтом; fseek( ) приказала getc( ) "посмотреть" туда, куда она сама уже собиралась посмотреть.

Вот пример (рис. 15.2), в котором выполняется что-то несколько более необычное (Мы благодарим Вильяма Шекспира за этот пример в пьесе "Двенадцатая ночь").

/* чередование печати в прямом и обратном направлениях */

#include

main(number, names) /* вам не нужно применять argc и argv */

int number;

char *names[ ];

{

FILE *fp;

long offset = 0L;

if(number < 2)

puts(" Мне нужно имя файла в качестве аргумента.");

else {

if(fp = fopen(names[l], "r")) == 0)

printf(" Я не могу открыть %s.\n", names[l]);

else {

while(fseek(fp, offset++, 0) == 0)

{ putchar(getc(fp));

if(fseek(fp, -(offset + 3), 2) == 0)

putchar(getc(fp)); }

fclose(fp); }

} }


РИС. 15.2. Программа, чередующая печать в прямом и обратном направлениях.

Применение этой программы к файлу, содержащему имя "Мальволио", дает такой приятный результат:

МоаилльоввоьллиаоМ

Наша программа печатает первый символ файла, затем последний, затем второй, затем предшествующий последнему и т.д. Мы только добавили вот эти строки в последнюю программу:

if(fseek(fp, -(offset + 3), 2) == 0)

putchar(getc(fp));

Код 2 в операторе предполагает, что мы будем считать позиции от конца файла. Знак минус означает счет в обратном направлении. +3 стоит здесь потому, что мы начинаем с последнего регулярного символа файла и пропускаем несколько символов "новая строка" и EOF в самом конце файла. (Точное значение этой корректировки зависит от типа системы. Наши файлы имеют в конце по два символа новой строки, за которыми следуют два EOF, поэтому мы как раз их и обходим.)

Таким образом, эта часть программы чередует печать в обратном направлении и печать в прямом направлении. Следует заметить, что в некоторых системах может не предусматриваться код 2 для fseek( ).

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

ПРОВЕРКА И ПРЕОБРАЗОВАНИЕ СИМВОЛОВ

Заголовочный файл ctype.h содержит несколько функций макроопределений, которые проверяют, к какому классу принадлежат символы. Функция isalpha(c), например, возвращает ненулевое значение (истина), если с является символом буквы, и нуль (ложь), если символ не является буквой. Таким образом,

isalpha('S') != 0, но isalpha('#') ==0

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

ФУНКЦИЯ ПРОВЕРЯЕТ, ЯВЛЯЕТСЯ ЛИ С
isalpha(c) буквой
isdigit(c) цифрой
islower(c) строчной буквой
isspace(c) пустым символом (пробел, табуляция или новая строка)
isupper(c) прописной буквой

Ваша система может иметь дополнительные функции, такие как

ФУНКЦИЯ ПРОВEРЯEТ, ЯВЛЯЕТСЯ ЛИ С
isalnum(c) алфавитноцифровым (буква или цифра)
isascii(c) кодом ASCII (0-127)
iscntrl(c) управляющим символом
ispunct(c) знаком пунктуации

Еще две функции выполняют преобразования

toupper(c) преобразует с в прописную букву
tolower(c) преобразует с в строчную букву

В некоторых системах преобразование выполняется только в случае, если символ находится в регистре (прописных или строчных букв), противоположном тому, с которого следует начинать. Однако надежнее предварительно проверить регистр.

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

/* преобразование строчных букв в прописные и обратно */

#include

#include /* включает файл макроопределений */

#define UPPER 1

#define LOWER 0

main( )

{

int crit; /* для установки регистра прописных или строчных букв */

char file1[14], file2[14]; /* имена входного и выходного файлов */

crit = choose( ); /* выбирает прописные или строчные буквы */

getfiles(file1, file2); /* получаст имена файлов */

conv(file1, file2, crit); /* выполняет преобразование */

} choose( )

{ int ch;

printf("Программа преобразует весь файл в прописные буквы или \n");

printf(" в строчные буквы. Вводит U, если нужны прописные буквы\n");

printf(" или вводит L, если нужны строчные буквы. \n");

while((ch=getchar( ))!='U' && ch!='u' && ch!='L'

&& ch!='l')

printf(" Введите, пожайлуста, U или L.\n");

while(getchar( )!='\n')

; /* сбрасывает последний символ новой строки */

if(ch =='U' || ch =='u')

{ printf(" Все в порядке, есть регистр прописных букв.");

return(UPPER);

else

{ printf(" Все в порядке, есть регистр строчных букв.");

return(LOWER); } }

getfiles(namel, name2);

char *namel, name2;

{ printf(" Какой файл вы хотите преобразовать?\n");

gets(name1);

printf(" Это\" %s\" .\n", name1);

printf("Какое имя вы хотите выбрать для преобразуемого файла?\n");

while(strcmp(gets(name2), name1) == NULL)

printf(" Выберите другое имя.\n" );

printf(" Ваш выходной файл\" %s \".\n", name2);

} conv(name1, name2, crit);

char *name1, name2;

int crit;

{ int ch;

FILE *f1, *f2;

if((f1 = fopen(name1, "r" )) == NULL)

printf(Извините, я не могу открыть % s. До свидания.\n", name1);

else

{ puts(" Итак, начнем!");

f2 = fopen(name2, "w");

while((ch = getc(f1)) != EOF)

if(crit == UPPER)

ch = islower(ch) ? toupper(ch) : ch;

else

ch = isupper(ch) ? tolower(ch) : ch;

putc(ch, f2);

} fclosc(f2);

fclosc(f1);

puts("Сделано!");

} }

РИС. 15.3. Программа преобразования строчных букв в прописные и обратно.

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

while(getchar( ) != '\n');

Этот цикл включен для решения проблемы, с которой мы столкнулись в гл. 14. Когда пользователь отвечает на вопрос о виде преобразования, скажем, буквой U, он нажимает клавишу U, а затем клавишу [ввод], которая передает '\n'.

Первоначальная функция getchar( ) извлекает U, но оставляет '\n' для следующего чтения строки. Функция gets(), входящая в getnames(), интерпретировала бы '\n' как пустую строку, поэтому мы использовали малый цикл while, чтобы избавиться от символа "новая строка". Действительно, простая getchar( ), сделала бы это, если бы пользователь непосредственно за U нажимал бы [ввод]. Но наша версия, кроме того, предусматривает возможность нажать на клавишу пробела несколько раз перед [ввод].

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

Функция conv( ) является функцией копирования с выполнением преобразования. Значение crit используется для определения требуемого преобразования. Работа выполняется простым условным оператором, таким как

ch = islower(ch) ? toupper(ch) : ch;

Он проверяет, является ли ch строчной буквой. Если да, то символ преобразуется в прописную букву. Если нет, остается как есть.

Макрофункции файла ctype.h предоставляют удобные и полезные средства для программирования. Теперь давайте займемся некоторыми более сложными функциями преобразования.

ПРЕОБРАЗОВАНИЯ СИМВОЛЬНЫХ СТРОК: atoi( ), atof( )

Использование scanf( ) для считывания цифровых значений не является самым надежным способом, поскольку scanf( ) легко внести в заблуждение ошибками пользователей при вводе чисел с клавиатуры. Некоторые программисты предпочитают считывать даже числовые данные как символьные строки и преобразовывать строку в соответствующее числовое значение. Для этого используются функции atoi( ) и atof( ). Первая преобразует строку в целое, вторая - в число с плавающей точкой. Вот (рис. 15.4) образец их использования:

/* включение atoi( ) */

#include

#define issign(c) (((с) == '-' || (с) == '+') ? (1) : (0))

#define SIZE 10

#define YES 1

#define NO 0

main( )

{

char ch;

static char number[SIZE];

int value;

int digit = YES;

int count = 0;

puts(" Введите, пожалуйста, целое.");

gets(number);

if(number[SIZE - 1] != '\0')

{ puts("Слишком много цифр; вы уничтожили меня.");

exit(1);

} while((ch = number[count]) !='0' && digit == YES)

if(!issign(ch) && iisdigit(ch) && !isspace(ch))

digit = NO;

if(digit == YES)

{ value = atoi(number);

printf(" Число было %d.\n" , value); }

else

printf(" Это не похоже на целое.");

}

РИС. 15.4. Программа использования atoi( ).

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

Затем посмотрим, не содержит ли строка что-нибудь кроме пробелов, цифр и алгебраических знаков. Функция отвергает такие строки, как "дерево" или "1.2Е2". Ее устраивает смесь, подобная "3 - 4 + 2", но atoi( ) будет выполнять дальнейший отбор. Вспомним, что ! является операцией отрицания, поэтому !isdigit(c) означает: "с не является цифрой". Строка

value = atoi(nuinbcr);

показывает, как используется функция atoi( ). Ее аргумент является указателем символьной строки; в этом случае мы применили имя массива number. Функция возвращает целое значение для такой строки. Таким образом, "1234" является строкой из четырех символов и переводится в 1234 - единое число типа int.

Функция atoi( ) игнорирует ведущие пробелы, обрабатывает ведущий алгебраический знак, если он есть, и обрабатывает цифры вплоть до первого символа, нс являющегося цифрой. Поэтому наш пример "3 - 4 + 2" был бы превращен в значение 3. Посмотрите "Вопросы" в конце главы для возможного применения этой функции.

Функция atof( ) выполняет подобные действия для чисел с плавающей точкой. Она возвращает тип double, поэтому должна быть описана как double в использующей ее программе.

Простые версии atof( ) будут обрабатывать числа вида 10.2, 46 и - 124.26. Более мощные версии преобразуют также экспоненциальную запись, т. е. числа, подобные 1.25Е - 13.

Ваша система может также иметь обратные функции, работающие в противоположном направлении. Функция itoa( ) будет преобразовывать целое в символьную строку, а функция ftoa( ) преобразовывать число с плавающей точкой в символьную строку.

ВЫХОД: exit( )

Функция exit( ) даст вам удобный способ "покинуть" программу. Она часто используется для прекращения работы программы при появлении ошибки. Если к exit( ) обратились из функции, вызванной главной программой, то прекращает работу вся программа, а не только эта функция. В приведенном выше примере с функцией atoi( ) использование exit( ) позволяет нам избежать включения дополнительного оператора else для обхода остатка программы.

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

Аргументом exit( ) является номер кода ошибки. В некоторых системах он может передаваться другой программе, если исходная прекратила работу. Существует соглашение, что 0 указывает на нормальное завершение, в то время как любое другое значение говорит об ошибке.

Есть еще одна тема, которую мы хотим обсудить.


РАСПРЕДЕЛЕНИЕ ПАМЯТИ: malloc( ) И са11ос( )

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

char place[ ] = "Залив Свиной печенки";

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

Или мы можем быть более конкретны и запросить определенный объем памяти:

int plates[100];

Это описание выделяет 100 ячеек памяти, каждая из которых предназначена для запоминания целого значения.

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

/* добавляет память, если необходимо */

#include

#define STOP " " /* сигнал прекращения ввода */

#define BLOCK 100 /* байты памяти */

#define LIM 40 /* предельная длина вводимой строки */

#define MAX 50 /* максимальное число вводимых строк */

#define DRAMA 20000 /* большая задержка времени */

main( )

{

char store[BLOCK]; /* исходный блок памяти */

char symph[LIM]; /* приемник вводимых строк */

char *end; /* указывает на конец памяти */

char *starts[MAX]; /* указывает на начала строк */

int index = 0; /* количество вводимых строк */

int count; /* счетчик */

char *malloc( ); /* распределитель памяти */

starts[0] = store;

end = starts[0] + BLOCK - 1;

puts(" Назовите несколько симфонических оркестром.");

puts(" Вводите по одному: нажмите клавишу [ввод] в начале");

puts(" строки для завершения вашего списка. Хорошо, я готова." );

while(strcmp(fgets(symph, LIM, stdin), STOP) != 0 && index < MAX)

{ if(strlen(symph) > end - starts[index])

{ /* действия при недостатке памяти для запоминания вводимых данных*/

puts(" Подождите секунду. Я попробую найти дополнительную память.");

starts[index] = malloc(BLOCK);

end = starts[index] + BLOCK - 1;

for(count = 0; count < DRAMA; count++);

puts(" Нашла немного!" ); }

strcpy (starts [index], symph);

starts[index + 1] = starts[index] + strlen(symph) + 1;

if(++index < MAX)

printf("Этo %d. Продолжайте, если хотите.\n", index); }

puts(" Хорошо, вот что я получила:");

for(count = 0; count < index; count ++)

puts(starts[count]);

}

РИС. 15.5. Программа, добавляющая память по требованию.

Вот образец работы программы:

Назовите несколько симфонических оркестров оркестров.

Вводите их по одному; нажмите клавишу [ввод] в начале

строки для завершения нашего списка. Хорошо, я готова.

Сан-франциский симфонический.

Это 1. Продолжайте, если хотите.

Чикагский симфонический

Это 2. Продолжайте, если хотите.

Берлинский филармонический

Это 3. Продолжайте, если хотите.

Московский камерный

Это 4. Продолжайте, если хотите. Лондонский симфонический

Это 5. Продолжайте, если хотите. Венский филармонический

Подождите секунду. Я попробую найти дополнительную память.

Нашла немного!

Это 6. Продолжайте, если хотите.

Питтсбургский симфонический

Это 7. Продолжайте, если хотите.

Хорошо, вот что я получила:

Сан-францизкий симфонический

Чикагский симфонический

Берлинский филармонический

Московский камерный

Лондонский симфонический

Венский филармонический

Питтсбургский симфонический

Сначала давайте посмотрим, что делает функция malloc( ). Она берет аргумент в виде целого без знака, которое представляет количество требуемых байтов памяти. Так, malloc(BLOCK) требует 100 байт. Функция возвращает указатель на тип char в начало нового блока памяти. Мы использовали описание

char *malloc( );

чтобы предупредить компилятор, что malloc( ) возвращает указатель на тип char. Поэтому мы присвоили значение этого указателя элементу массива starts[index] при помощи оператора

starts[index] = malloc(BLOCK);

Хорошо, давайте теперь рассмотрим проект программы, заключающийся в том, чтобы запомнить все исходные строки подряд в большом массиве store. Мы хотим использовать starts[0] для ссылки на начало первой строки, starts[l] - второй строки и т. д. На промежуточном этапе программа вводит строку в массив symph. Мы использовали fgets( ) вместо gets( ), чтобы ограничить входную строку длиной массива symph.


РИС. 15.6. Последовательные строки symph, записанные в массив store.

Прежде чем копировать symph в store, мы должны проверить, достаточно ли для нее оставшегося места. Указатель end ссылается на конец памяти, а текущее значение starts[index] ссылается на начало неиспользованной памяти. Таким образом, мы можем сравнить разницу между этими двумя указателями с длиной symph и определить, достаточно ли осталось памяти.

Если места недостаточно, вызываем malloc( ), чтобы подготовить дополнительную память. Мы устанавливаем starts[index] на начало нового блока памяти, a end - на конец нового блока. Заметим, что у нас нет имени этой новой памяти. Она не является, например, расширением store. У нас есть только обозначения указателей, ссылающихся на новую область памяти.

Когда программа работает, на каждую новую строку ссылается элемент массива указателей starts. Некоторые строки находятся в store, другие - в одной или нескольких новых областях памяти.

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

Таким образом используется mаllос( ). Но предположим, что вы хотите работать с памятью типа int, а не char. Можете и здесь использовать mаllос( ). Вот как это делается:

char *malloc( ); /* по-прежнему описываем как указатель на char */

int *newmem;

newmem = (int *) malloc(l00); /* используем операцию приведения типа */

Снова требуется 100 байт. Операция приведения типа преобразует значение, возвращенное указателем на тип char, в указатель на тип int. Если, как в нашей системе, int занимает два байта памяти, это значит, что newmem + 1 будет увеличивать указатель на два байта, т. е. передвигать его к следующему целому. Это также означает, что 100 байт можно использовать для запоминания 50 целых чисел.

Другую возможность распределения памяти дает нам применение функции саllос( ):

char *calloc( );

long *newmem;

newmem = (long *) calloc(100, sizeof(long));

Подобно malloc( ) функция саllос( ) возвращает указатель на char. Нужно использовать оператор приведения типа, если вы хотите запомнить другой тип. Эта новая функция имеет два аргумента, и оба они должны быть целыми без знака. Первый аргумент содержит количество требуемых ячеек памяти. Второй аргумент - размер каждой ячейки в байтах. В нашем случае long использует четыре байта, поэтому оператор выделит 100 четырехбайтных элементов, используя в целом 400 байтов памяти.

Применяя sizeof (long) вместо 4, мы сделали эту программу более мобильной. Она будет работать на системах, где long имеет размер, отличный от четырех.

Функция саllос( ) имеет еще одну особенность; она обнуляет содержимое всего блока.

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

ДРУГИЕ БИБЛИОТЕЧНЫЕ ФУНКЦИИ

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

Некоторые функции, работающие с файлами, включают ореn( ), close( ), create( ), fseek( ), read( ) и write( ). Они выполняют почти те же самые задачи, что и функции, которые мы обсудили, но на более фундаментальном уровне. Действительно, функции, подобные fopen( ), обычно пишутся с применением этих более общих функций. Они немного более трудны в использовании, но могут работать с двоичными файлами так же, как и с текстовыми.

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

Вам нужно время, чтобы освоить то, что предлагает наша система. Если у нее нет того, что вам нужно, создайте свои собственные функции. Это часть языка Си. Если вы полагаете, что можете улучшить работу, скажем, функции ввода, сделайте это! А когда вы усовершенствуете и отшлифуете свои методы программирования, вы перейдете от обычного языка Си к блестящему языку Си.

ЗАКЛЮЧЕНИЕ

Мы прошли долгий путь от начала этого руководства. Теперь вы уже познакомились с большинством основных свойств языка Си. (Главное из того что, мы опустили,- операции с разрядами и расширения UNIX 7 - рассматриваются кратко в приложении Б). Вы узнали и использовали все изобилие его операторов, огромное разнообразие основных и производных типов данных, его "умные" управляющие конструкции и мощную систему указателей. Мы надеемся, что подготовили вас к использованию языка Си в ваших собственных целях. Поэтому начинайте программировать, и удачи вам!

ЧТО ВЫ ДОЛЖНЫ БЫЛИ УЗНАТЬ В ЭТОЙ ГЛАВЕ

Что такое библиотека языка Си и как ее использовать.

Как открывать и закрывать текстовые файлы: fopen( ) и fclose( )

Что такое тип FILE

Как читать из файла и записывать в файл: getc( ), putc( ), fgets( ), fputs( ), fscanf( ), fprintf( )

Как проверять классы символов: isdigit( ), isalpha( ) и т. д.

Как превращать строки в числа: atoi( ) и atof( )

Как осуществлять быстрый выход: exit( )

Как распределять память: malloc( ), саllос( )

ВОПРОСЫ И ОТВЕТЫ

Вопросы

1. Что неправильно в этой программе?

main( )

{ int *fp;

int k;

fp = fopen("желе");

for(k = 0; k < 30; k++)

fputs(fp, "Нанетта ест желе.");

fclose("желе");

}

2. Что будет делать следующая программа?

#include

#include

main(argc, argv)

int argc;

char *argv[ ];

{ int ch;

FILE *fp;

if((fp=fopen(argv[1], "r")) == NULL)

exit(1);

while((ch=getc(fp)) != EOF)

if(isdigit(ch))

putchar(ch);

fclose (fp);

}


3. Вce ли правильно в выражении isalpha(c[i]), где с является массивом типа char. Что можно сказать о isalpha(c[i ++])?

4.Используйте функции классификации символе" для подготовки выполненния atoi( ).

5. Как вы могли бы распределить, память для размещения массива структур?

Ответы

1. Должна быть директива #include для определения ее файлов. Следует описать указатель fp файла: FILE *fp; функция fopen( ) должна иметь вид fopen("желе", "w"), или, может быть, включать "а" . Порядок аргументов в fputs( ) должен быть обратным. Функция fclose( ) требует указателя файла, а не имени файла: fclose(fp).

2. Она будет открывать файл, заданный как аргумент командной строки, и выво дить на печать все цифры в файле. Программа должна проверять (но не делает этого), не аргумент ли это командной строки.

3. Первое выражение правильно, так как с[i] имеет значение типа char. Второе выражение не выводит компьютер из строя, но может давать непредсказуемый результат. Причина в том, что isalpha( ) является макроопределением, у которого, по всей вероятности, аргумент появляется дважды в определяющем выражении (проверка на принадлежность к регистру строчных букв, а зачем - прописных букв) и это дает в результате два увеличения i. Лучше всего избегать использования оператора увеличения в аргументе макрофункции.

4.

#include

#include

#define issign(c) (((c) == '-' || (c) == '+') ? (1) : (0)) atoi(s);

char *s;

{

int i = 0;

int n, sign;

while(isspace(s[i]))

i ++; /* пропуск пустого символа */

sign = 1;

if(issign(s[i])) /* установка необязательного знака */

sign = (s[i++] == '+') ? 1 : -1;

for(n = 0; isdigit(s[i]); i++)

n = 10*n + s[i] - '0';

return(sign * n);

}


5. Предположим, что wine является именем структуры. Эти операторы, надлежащим образом расположенные в программе, будут выполнять данную работу.

struct wine *ptrwine;

char *calloc( );

ptrwine = (struc wine *) calloc(100, sizcof(struct wine));

УПРАЖНЕНИЯ

1. Напишите программу копирования файла, которая использует имена исходного файла файла и копируемого файла как аргументы командной строки.

2. Напишитe программу, которая будет принимать все файлы, заданные радом аргументов командной строки, и печатать их один за другим. Используйте argc для создания цикла.

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

4. Используйте gets( ) и atoi( ) для создания функции, эквивалентной нашей getint( ) в гл. 10.

5. Перепишите нашу программу из гл. 7, считающую слова, используя макроопределения ctype.h и аргумент командной строки для обработки файла.

Загрузка...