Функции fputs(), fgets() и fprintf(), fscanf()

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

На этом занятии речь пойдет о функциях записи и считывания строк. Начнем с функций:

int fputs(const char* s, FILE* stream);
char* fgets(char* s, int size, FILE* stream);

определенных в заголовочном файле stdio.h. Первая функция fputs() служит для записи строк, а вторая fgets() для их считывания. Функция fputs() возвращает -1 в случае ошибки и другое значение при успешной записи данных. Обычно другим значением является число записанных символов, но стандартом языка Си это не оговорено. Функция fgets() возвращает переданный указатель s или NULL, если не удалось прочитать ни одного значения.

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

– Скажи-ка, дядя, ведь не даром
Я Python, Си учил с каналом
Балакирев что раздавал?

Сделать это можно следующим образом:

#include <stdio.h>
 
int main(void)
{
    const char* str[] = {
        "- Скажи-ка, дядя, ведь не даром",
        "Я Python, Си учил с каналом",
        "Балакирев что раздавал?"
    };
 
    FILE* fp = fopen("my_file.txt", "w");
    if(fp == NULL) {
        perror("my_file.txt");
        return 1;
    }
 
    for(int i = 0;i < sizeof(str)/sizeof(*str); ++i)
        fputs(str[i], fp);
 
    fclose(fp);
    return 0;
}

Мы здесь в цикле перебираем все строки и с помощью функции fputs() записываем их целиком в файл. В результате получим следующее содержимое:

- Скажи-ка, дядя, ведь не даромЯ Python, Си учил с каналомБалакирев что раздавал?

Как видите, все выстроилось в одну строчку без переноса строк. И это не удивительно, так как функция fputs() записывает данные так, как они есть, пока не встретит символ конца строки ‘\0’. Никаких переносов она автоматом не добавляет. Поэтому, если мы хотим их иметь в выходном файле, нужно об этом позаботиться самостоятельно и записать цикл, например, так:

    for(int i = 0;i < sizeof(str)/sizeof(*str); ++i) {
        fputs(str[i], fp);
        fputc('\n', fp);
    }

Получим ожидаемый вид текстового файла.

Теперь давайте прочитаем все строки из этого файла и выведем их на экран:

#include <stdio.h>
 
int main(void)
{
    char buffer[100];
 
    FILE* fp = fopen("my_file.txt", "r");
    if(fp == NULL) {
        perror("my_file.txt");
        return 1;
    }
 
    while(fgets(buffer, sizeof(buffer), fp))
        puts(buffer);
 
    fclose(fp);
    return 0;
}

Мы открываем файл на чтение и в цикле выполняем функцию fgets(), пока она возвращает значение отличное от NULL, то есть, цикл while будет продолжаться, пока данные в файле присутствуют. На каждой итерации читается строка из файла. При этом признаком конца строки является символ переноса ‘\n’, либо достижение конца файла. Функция fgets() максимум читает:

sizeof(buffer) - 1

символ так, чтобы оставалось место для последнего символа конца строки ‘\0’. Этот символ функция fgets() всегда автоматически добавляет после последнего прочитанного значения. Поэтому в buffer формируется корректная строка языка Си.

После выполнения программы увидим следующий результат:

- Скажи-ка, дядя, ведь не даром

Я Python, Си учил с каналом

Балакирев что раздавал?

Откуда взялись пустые строки? Дело в том, что символ переноса строки, прочитанный из файла, попадает в строку buffer. Функция fgets() ничего не убирает. А функция puts() при выводе строк добавляет еще один символ переноса строки. Получается два переноса, которые мы наблюдаем в виде пустых строк. По идее, если мы читаем строки из файла, то нам следовало бы убирать символ переноса. В самом простом варианте сделать это можно с помощью строковой функции strchr() следующим образом:

    while(fgets(buffer, sizeof(buffer), fp)) {
        char *ptr = strchr(buffer, '\n');
        if(ptr != NULL)
            *ptr = '\0';
 
        puts(buffer);
    }

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

- Скажи-ка, дядя, ведь не даром
Я Python, Си учил с каналом
Балакирев что раздавал?

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

Далее рассмотрим следующие две функции для форматной записи и чтения данных:

int fprintf(FILE* stream, const char* format, ...);
int fscanf(FILE* stream, const char* format, ...);

Мы уже знакомы с вами с функциями printf() и scanf(). Так вот, функции fprintf() и fscanf() работают аналогичным образом, только с дополнительным указанием потока stream. Обычно, это файловые потоки, но мы можем прописывать и стандартные stdout и stdin. Тогда fprintf() и fscanf() будут полными аналогами функций printf() и scanf().

Зачем нужны такие функции? Обычно они используются, когда нужно записать данные в файл в определенном формате или прочитать данные по определенному формату. Например, можно сделать экспорт excel-файла в текстовый формат csv следующего вида:

78.43; 78.65; 78.90; 80.10; 79.88; 80.23; 80.67; 81.22; 82.34; 81.54

Предположим, это котировки рубль/доллар и сохранены в файле с именем data_rubusd.csv. Тогда для чтения этих вещественных чисел удобно воспользоваться функцией fscanf(). Получим:

#include <stdio.h>
 
enum {max_length=1024};
 
int main(void)
{
    double rub_usd[max_length] = {0.0};
    unsigned length = 0;
 
    FILE* fp = fopen("data_rubusd.csv", "r");
    if(fp == NULL) {
        perror("data_rubusd.csv");
        return 1;
    }
 
    while(fscanf(fp, "%lf ; ", &rub_usd[length]) == 1)
        length++;
 
    fclose(fp);
 
    for(int i = 0; i < length; ++i)
        printf("%.3f ", rub_usd[i]);
 
    return 0;
}

Мы здесь делаем цикл, пока функция fscanf() успешно читает данные. Так как на каждой итерации читается только одно значение, то делаем цикл, пока возвращается число 1. В теле цикла увеличиваем счетчик прочитанных данных length на единицу.

После запуска программы, увидим:

78.430 78.650 78.900 80.100 79.880 80.230 80.670 81.220 82.340 81.540

Все данные были прочитаны успешно.

Для такой же форматной записи в файл удобно воспользоваться функцией fprintf(). Например, у нас имеется список из номеров телефонов и мы их хотим записать в файл в определенном формате:

#include <stdio.h>
 
int main(void)
{
    const char* phones[][5] = {
        {"8", "917", "123", "45", "67"},
        {"8", "904", "123", "45", "68"},
        {"8", "906", "123", "45", "69"}
    };
 
    FILE* fp = fopen("my_file.txt", "w");
    if(fp == NULL) {
        perror("my_file.txt");
        return 1;
    }
 
    for(int i = 0;i < sizeof(phones) / sizeof(*phones); ++i)
        fprintf(fp,"%s(%s)%s-%s-%s\n", phones[i][0], phones[i][1], phones[i][2],
                                    phones[i][3], phones[i][4], phones[i][5]);
 
    fclose(fp);
 
    return 0;
}

В файле my_file.txt увидим следующий результат:

8(917)123-45-67
8(904)123-45-68
8(906)123-45-69

Вот общий принцип использования функций форматного ввода/вывода информации из файла.

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

Видео по теме