Чтение и запись данных в файл в бинарном режиме

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

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

Итак, что такое бинарный режим? Мы с вами об этом подробно уже говорили на курсе по языку Си. Кратко напомню. Бинарный режим позволяет сохранять данные в файл в том же виде, в котором они представлены в ячейках памяти устройства. То есть, мы можем просто прочитать данные из памяти и перенести их в файл. И, наоборот, прочитать данные из файла и занести их в нужные ячейки памяти. В результате, сложные структуры данных достаточно просто можно сохранять и загружать из файла.

Давайте предположим, что нам в программе нужно сохранять массив из вещественных чисел в файл. Такие «не простые» наборы данных удобнее всего сохранять и, затем, читать в бинарном режиме доступа:

#include <iostream>
#include <fstream>
 
using std::ios;
 
int main()
{
    double pow[] {4.3, -54.33, 0.01};
 
    std::ofstream ofs("out_course.dat", ios::out | ios::binary);
 
    if(ofs.is_open()) {
    }
 
    ofs.close();
    return 0;
}

Если сейчас попытаться сохранить массив pow, используя операцию <<:

    if(ofs.is_open()) {
        ofs << pow;
    }

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

    if(ofs.is_open()) {
        ofs.write((char *)pow, sizeof(pow));
    }

Этот метод работает по аналогии с функцией write() языка Си. Первым аргументом передается адрес области памяти, которая переносится в файл, а вторым аргументом – размер сохраняемого фрагмента. В нашем случае – это размер массива pow в байтах.

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

    // ------------- чтение данных из файла --------------------------------
    double read_pow[5] {0};
 
    std::ifstream ifs("out_course.dat", ios::in | ios::binary);
 
    if(ifs.is_open()) {
        ifs.read((char *)read_pow, sizeof(pow));
    }
 
    for(double x : read_pow)
        std::cout << x << " ";
 
    ifs.close()

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

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

4.3 -54.33 0.01 0 0

Как видим, данные были успешно прочитаны.

Чтение и запись структур в файл

Давайте, для лучшего понимания, приведу еще один практический пример использования бинарного режима доступа. Пусть у нас объявлена следующая структура:

struct person {
    char fio[100];
    short old;
    unsigned int salary;
    double weight;
};

И, затем, в программе выполняется сохранение массива таких структур и их последующее чтение следующим образом:

int main()
{
    struct person ps[] {{"Sergey Balakirev", 102, 1000001, 82.6},
                        {"Bjarne Stroustrup", 56, 100001, 78.2},
                        {"Dennis Ritchie", 62, 10001, 88.9},
                        {"Kenneth Thompson", 58, 10002, 75.3},
                    };
 
    std::ofstream ofs("out_course.dat", ios::out | ios::binary);
 
    if(ofs.is_open()) {
        for(auto& p : ps)
            ofs.write((char *)&p, sizeof(p));
    }
 
    ofs.close();
 
    // ------------- чтение данных из файла --------------------------------
    struct person ps_r[10];
    int count = 0;
 
    std::ifstream ifs("out_course.dat", ios::in | ios::binary);
 
    if(ifs.is_open()) {
        while(ifs.read((char *)&ps_r[count], sizeof(person))) {
            count++;
        }
    }
 
    ifs.close();
 
    std::cout << count << std::endl;
    for(int i = 0; i < count; ++i) {
        std::cout << ps_r[i].fio << "\n";
        std::cout << ps_r[i].old << " " << ps_r[i].salary << " " << ps_r[i].weight << std::endl;
    }
 
    return 0;
}

Вначале мы перебираем поэлементно все элементы массива ps и с помощью метода write() записываем друг за другом в файл. После этого объявляем еще один массив таких же структур и в цикле поэлементно читаем данные из файла в этот новый массив.

Обратите внимание, как выполняется чтение данных. Условием окончания цикла while является возвращаемое значение метода read(). Этот метод вернет 0 (false), когда данные в файле закончатся. Таким образом, мы прочитаем ровно столько же порций данных, сколько было записано. Вывод результата в консоль это подтверждает.

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

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

Видео по теме