Есть два типа файлов: Двоичный и Текстовый.

Текстовый файл, по сути, представляет собой двоичный файл, предназначенный для работы с символами и символами, в то время как двоичный файл предназначен для работы с ... ну, двоичными файлами.

Текстовые файлы читать намного проще, потому что мы их понимаем. Однако двоичные файлы - нет, потому что мы, люди, не компьютеры, которые понимают последовательности нулей и единиц.

Условно для просмотра двоичных файлов мы используем шестнадцатеричные значения. Вот пример ниже, который показывает наш файл sample.txt в текстовой форме:

Hello world!

И в двоичной форме:

48 65 6C 6C 6F 20 77 6F 72 6C 64 21

Бинарные операции

Чтобы создать двоичный файл, мы создаем объект fstream, а затем передаем в качестве параметров имя файла, который мы хотим создать, и режимы работы с файлом.

fstream file("sample.bin", ios::in | ios::out | ios::binary | ios:: trunc);

В приведенном выше коде мы создаем объект fstream с именем sample.bin, где bin обозначает двоичный файл, а затем указываем, какие действия мы хотим сделать с этим файлом.

ios::in означает, что мы будем обрабатывать ввод в этот файл, например записывать данные.

ios::out означает, что мы будем принимать или читать содержимое этого файла.

ios::binary означает, что мы специально указываем, что будем работать с двоичными файлами, почему? Потому что по умолчанию объект fstream работает с текстом.

ios::trunc означает, что мы хотим, чтобы этот файл был пустым, если файл sample.bin уже существует, ios::trunc отбросит все, что находится внутри файла. Вы можете думать о ios::trunc как о clear() функции объекта string.

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

if (file.is_open()) {
    // some code...
    file.close();
}
else {
    cout << "Error! File cannot be opened!" << endl;

Теперь, когда у нас открыт файл, как нам что-то в него поместить / записать? Мы можем использовать функцию put() для ввода одного символа за раз или использовать функцию write() для записи блока данных.

Вот пример использования функции put():

file.put('a');

Выполнение этой строки кода даст нам файл с именем sample.bin, а внутри мы увидим a.

Как насчет того, чтобы записать кусок данных?

char str[20] = "Hello world!";
file.write(str, 20);

Здесь мы создали массив символов размером 20 и поместили внутрь строки Hello world!, а затем записали его в файл с помощью функцииwrite(). Технически первый параметр - это (если вы знаете об указателях, вы поймете) адрес первого элемента в массиве. Второй параметр - это размер массива, он равен 20.

Как насчет чтения данных? Легко, просто используйте get(), чтобы получить / взять один символ из файла, или используйте read(), чтобы прочитать блок данных. Они очень похожи на put() и write(), но вместо передачи данных для записи мы передаем переменную, в которую мы хотим поместить читаемые данные.

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

Для выполнения этой задачи есть 4 функции:

seekp(x) перемещает наш курсор ввода / чтения на x. Может быть отрицательным.

tellp() сообщает текущую позицию курсора чтения.

seekg(x) перемещает наш курсор получения / записи на x. Может быть отрицательным.

tellg() сообщает текущую позицию курсора записи.

Разница между частями p и g заключается в том, что мы используем p, когда выполняем операции вывода / чтения, а g - для операций ввода / записи файла.

seekp() и seekg() также имеют второй параметр, который мы можем дать, чтобы указать нашему курсору двигаться / искать из начала, конца или текущей позиции. Они принимают в качестве второго параметра следующие значения: ios_base::beg (поиск с начала), ios_base::end (поиск с конца) и ios_base::cur (поиск с текущей позиции).

Я отказывался от примеров, и это нехорошо. Вот пример того, как работают указанные выше функции:

char str[20] = "Hello world!";
file.write(str, 20);
// sample.bin:
// Hello world!\0\0\0\0\0\0\0\0
char name[10] = "Everyone!";
file.seekg(6, ios_base::beg);
file.write(name, 10);
// sample.bin:
// Hello Everyone!\0\0\0\0\0
char str2[20];
file.seekp(0, ios_base::beg);
file.read(str2, 20);
cout << str2 << endl;
// console:
// Hello Everyone!

Вам может быть интересно: что с этим \0? Это называется null или ничего. Это значение символа по умолчанию. Наша строка Hello world! состоит из 12 символов и не заполняет все в нашем массиве символов, следовательно, оставшиеся неиспользуемые символы имеют значение по умолчанию или null.

Двоичные файлы не игнорируют символы новой строки, нули и т. Д. В отличие от текстовых файлов. Они помещают их в двоичные файлы. Это и хорошо, и плохо. Плохо, потому что что, если мы хотим записать динамический массив, такой как строка, в наш двоичный файл? Нам также нужно будет указать его размер, прежде чем мы начнем писать. Мы не можем просто использовать null для обозначения конца строки, это не имеет смысла, потому что мы могли использовать его и для других целей.

Сохранение объекта структуры

В двоичных файлах хорошо то, что мы можем сохранять объекты структуры или класса. Например, у нас есть структура под названием person:

struct person {
    char name[30];
    int age;
};

И мы хотим сохранить его данные в нашем файле. Мы можем использовать для этого функцию write(), как? Взгляните на код ниже:

person p;
strcpy(p.name, "Some Name");
p.age = 10;
file.write((char*)& p, sizeof(person));

Сначала мы создали наш объект person с именем p. Затем мы присвоили его значения. Наконец, мы записали этот p объект в наш двоичный файл. Если мы попытаемся открыть наш двоичный файл, мы получим странные символы. В этом случае для просмотра этого файла лучше использовать шестнадцатеричные значения. И это должно выглядеть так:

53 6F 6D 65 20 4E 61 6D 65 00 FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE CC CC 0A 00 00 00

Вы можете сказать, что первые 30 байтов - это наша строка Some Name. После них идут два CC шестнадцатеричных значения, а затем 4 байта 0A 00 00 00. Эти последние 4 байта имеют прямой порядок байтов, и если мы попытаемся преобразовать их в прямой порядок байтов, у нас будет 00 00 00 0A, и это будет наше целое число! Почему же 4 байта? Большинство систем используют 4 байта для целых чисел. Это A в конце 10 в десятичной системе, и это наш номер!

Использование двоичных файлов также уменьшает пространство, как так? Если бы нам пришлось сохранить число в текстовый файл, например, число 10, оно будет распознано как два отдельных символа 1 и 0. И это будет 31 30 в двоичном формате, где мы могли бы просто использовать 0A. Вы могли подумать, что этот пример дешевый, целое число - 4 байта. Что ж, подумайте, если бы мы использовали число 2849294, то это 7 байт! Но с двоичными файлами это просто: 0E 7A 2B 00 или 4 байта.

(Char *) & дилемма

Если вы помните, функция write() и read() принимает в качестве первого параметра указатель символа. Другими словами, он принимает адрес. И с этого адреса он выполняет операции до указанного размера.

Почему он принимает указатель на символ? Потому что char - это один байт. Мы хотим работать с байтами побайтно, поэтому мы используем char.

Чтобы полностью понять концепцию, почему мы использовали (char*)& p в качестве первого параметра в приведенном выше коде. Вам нужно узнать об указателях. Они очень важны, если вы действительно хотите понять. Хотя я постараюсь объяснить это как можно короче и проще.

Вы когда-нибудь пробовали сделать что-то подобное: int* pnum = (int*)& num;? num здесь int переменная со значением 14. Сначала мы взяли адрес num, а затем разыменовали его, это кажется глупым, потому что указание на что-то и разыменование его даст нам значение num или 14. Однако мы делаем это не для того, чтобы поиграть с 14, а чтобы получить его значение в шестнадцатеричной форме. Да, мы получаем его значение в шестнадцатеричной форме! Должен сказать, довольно круто. Это также должно объяснить, почему мы получили 4 байта в нашем целочисленном ранее (0A 00 00 00).

Почему я объяснил это сейчас? С ясным примером из int*, мы теперь сможем полностью понять char*.

Одно замечание: даже если вы видите char, на самом деле мы не работаем с символами, мы работаем с байтами! Помните, мы работаем с байтами!

Теперь, когда вы понимаете концепцию (int*)& var, я думаю, вы уже понимаете, почему мы использовали (char*)& var.

Если нет, ну… Вот еще одно объяснение:

В коде: file.write((char*)& p, sizeof(person)); мы можем представить объект p как массив, содержащий как массив символов, или name,, и int, или age. Как массив, мы знаем, что его первый элемент является основным адресом массива. Теперь мы хотим записать этот массив в наш двоичный файл, используя write(), до указанного размера. Его размер легко узнать, достаточно воспользоваться функцией sizeof()!

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

И это все!

Чтение нашего двоичного файла

Теперь, когда мы сохранили содержимое нашего объекта p. Было бы лучше, если бы мы могли прочитать его и присвоить его значения новому person объекту!

Процедура такая же, но теперь мы используем read() вместо write():

person newp;
file.read((char * ) & newp, sizeof(person));
cout << "Name: " << newp.name << endl;
cout << "Age: " << newp.age << endl;

Видеть? Та же процедура.

А вот что классно в двоичных файлах:

Если структура двоичного файла неизвестна, его содержимое недоступно или неразборчиво.

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

Вывод

Мы говорили о том, что такое двоичные файлы, как их создавать и манипулировать ими с помощью seekg(), seekp(), tellg(), tellp(), put(), get(), write() и read().

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

Еще один факт:

Фактически, каждый файл, который вы видите, является двоичным файлом. Сделайте это файлами в формате mp3, mp4 или даже exe.

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

Я действительно ненавидел двоичные файлы, потому что думал, что они слишком сложны для понимания. Однако забавно и просто, как они работают, как сказал Simplicity is the ultimate sophistication Леонардо да Винчи. Я до сих пор не знаю, ошибочны ли некоторые из моих объяснений, и если да, пожалуйста, поправьте меня. Я на самом деле программист-самоучка и 2 года занимаюсь программированием дома. Узнаю кое-что здесь и там.

Я могу отредактировать эту статью в будущем, мне кажется, что некоторые вещи остались необъясненными или объяснены двусмысленно.

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