Содержание | <<< | >>>

Основы файловой системы

Файловая система языка С состоит из нескольких взаимосвязанных функций. Самые распространенные из них показаны в табл. 9.1. Для их работы требуется заголовок <stdio.h>.

Таблица 9.1. Часто используемые функции файловой системы С
ИмяЧто делает
fopen()Открывает файл
fclose()Закрывает файл
putc()Записывает символ в файл
fputc()To же, что и putc()
getc()Читает символ из файла
fgetc()To же, что и getc()
fgets()Читает строку из файла
fputs()Записывает строку в файл
fseek()Устанавливает указатель текущей позиции на определенный байт файла
ftell()Возвращает текущее значение указателя текущей позиции в файле
fprintf()Для файла то же, что printf() для консоли
fscanf()Для файла то же, что scanf() для консоли
feof()Возвращает значение true (истина), если достигнут конец файла
ferror()Возвращает значение true, если произошла ошибка
rewind()Устанавливает указатель текущей позиции в начало файла
remove()Стирает файл
fflush()Дозапись потока в файл

Заголовок <stdio.h> предоставляет прототипы функций ввода/вывода и определяет следующие три типа: size_t, fpos_t и FILE. size_t и fpos_t представляют собой определенные разновидности такого типа, как целое без знака. А о третьем типе, FILE, рассказывается в следующем разделе.

Кроме того, в <stdio.h> определяется несколько макросов. Из них к материалу этой главы относятся NULL, EOF, FOPEN_MAX, SEEK_SET, SEEK_CUR и SEEK_END. Макрос NULL определяет пустой (null) указатель. Макрос EOF, часто определяемый как -1, является значением, возвращаемым тогда, когда функция ввода пытается выполнить чтение после конца файла. FOPEN_MAX определяет целое значение, равное максимальному числу одновременно открытых файлов. Другие макросы используются вместе с fseek() — функцией, выполняющей операции прямого доступа к файлу.

Указатель файла

Указатель файла — это то, что соединяет в единое целое всю систему ввода/вывода языка С. Указатель файла — это указатель на структуру типа FILE. Он указывает на структуру, содержащую различные сведения о файле, например, его имя, статус и указатель текущей позиции в начало файла. В сущности, указатель файла определяет конкретный файл и используется соответствующим потоком при выполнении функций ввода/вывода. Чтобы выполнять в файлах операции чтения и записи, программы должны использовать указатели соответствующих файлов. Чтобы объявить переменную-указатель файла, используйте такого рода оператор:

FILE *fp;

Открытие файла

Функция fopen() открывает поток и связывает с этим потоком определенный файл. Затем она возвращает указатель этого файла. Чаще всего (а также в оставшейся части этой главы) под файлом подразумевается дисковый файл. Прототип функции fopen() такой:

FILE *fopen(const char *имя_файла, const char *режим);

где имя_файла — это указатель на строку символов, представляющую собой допустимое имя файла, в которое также может входить спецификация пути к этому файлу. Строка, на которую указывает режим, определяет, каким образом файл будет открыт. В табл. 9.2 показано, какие значения строки режим являются допустимыми. Строки, подобные "r+b" могут быть представлены и в виде "rb+".

Таблица 9.2. Допустимые значения режим
РежимЧто означает
rОткрыть текстовый файл для чтения
wСоздать текстовый файл для записи
aДобавить в конец текстового файла
rbОткрыть двоичный файл для чтения
wbСоздать двоичный файл для записи
abДобавить в конец двоичного файла
r+Открыть текстовый файл для чтения/записи
w+Создать текстовый файл для чтения/записи
a+Добавить в конец текстового файла или создать текстовый файл для чтения/записи
r+bОткрыть двоичный файл для чтения/записи
w+bСоздать двоичный файл для чтения/записи
a+bДобавить в конец двоичного файла или создать двоичный файл для чтения/записи

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

В следующем коде функция fopen() используется для открытия файла по имени TEST для записи.

FILE *fp;
fp = fopen("test", "w");

Хотя предыдущий код технически правильный, но его обычно пишут немного по-другому:

FILE *fp;

if ((fp = fopen("test","w"))==NULL) {
  printf("Ошибка при открытии файла.\n");
  exit(1);
}

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

Хотя название большинства файловых режимов объясняет их смысл, однако не помешает сделать некоторые дополнения. Если попытаться открыть файл только для чтения, а он не существует, то работа fopen() завершится отказом. А если попытаться открыть файл в режиме дозаписи, а сам этот файл не существует, то он просто будет создан. Более того, если файл открыт в режиме дозаписи, то все новые данные, которые записываются в него, будут добавляться в конец файла. Содержимое, которое хранилось в нем до открытия (если только оно было), изменено не будет. Далее, если файл открывают для записи, но выясняется, что он не существует, то он будет создан. А если он существует, то содержимое, которое хранилось в нем до открытия, будет утеряно, причем будет создан новый файл. Разница между режимами r+ и w+ состоит в том, что если файл не существует, то в режиме открытия r+ он создан не будет, а в режиме w+ все произойдет наоборот: файл будет создан! Более того, если файл уже существует, то открытие его в режиме w+ приведет к утрате его содержимого, а в режиме r+ оно останется нетронутым.

Из табл. 9.2 видно, что файл можно открыть либо в одном из текстовых, либо в одном из двоичных режимов. В большинстве реализаций в текстовых режимах каждая комбинация кодов возврата каретки (ASCII 13) и конца строки (ASCII 10) преобразуется при вводе в символ новой строки. При выводе же происходит обратный процесс: символы новой строки преобразуются в комбинацию кодов возврата каретки (ASCII 13) и конца строки (ASCII 10). В двоичных режимах такие преобразования не выполняются.

Максимальное число одновременно открытых файлов определяется FOPEN_MAX. Это значение не меньше 8, но чему оно точно равняется — это должно быть написано в документации по компилятору.

Закрытие файла

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

int fclose(FILE *уф);

где уф — указатель файла, возвращенный в результате вызова fopen(). Возвращение нуля означает успешную операцию закрытия. В случае же ошибки возвращается EOF. Чтобы точно узнать, в чем причина этой ошибки, можно использовать стандартную функцию ferror() (о которой вскоре пойдет речь). Обычно отказ при выполнении fclose() происходит только тогда, когда диск был преждевременно удален (стерт) с дисковода или на диске не осталось свободного места.

Запись символа

В системе ввода/вывода языка С определяются две эквивалентные функции, предназначенные для вывода символов: putc() и fputc(). (На самом деле putc() обычно реализуется в виде макроса.) Две идентичные функции имеются просто потому, чтобы сохранять совместимость со старыми версиями С. В этой книге используется putc(), но применение fputc() также вполне возможно.

Функция putc() записывает символы в файл, который с помощью fopen() уже открыт в режиме записи. Прототип этой функции следующий:

int putc(int ch, FILE *уф);

где уф — это указатель файла, возвращенный функцией fopen(), a ch — выводимый символ. Указатель файла сообщает putc(), в какой именно файл следует записывать символ. Хотя ch и определяется как int, однако записывается только младший байт.

Если функция putc() выполнилась успешно, то возвращается записанный символ. В противном же случае возвращается EOF.

Чтение символа

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

Функция getc() записывает символы в файл, который с помощью fopen() уже открыт в режиме для чтения. Прототип этой функции следующий:

int getc(FILE *уф);

где уф — это указатель файла, имеющий тип FILE и возвращенный функцией fopen(). Функция getc() возвращает целое значение, но символ находится в младшем байте. Если не произошла ошибка, то старший байт (байты) будет обнулен.

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

do {
  ch = getc(fp);
} while(ch!=EOF);

Однако getc() возвращает EOF и в случае ошибки. Для определения того, что же на самом деле произошло, можно использовать ferror().

Использование fopen(), getc(), putc(), и fclose()

Функции fopen(), getc(), putc() и fclose() — это минимальный набор функций для операций с файлами. Следующая программа, KTOD, представляет собой простой пример, в котором используются только функции putc(), fopen() и fclose(). В этой программе символы считываются с клавиатуры и записываются в дисковый файл до тех пор, пока пользователь не введет знак доллара. Имя файла определяется в командной строке. Например, если вызвать программу KTOD, введя в командной строке KTOD TEST, то строки текста будут вводиться в файл TEST.

/* KTOD: программа ввода с клавиатуры на диск. */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  FILE *fp;
  char ch;

  if(argc!=2) {
    printf("Вы забыли ввести имя файла.\n");
    exit(1);
  }

  if((fp=fopen(argv[1], "w"))==NULL) {
    printf("Ошибка при открытии файла.\n");
    exit(1);
  }

  do {
    ch = getchar();
    putc(ch, fp);
  } while (ch != '$');

  fclose(fp);

  return 0;
}

Программа DTOS, являющаяся дополнением к программе KTOD, читает любой текстовый файл и выводит его содержимое на экран.

/* DTOS: программа, которая читает файлы
         и выводит их на экран. */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  FILE *fp;
  char ch;

  if(argc!=2) {
    printf("Вы забыли ввести имя файла.\n");
    exit(1);
  }

  if((fp=fopen(argv[1], "r"))==NULL) {
    printf("Ошибка при открытии файла.\n");
    exit(1);
  }

  ch = getc(fp);   /* чтение одного символа */

  while (ch!=EOF) {
    putchar(ch);  /* вывод на экран */
    ch = getc(fp);
  }

  fclose(fp);

  return 0;
}

Испытывая эти две программы, вначале с помошью KTOD создайте текстовый файл, а затем с помошью DTOS прочитайте его содержимое.

Использование feof()

Как уже говорилось, если достигнут конец файла, то getc() возвращает EOF. Однако проверка значения, возвращенного getc(), возможно, не является наилучшим способом узнать, достигнут ли конец файла. Во-первых, файловая система языка С может работать как с текстовыми, так и с двоичными файлами. Когда файл открывается для двоичного ввода, то может быть прочитано целое значение, которое, как выяснится при проверке, равняется EOF. В таком случае программа ввода сообщит о том, что достигнут конец файла, чего на самом деле может и не быть. Во-вторых, функция getc() возвращает EOF и в случае отказа, а не только тогда, когда достигнут конец файла. Если использовать только возвращаемое значение getc(), то невозможно определить, что же на самом деле произошло. Для решения этой проблемы в С имеется функция feof(), которая определяет, достигнут ли конец файла. Прототип функции feof() такой:

int feof(FILE *уф);

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

while(!feof(fp)) ch = getc(fp);

Ясно, что этот метод можно применять как к двоичным, так и к текстовым файлам.

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

/* Копирование файла. */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  FILE *in, *out;
  char ch;

  if(argc!=3) {
    printf("Вы забыли ввести имя файла.\n");
    exit(1);
  }

  if((in=fopen(argv[1], "rb"))==NULL) {
    printf("Нельзя открыть исходный файл.\n");
    exit(1);
  }
  if((out=fopen(argv[2], "wb")) == NULL) {
    printf("Нельзя открыть файл результатов.\n");
    exit(1);
  }

  /* Именно этот код копирует файл. */
  while(!feof(in)) {
    ch = getc(in);
    if(!feof(in)) putc(ch, out);
  }

  fclose(in);
  fclose(out);

  return 0;
}

Ввод / вывод строк: fputs() и fgets()

Кроме getc() и putc(), в языке С также поддерживаются родственные им функции fgets() и fputs(). Первая из них читает строки символов из файла на диске, а вторая записывает строки такого же типа в файл, тоже находящийся на диске. Эти функции работают почти как putc() и getc(), но читают и записывают не один символ, а целую строку. Прототипы функций fgets() и fputs() следующие:

int fputs(const char *cmp, FILE *уф);
char *fgets(char *cmp, int длина, FILE *уф);

Функция fputs() пишет в определенный поток строку, на которую указывает cmp. В случае ошибки эта функция возвращает EOF.

Функция fgets() читает из определенного потока строку, и делает это до тех пор, пока не будет прочитан символ новой строки или количество прочитанных символов не станет равным длина-1. Если был прочитан разделитель строк, он записывается в строку, чем функция fgets() отличается от функции gets(). Полученная в результате строка будет оканчиваться символом конца строки ('0'). При успешном завершении работы функция возвращает cmp, а в случае ошибки — пустой указатель (null).

В следующей программе показано использование функции fputs(). Она читает строки с клавиатуры и записывает их в файл, который называется TEST. Чтобы завершить выполнение программы, введите пустую строку. Так как функция gets() не записывает разделитель строк, то его приходится специально вставлять перед каждой строкой, записываемой в файл; это делается для того, чтобы файл было легче читать:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
  char str[80];
  FILE *fp;

  if((fp = fopen("TEST", "w"))==NULL) {
    printf("Ошибка при открытии файла.\n");
    exit(1);
  }

  do {
    printf("Введите строку (пустую - для выхода):\n");
    gets(str);
    strcat(str, "\n");  /* добавление разделителя строк */
    fputs(str, fp);
  } while(*str!='\n');

  return 0;
}

Функция rewind()

Функция rewind() устанавливает указатель текущей позиции в файле на начало файла, указанного в качестве аргумента этой функции. Иными словами, функция rewind() выполняет "перемотку" (rewind) файла. Вот ее прототип:

void rewind(FILE *уф);

где уф — это допустимый указатель файла.

Чтобы познакомиться с rewind(), изменим программу из предыдущего раздела таким образом, чтобы она отображала содержимое файла сразу после его создания. Чтобы выполнить отображение, программа после завершения ввода "перематывает" файл, а затем с помощью fback() читает его с самого начала. Обратите внимание, что сейчас файл необходимо открыть в режиме чтения/записи, используя в качестве аргумента, задающего режим, строку "w+".

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
  char str[80];
  FILE *fp;

  if((fp = fopen("TEST", "w+"))==NULL) {
    printf("Ошибка при открытии файла.\n");
    exit(1);
  }

  do {
    printf("Введите строку (пустую - для выхода):\n");
    gets(str);
    strcat(str, "\n");  /* ввод разделителя строк */
    fputs(str, fp);
  } while(*str!='\n');

  /* теперь выполняется чтение и отображение файла */
  rewind(fp);  /* установить указатель
                  текущей позиции на начало файла. */
  while(!feof(fp)) {
    fgets(str, 79, fp);
    printf(str);
  }

  return 0;
}

Функция ferror()

Функция ferror() определяет, произошла ли ошибка во время выполнения операции с файлом. Прототип этой функции следующий:

int ferror(FILE *уф);

где уф — допустимый указатель файла. Она возвращает значение true (истина), если при последней операции с файлом произошла ошибка; в противном же случае она возвращает false (ложь). Так как при любой операции с файлом устанавливается свое условие ошибки, то после каждой такой операции следует сразу вызывать ferror(), а иначе данные об ошибке могут быть потеряны.

В следующей программе показано применение ferror(). Программа удаляет табуляции из файла, заменяя их соответствующим количеством пробелов. Размер табуляции определяется макросом TAB_SIZE. Обратите внимание, что ferror() вызывается после каждой операции с файлом. При запуске этой программы указывайте в командной строке имена входного и выходного файлов.

/* Программа заменяет в текстовом файле символы
   табуляции пробелами и отслеживает ошибки. */

#include <stdio.h>
#include <stdlib.h>

#define TAB_SIZE 8
#define IN 0
#define OUT 1

void err(int e);

int main(int argc, char *argv[])
{
  FILE *in, *out;
  int tab, i;
  char ch;

  if(argc!=3) {
    printf("Синтаксис: detab <входной_файл> <выходной файл>\n");
    exit(1);
  }

  if((in = fopen(argv[1], "rb"))==NULL) {
    printf("Нельзя открыть %s.\n", argv[1]);
    exit(1);
  }

  if((out = fopen(argv[2], "wb"))==NULL) {
    printf("Нельзя открыть %s.\n", argv[2]);
    exit(1);
  }

  tab = 0;
  do {
    ch = getc(in);
    if(ferror(in)) err(IN);

    /* если найдена табуляция, выводится
       соответствующее число пробелов */
    if(ch=='\t') {
      for(i=tab; i<8; i++) {
        putc(' ', out);
        if(ferror(out)) err(OUT);
      }
      tab = 0;
    }
    else {
      putc(ch, out);
      if(ferror(out)) err(OUT);
      tab++;
      if(tab==TAB_SIZE) tab = 0;
      if(ch=='\n' || ch=='\r') tab = 0;
    }
  } while(!feof(in));
  fclose(in);
  fclose(out);

  return 0;
}

void err(int e)
{
  if(e==IN) printf("Ошибка при вводе.\n");
  else printf("Ошибка привыводе.\n");
  exit(1);
}

Стирание файлов

Функция remove() стирает указанный файл. Вот ее прототип:

int remove(const char *имя_файла);

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

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

/* Двойная проверка перед стиранием. */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int main(int argc, char *argv[])
{
  char str[80];

  if(argc!=2) {
    printf("Синтаксис: xerase <имя_файла>\n");
    exit(1);
  }

  printf("Стереть %s? (Y/N): ", argv[1]);
  gets(str);

  if(toupper(*str)=='Y')
    if(remove(argv[1])) {
      printf("Нельзя стиреть файл.\n");
      exit(1);
    }
  return 0; 
}

Дозапись потока

Для дозаписи содержимого выводного потока в файл применяется функция fflush(). Вот ее прототип:

int fflush(FILE *уф);

Эта функция записывает все данные, находящиеся в буфере в файл, который указан с помощью уф. При вызове функции fflush() с пустым (null) указателем файла уф будет выполнена дозапись во все файлы, открытые для вывода.

После своего успешного выполнения fflush() возвращает нуль, в противном случае — EOF.


Содержание | <<< | >>>