Функции feof(), fflush(), setvbuf()

Практический курс по C/C++: https://stepik.org/course/193691

Давайте представим, что нам нужно из файла data_rubusd2.csv прочитать данные в более сложном формате:

rub/usd
78.43; 78.65; 78.90; 80.10; 79.88; 80.23; 80.67; 81.22; 82.34; 81.54
rub/eur
88.43; 88.65; 88.90; 88.10; 89.88; 88.23; 88.67; 89.22; 90.34; 90.54; 90.23; 89.10
rub/yan
9.23; 9.44; 9.56; 9.32; 9.88; 10.01; 10.20; 10.11; 10.54; 10.78; 11.0; 11.04

То есть, перед данными идут еще и заголовки. Очевидно, что заголовки можно прочитать функцией fgets(), а последующие числовые данные, с помощью функции fscanf(). Получаем следующую программу:

#include <stdio.h>
 
enum {max_length=100};
 
int main(void)
{
    double data[max_length];
    char buffer[max_length];
    int length = 0;
 
    FILE* fp = fopen("data_rubusd2.csv", "r");
    if(fp == NULL) {
        perror("data_rubusd2.csv");
        return 1;
    }
 
    while(!feof(fp)) {
        fgets(buffer, sizeof(buffer), fp);
 
        length = 0;
        while(fscanf(fp, "%lf ; ", &data[length]) == 1)
            length++;
 
        puts(buffer);
        for(int i = 0;i < length;++i)
            printf("%.2f ", data[i]);
        putchar('\n');
    }
 
    fclose(fp);
    return 0;
}

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

Функция fflush()

Следующая функция, которая бывает полезна при работе с файлами, – это fflush():

int fflush(FILE* stream);

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

Давайте на простом примере посмотрим на принцип ее работы:

#include <stdio.h>
 
int main(void)
{
    int data[] = {1, 2, 3, 4, 5, 4, 3, 2, 1, -1};
    int length = sizeof(data) / sizeof(*data);
 
    FILE* fp = fopen("write_and_read.dat", "w");
    FILE* in = fopen("write_and_read.dat", "r");
    if(fp == NULL || in == NULL) {
        perror("write_and_read.dat");
        return 1;
    }
 
    for(int i = 0;i < length;++i)
        fprintf(fp, "%d ", data[i]);
 
    fflush(fp);
 
    int value;
    for(int i = 0;i < length;++i)
        if(fscanf(in, "%d ", &value) == 1)
            printf("%d ", value);
 
    fclose(fp);
    fclose(in);
 
    return 0;
}

Мы здесь открыли для одного и того же файла write_and_read.dat сразу два потока: на запись и на чтение. Затем, записали целочисленные значения в выходной поток, они были занесены в буфер этого потока и далее, чтобы перенести данные физически в файл, вызывается функция fflush() для выходного потока. После этого входной поток читает данные из файла и выводит их на экран:

1 2 3 4 5 4 3 2 1 -1

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

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

Функция setvbuf()

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

int setvbuf(FILE * restrict stream, char * restrict buf, int mode, size_t size);

Здесь указатель buf – это адрес области памяти для буфера (если равен NULL, то буфер создается автоматически); mode – режим работы буфера; size – размер буфера в байтах. Режимы определены следующими константами:

  • _IOFBF – полная буферизация (очистка после заполнения буфера);
  • _IOLBF – построчная буферизация (очистка по символу переноса строки);
  • _IONBF – выключение буферизации.

Функция setvbuf() должна вызываться после создания потока, но перед использованием какой-либо операции записи/чтения данных из этого потока.

Давайте посмотрим на пример использования этой функции:

#include <stdio.h>
 
enum {buffer_size=512, data_size=100};
 
int main(void)
{
    char buffer[buffer_size] = {0};
    char data[data_size];
 
    FILE* fp = fopen("data_rubusd2.csv", "r");
    if(fp == NULL) {
        perror("data_rubusd2.csv");
        return 1;
    }
 
    if(setvbuf(fp, buffer, _IOFBF, buffer_size) != 0) {
        puts("Incorrect type or size of buffer");
        return 2;
    }
 
    fgets(data, sizeof(data), fp);
    puts(buffer);
    puts("----------------");
    puts(data);
 
    fclose(fp);
    return 0;
}

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

Если отключить буферизацию (использовать режим _IONBF), то буфер будет пустым.

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

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме