Васин Д.Ю. - Язык программирования Си. Курс лекций - 2003. Руководство для начинающих. М. Мир, 1988г. 512 с. Трой Д. Программирование на языке Си для персонального компьютера ibm pc Пер с англ. М. Радио и связь, 1991г. 432 с
Скачать 1.1 Mb.
|
4.22. Доступ к файламВо всех предыдущих примерах мы имели дело со стандартным вводом и стандартным выводом, которые для программы автоматически предопределены операционной системой конкретной машины. Следующий шаг - научиться писать программы, которые имели бы доступ к файлам, заранее не подсоединенным к программам. Одна из программ, в которой возникает такая необходимость, - это программа cat, объединяющая несколько именованных файлов и направляющая результат в стандартный вывод. Возникает вопрос: что надо сделать, чтобы именованные файлы можно было читать; иначе говоря, как связать внешние имена, придуманные пользователем, с инструкциями чтения данных? На этот счет имеются простые правила. Для того чтобы можно было читать из файла или писать в файл, он должен быть предварительно открыт с помощью библиотечной функции fopen. Функция fopen получает внешнее имя после чего осуществляет некоторые организационные действия и "переговоры" с операционной системой (технические детали которых здесь не рассматриваются) и возвращает указатель, используемый в дальнейшем для доступа к файлу. Этот указатель, называемый указателем файла, ссылается на структуру, содержащую информацию о файле (адрес буфера, положение текущего символа в буфере, открыт файл на чтение или на запись, были ли ошибки при работе с файлом и не встретился ли конец файла). Пользователю не нужно знать подробности, поскольку определения, полученные из Единственное, что требуется для определения указателя файла, - это задать описания такого, например, вида: FILE *fp; FILE *fopen(char *name, char *mode); Это говорит, что fp есть указатель на FILE, a fopen возвращает указатель на FILE. Заметим, что FILE — это имя типа) наподобие int, а не тег структуры. Оно определено с помощью typedef. Обращение к fopen в программе может выглядеть следующим образом: fp = fopen(name, mode); Первый аргумент - строка, содержащая имя файла. Второй аргумент несет информацию о режиме. Это тоже строка: в ней указывается, каким образом пользователь намерен применять файл. Возможны следующие режимы: чтение (read - "r"), запись (write - "w") и добавление (append - "a"), т. е. запись информации в конец уже существующего файла. В некоторых системах различаются текстовые и бинарные файлы; в случае последних в строку режима необходимо добавить букву "b" (binary - бинарный). Тот факт, что некий файл, которого раньше не было, открывается на запись или добавление, означает, что он создается (если такая процедура физически возможна). Открытие уже существующего файла на запись приводит к выбрасыванию его старого содержимого, в то время как при открытии файла на добавление его старое содержимое сохраняется. Попытка читать несуществующий файл является ошибкой. Могут иметь место и другие ошибки; например, ошибкой считается попытка чтения файла, который по статусу запрещено читать. При наличии любой ошибки fopen возвращает NULL. Следующее, что нам необходимо знать, - это как читать из файла или писать в файл, коль скоро он открыт. Существует несколько способов сделать это, из которых самый простой состоит в том, чтобы воспользоваться функциями getc и putc. Функция getc возвращает следующий символ из файла; ей необходимо сообщить указатель файла, чтобы она знала откуда брать символ. int getc(FILE *fp); Функция getc возвращает следующий символ из потока, на который указывает *fp; в случае исчерпания файла или ошибки она возвращает EOF. Функция putc пишет символ c в файл fp int putc(int с, FILE *fp); и возвращает записанный символ или EOF в случае ошибки. Аналогично getchar и putchar, реализация getc и putc может быть выполнена в виде макросов, а не функций. При запуске Си-программы операционная система всегда открывает три файла и обеспечивает три файловые ссылки на них. Этими файлами являются: стандартный ввод, стандартный вывод и стандартный файл ошибок; соответствующие им указатели называются stdin, stdout и stderr; они описаны в С помощью getc, putc, stdin и stdout функции getchar и putchar теперь можно определить следующим образом: #define getchar() getc(stdin) #define putchar(c) putc((c), stdout) Форматный ввод-вывод файлов можно построить на функциях fscanf и fprintf. Они идентичны scanf и printf с той лишь разницей, что первым их аргументом является указатель на файл, для которого осуществляется ввод-вывод, формат же указывается вторым аргументом. int fscanf(FILE *fp, char *format, ...) int fprintf(FILE *fp, char *format, ...) Вот теперь мы располагаем теми сведениями, которые достаточны для написания программы cat, предназначенной для конкатенации (последовательного соединения) файлов. Предлагаемая версия функции cat, как оказалось, удобна для многих программ. Если в командной строке присутствуют аргументы, они рассматриваются как имена последовательно обрабатываемых файлов. Если аргументов нет, то обработке подвергается стандартный ввод. #include /* cat: конкатенация файлов, версия 1 */ main(int argc, char *argv[]) { FILE *fp; void filecopy(FILE *, FILE *); if (argc == 1) /* нет аргументов; копируется стандартный ввод */ filecopy(stdin, stdout); else while (--argc > 0) if ((fp = fopen(*++argv, "r")) == NULL) { printf("cat: не могу открыть файл %s\n", *argv); return 1; } else { filecopy(fp, stdout); fclose(fp); } return 0; } /* filecopy: копирует файл ifp в файл ofp */ void filecopy(FILE *ifp, FILE *ofp) { int c; while ((c = getc(ifp)) != EOF) putc(c, ofp); } Файловые указатели stdin и stdout представляют собой объекты типа FILE*. Это константы, а не переменные, следовательно, им нельзя ничего присваивать. Функция int fclose(FILE *fp) - обратная по отношению к fopen; она разрывает связь между файловым указателем и внешним именем (которая раньше была установлена с помощью fopen), освобождая тем самым этот указатель для других файлов. Так как в большинстве операционных систем количество одновременно открытых одной программой файлов ограничено, то файловые указатели, если они больше не нужны, лучше освобождать, как это и делается в программе cat. Есть еще одна причина применить fclose к файлу вывода, - это необходимость "опорожнить" буфер, в котором putc накопила предназначенные для вывода данные. При нормальном завершении работы программы для каждого открытого файла fclose вызывается автоматически. (Вы можете закрыть stdin и stdout, если они вам не нужны. Воспользовавшись библиотечной функцией freopen, их можно восстановить.) 4.22.1. Вводвывод строкВ стандартной библиотеке имеется программа ввода fgets, аналогичная программе getline, которой мы пользовались в предыдущих главах. char *fgets(char *line, int maxline, FILE *fp) Функция fgets читает следующую строку ввода (включая и символ новой строки) из файла fp в массив символов line, причем она может прочитать не более MAXLINE-1 символов. Переписанная строка дополняется символом '\0'. Обычно fgets возвращает line, а по исчерпании файла или в случае ошибки - NULL. (Наша getline возвращала длину строки, которой мы потом пользовались, и нуль в случае конца файла.) Функция вывода fputs пишет строку (которая может и не заканчиваться символом новой строки) в файл. int fputs(char *line, FILE *fp) Эта функция возвращает EOF, если возникла ошибка, и неотрицательное значение в противном случае. Библиотечные функции gets и puts подобны функциям fgets и fputs. Отличаются они тем, что оперируют только стандартными файлами stdin и stdout, и кроме того, gets выбрасывает последний символ '\n', a puts его добавляет. Чтобы показать, что ничего особенного в функциях вроде fgets и fputs нет, мы приводим их здесь в том виде, в каком они существуют в стандартной библиотеке на нашей системе. /* fgets: получает не более n символов из iop */ char *fgets(char *s, int n, FILE *iop) { register int c; register char *cs; cs = s; while (--n > 0 && (с = getc(iop)) != EOF) if ((*cs++ = c) == '\n') break; *cs= '\0'; return (c == EOF && cs == s) ? NULL : s; } /* fputs: посылает строку s в файл iop */ int fputs(char *s, FILE *iop) { int c; while (c = *s++) putc(c, iop); return ferror(iop) ? EOF : 0; } Стандарт определяет, что функция ferror возвращает в случае ошибки ненулевое значение; fputs в случае ошибки возвращает EOF, в противном случае - неотрицательное значение. С помощью fgets легко реализовать нашу функцию getline: /* getline: читает строку, возвращает ее длину */ int getline(char *line, int max) { if (fgets(line, max, stdin) == NULL) return 0; else return strlen(line); } 4.22.2. Дескрипторы файловВ самом общем случае, прежде чем читать или писать, вы должны проинформировать систему о действиях, которые вы намереваетесь выполнять в отношении файла; эта процедура называется открытием файла. Если вы собираетесь писать в файл, то, возможно, его потребуется создать заново или очистить от хранимой информации. Система проверяет ваши права на эти действия (файл существует? вы имеете к нему доступ?) и, если все в порядке, возвращает программе небольшое неотрицательное целое, называемое дескриптором файла. Всякий раз, когда осуществляется ввод-вывод, идентификация файла выполняется по его дескриптору, а не по имени. Вся информация об открытом файле хранится и обрабатывается операционной системой; программа пользователя обращается к файлу только через его дескриптор. 4.22.3. Нижний уровень вводавывода (read и write)Ввод-вывод основан на системных вызовах read и write, к которым Си-программа обращается с помощью функций с именами read и write. Для обеих первым аргументом является дескриптор файла. Во втором аргументе указывается массив символов вашей программы, куда посылаются или откуда берутся данные. Третий аргумент - это количество пересылаемых байтов. int n_read = read(int fd, char *buf, int n); int n_written = write(int fd, char *buf, int n); Обе функции возвращают число переданных байтов. При чтении количество прочитанных байтов может оказаться меньше числа, указанного в третьем аргументе. Нуль означает конец файла, а -1 сигнализирует о какой-то ошибке. При записи функция возвращает количество записанных байтов, и если это число не совпадает с требуемым, следует считать, что запись не произошла. За один вызов можно прочитать или записать любое число байтов. Обычно это число равно или 1, что означает посимвольную передачу "без буферизации", или чему-нибудь вроде 1024 или 4096, соответствующих размеру физического блока внешнего устройства. Эффективнее обмениваться большим числом байтов, поскольку при этом требуется меньше системных вызовов. 4.22.4. Системные вызовы open, creat,close,unlinkВ отличие от стандартных файлов ввода, вывода и ошибок, которые открыты по умолчанию, остальные файлы нужно открывать явно. Для этого есть два системных вызова: open и creat. Функция open почти совпадает с fopen. Разница между ними в том, что первая возвращает не файловый указатель, а дескриптор файла типа int. При любой ошибке open возвращает -1. include int fd; int open(char *name, int flags, int mode); fd = open(name, flags, perms); Как и в fopen, аргумент name - это строка, содержащая имя файла. Второй аргумент, flags, имеет тип int и специфицирует, каким образом должен быть открыт файл. Его основными значениями являются: O_RDONLY - открыть только на чтение; O_WRONLY - открыть только на запись; O_RDWR - открыть и на чтение, и на запись. O_CREAT если файл не существует, создать его O_EXCL если указан режим O_CREAT, а файл уже существует, то возвратить ошибку открытия файла O_APPEND начать запись с конца файла O_TRUNC удалить текущее содержимое файла и обеспечить запись с начала файла O_BINARY открытие файла в двоичном режиме O_TEXT открытие файла в текстовом виде. Эти константы определены в По умолчанию файл открывается в двоичном виде. Третий параметр mode принимает следующие значения: S_IWRITE разрешение на запись S_IREAD разрешение на чтение S_IREAD|S_IWRITE разрешение на чтение и запись. Эти константы определены в Примеры использования: Open(“stock.dat”,O_CREAT|O_RDWR,S_IREAD|S_IWRITE) создать новый файл stock.dat для модификации (чтения и записи) Open(“с:\\dir\\stock.dat”,O_RDONLY|O_BINARY) открыть файл с:\\dir\\stock.dat на чтение в бинарном виде На количество одновременно открытых в программе файлов имеется ограничение (обычно их число колеблется около 20). Поэтому любая программа, которая намеревается работать с большим количеством файлов, должна быть готова повторно использовать их дескрипторы. Функция close(int fd) разрывает связь между файловым дескриптором и открытым файлом и освобождает дескриптор для его применения с другим файлом. Она аналогична библиотечной функции fclose с тем лишь различием, что никакой очистки буфера не делает. Завершение программы с помощью exit или return в главной программе закрывает все открытые файлы. Функция unlink(char *name) удаляет имя файла из файловой системы. 4.22.5. Произвольный доступ (lseek)Ввод-вывод обычно бывает последовательным, т. е. каждая новая операция чтения-записи имеет дело с позицией файла, следующей за той, что была в предыдущей операции (чтения-записи). При желании, однако, файл можно читать или производить запись в него в произвольном порядке. Системный вызов lseek предоставляет способ передвигаться по файлу, не читая и не записывая данные. Так, функция long lseek(int fd, long offset, int origin); в файле с дескриптором fd устанавливает текущую позицию, смещая ее на величину offset относительно места, задаваемого значением origin. Значения параметра origin SEEK_SET, SEEK_CUR или SEEK_END означают, что на величину offset отступают соответственно от начала, от текущей позиции или от конца файла. Например, если требуется добавить что-либо в файл, то прежде чем что-либо записывать, необходимо найти конец файла с помощью вызова функции lseek(fd, 0L, SEEK_END); Чтобы вернуться назад, в начало файла, надо выполнить lseek(fd, 0L, SEEK_SET); Следует обратить внимание на аргумент 0L: вместо 0L можно было бы написать (long)0 или, если функция lseek должным образом объявлена, просто 0. Благодаря lseek с файлами можно работать так, как будто это большие массивы, правда, с замедленным доступом. Возвращаемое функцией lseek значение имеет тип long и является новой позицией в файле или, в случае ошибки, равно -1. Функция fseek из стандартной библиотеки аналогична lseek: от последней она отличается тем, что в случае ошибки возвращает некоторое ненулевое значение, а ее первый аргумент имеет тип FILE*. 4.22.6. Сравнение файлового вводавывода и вводавывода системного уровняКогда следует пользоваться функциями файлового вводавывода, а когда функциями вводавывода системного уровня? Надо исходить из ответов на два вопроса: что является более естественным для задачи? что более эффективно? При работе с текстовыми файлами удобнее пользоваться функциями файлового вводавывода, в особенности функциями форматированного вводавывода. Основным недостатком функций файлового вводавывода является то, что они нередко состоят из большого числа команд и резервируют больше памяти для данных, чем функции вводавывода системного уровня. Поэтому при использовании функций файлового вводавывода программа будет большего размера. При работе с бинарными данными удобнее пользоваться функциями системного уровня. Этими функциями полезно пользоваться также тогда, когда программа становится слишком большой, либо когда применение функций вводавывода верхнего уровня не дает ощутимого преимущества. Функции вводавывода нижнего уровня содержат меньше команд и резервируют меньше памяти для данных и могут в зависимости от деталей реализации оказаться более эффективными. |