Почему запись текстового файла на C не дает ожидаемых результатов?

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

В частности, я пытаюсь использовать следующий код для записи текстового файла:

#include <stdio.h>

int main() {
    FILE *ptr_myfile;
    char c = 'a';
    int numero = 12345;

    ptr_myfile = fopen("test.txt","w");

    if (!ptr_myfile){
        printf("Unable to open file!");
        return 1;
    }

    fwrite(&c, sizeof(char), 1, ptr_myfile);
    fwrite(&numero, sizeof(int), 1, ptr_myfile);

    fclose(ptr_myfile);

    return 0;
}

При выполнении «cat test.txt» я обнаружил, что содержимое файла:

кот тест.txt

а90

Не могу понять, как 12345 преобразовали в 90.

Более того, если я сделаю

hexdump test.txt

0000000 3961 0030 0000
0000005

В этом случае я нахожу первый байт, записанный со значением 39. Почему? Второе значение (61) уже соответствует ascii-значению fo 'a'' (61 hex = 97 dec = 'a' ascii-код), но не может найти логическое объяснение остальным битам.

Если я изменю режим записи на двоичный файл, изменив строку

ptr_myfile=fopen("test.txt","w")  by ptr_myfile=fopen("test.txt","wb")

Я не вижу никаких изменений в поведении записанного содержимого файла.

С помощью fwrite вы записываете необработанные двоичные данные значений, а не их текстовые представления. А для int это обычно четыре байта данных.

Some programmer dude 08.01.2023 13:04

Подсказка: 12345 в шестнадцатеричном формате — это 3039.

n. m. 08.01.2023 13:06

Используйте hexdump -C, это должно быть менее запутанным

Mat 08.01.2023 13:06

@Someprogrammerdude, так что с fwrite я всегда пишу в двоичном формате, независимо от того, в каком режиме я открывал файл? Вы говорите, что я рассматриваю файл как двоичный вместо текста? Почему игнорируется режим «fopen»?

AndresG 08.01.2023 13:14

Да, это правильно. Десятичное значение 12345 будет записано как четыре байта 0x00003039. Если вы хотите написать текст, используйте, например. fprintf лайк fprintf(ptr_myfile, "%c%d", c, numero)

Some programmer dude 08.01.2023 13:16

"Почему игнорируется режим "fopen"?" Прочитайте (не гадайте, а прочитайте), что на самом деле означает режим fopen.

n. m. 08.01.2023 14:04

@н.м. спасибо, с stackoverflow.com/questions/43777913/… Я понимаю, что единственная разница заключается в том, как преобразуются несколько символов (т.е. конец строки), либо в \r\n в текстовом режиме, либо просто \n в двоичный), но это не влияет на запись необработанных данных или текста... (я неправильно это понял).

AndresG 08.01.2023 14:43
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
78
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Содержимое файла test.txt:

$ hexdump -C test.txt

00000000  61 39 30 00 00                                    |a90..|
00000005

Первый байт 61 — это 'a', а байты после него — это прямое представление 12345.

39 30 00 00 — это 4 байта, что является типичным размером для int.

Обратите внимание, что это число не 0x39300000, а 0x00003039.

Порядок байтов записанного числа зависит от порядка байтов вашей системы.

Вы можете наблюдать это сами, используя htonl для преобразования порядка следования байтов хоста в порядок следования байтов (сетевой порядок байтов):

#include <stdio.h>

int main() {
    FILE *ptr_myfile;
    char c = 'a';
    int numero = 12345;
    ptr_myfile = fopen("test.txt","w");

    if (!ptr_myfile) {
        printf("Unable to open file!");
        return 1;
    }

    // convert from host endianness to network byte order
    int numero_big_endian = htonl(numero);

    fwrite(&c, sizeof(char), 1, ptr_myfile);
    fwrite(&numero_big_endian, sizeof(int), 1, ptr_myfile);
    fclose(ptr_myfile);

    return 0;
}

Это даст:

$ hexdump -C test.txt

00000000  61 00 00 30 39                                    |a..09|
00000005

Как видите, порядок байтов теперь обратный.

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

Система с прямым порядком байтов распознает 0x00003039 как 0x39300000, что будет 959447040, а не 1234.

Как уже упоминалось, fwrite не записывает данные в строковое представление.

Если вы хотите, вы можете использовать snprintf (или использовать fprintf), чтобы сначала преобразовать число в строку, а затем записать его в файл:

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

int main() {
    FILE *ptr_myfile;
    char c = 'a';
    int numero = 12345;
    ptr_myfile = fopen("test.txt","w");

    if (!ptr_myfile) {
        printf("Unable to open file!");
        return 1;
    }

    // convert numero to a string
    char numero_str[64];
    // check result of snprintf, omitted for readability
    snprintf(numero_str, sizeof(numero_str), "%d", numero);

    fwrite(&c, sizeof(char), 1, ptr_myfile);
    fwrite(numero_str, strlen(numero_str), 1, ptr_myfile);
    fclose(ptr_myfile);

    return 0;
}
$ cat test.txt

a12345

Когда вы используете fwrite, функция записи обрабатывает данные, как если бы они были двоичными определенной длины. Это никак не связано с выбранным вами ранее режимом открытия файла.

Рассмотрим следующий пример:

/** A character buffer. */
char *ascii_buf = "ABCD";

/** A buffer which contains binary representation of A, B, C, D letters in ASCII. */
uint8_t binary_buf[4] = { 65, 66, 67, 68 };

written = fwrite(ascii_buf, 1, strlen(ascii_buf), fout);
written = fwrite(binary_buf, 1, sizeof(binary_buf), fout);

Вышеупомянутые два вызова fwrite приводят к одному и тому же выводу "ABCD" в целевой выходной файл.

Единственная разница заключается в том, как интерпретируются данные. В первом случае ascii_buf данные интерпретируются как символ. В то время как во втором случае binary_buf данные интерпретируются как целые числа без знака. Там содержание одинаковое, но их представление разное.

Обычно вы хотите использовать:

  • fprintf для вывода отформатированных строк в файл.
  • fwrite для вывода необработанных данных в файл.

Другие вопросы по теме