ошибка чтения/записи двоичного файла, содержащая указатель char* в структуре

У меня странная проблема. Я не могу предположить, почему это происходит. Я пробовал разными способами. Может быть, это потому, что я все еще новичок в языке c.

Пожалуйста, посмотрите на приведенный ниже код.

Он поставляется с 2 аргументами. --write и --read.

  • В моей функции write() я пишу в файл, а затем вызываю функцию read(). Это записывает данные в файл и правильно печатает 3 строки значений, как и ожидалось.

  • В моей функции read() я прочитал файл. Когда я передаю только аргумент --read, программа выдает сообщение об ошибке segmentation fault. Хотя в приведенном ниже коде, если я назначу значение статической строки char *name, эта функция чтения работает, как и ожидалось.

Ниже приведен мой полный код, который я создал для имитации моей проблемы.

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

typedef struct _student {
    int id;
    char *name;
} Student;

void write();
void read();

int main(int argc, char *argv[])
{
    if (argc > 1) {
        if (strcmp(argv[1], "--write") == 0) {
            write();
            read();
        }
        else if (strcmp(argv[1], "--read") == 0) {
            read();
        }
    }
    return 0;
}

void write()
{
    printf("Write\n");
    FILE *fp;

    // write student
    Student *std_writer = (Student *) malloc(sizeof(Student));
    std_writer->id = 10;
    //std_writer->name = "Alice"; // But if i remove the below 4 lines and uncommented this line, everything works as expected.
    char *a = "Alice";
    std_writer->name = malloc(20);
    memset(std_writer->name, '\0', 20);
    strncpy(std_writer->name, a, 5);

    fp = fopen("Student.file", "wb");
    fwrite(std_writer, sizeof(Student), 1, fp);
    fwrite(std_writer, sizeof(Student), 1, fp);
    fwrite(std_writer, sizeof(Student), 1, fp);
    fclose(fp);

    free(std_writer);
}

void read()
{
    printf("Read\n");
    FILE *fp;

    // read student
    Student *std_reader = (Student *) malloc(sizeof(Student));
    fp = fopen("Student.file", "rb");
    while(fread(std_reader, sizeof(Student), 1, fp) == 1) {
        printf("ID %i  \tName : %s\n", std_reader->id, std_reader->name);
    }
    fclose(fp);

    free(std_reader);
}

пожалуйста, помогите мне понять и решить эту проблему.

ИЗМЕНИТЬ

Хорошо. Согласно приведенным ниже ответам, как я понял, я обновил свою структуру Student следующим образом.

typedef struct _student {
    int id;
    char name[20];
} Student;

Это работает.

Любые комментарии ?


person BlueBird    schedule 16.03.2014    source источник
comment
На какой операционной системе?   -  person Basile Starynkevitch    schedule 16.03.2014
comment
Очень короткий ответ: вы сохраняете указатель, но не то, на что он указывает.   -  person user253751    schedule 16.03.2014
comment
Есть комментарии? Что делать, если сохраняемое имя длиннее 19 символов?   -  person alk    schedule 16.03.2014
comment
Вы не должны были обновлять свою структуру. name должен оставаться указателем или быть гибким элементом массива.   -  person Basile Starynkevitch    schedule 16.03.2014
comment
Если вы действительно хотите, чтобы name имел фиксированный размер, по крайней мере, сделайте его намного длиннее (64 байта, а не 20). Но не стоит делать его фиксированного размера!   -  person Basile Starynkevitch    schedule 16.03.2014
comment
@BasileStarynkevitch, это зависит от программы. Для целей обучения это нормально, если нет имен длиннее 19 символов. Для реального использования либо делайте то, что вы сказали, либо увеличьте его до примерно 1024 символов и обязательно отклоните имена длиннее 1023. Предпочтительно первое, чтобы вы могли иметь дело с именами любой длины.   -  person user253751    schedule 16.03.2014
comment
Нет, в моем реальном приложении я храню mac-адрес.   -  person BlueBird    schedule 16.03.2014
comment
@BlueBird: сделайте свое реальное приложение бесплатным программным обеспечением, например. github.com   -  person Basile Starynkevitch    schedule 16.03.2014


Ответы (3)


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

Итак, в вашем write вы (при условии, что 64-битная система x86, например, Linux) пишете 12 байтов (4, т.е. sizeof(int) + 8, т.е. sizeof(char*)); последние 8 байтов являются числовым значением некоторого malloc-указателя.

В вашем read вы читаете эти 12 байтов. Таким образом, вы устанавливаете в поле name числовой указатель, который оказался действительным в процессе, выполнившем write. Обычно это не работает (например, из-за ASLR).

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

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


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

{ "id":123, "name":"John Doe" }

вот возможная процедура кодирования JSON с использованием Jansson:

int encode_student (FILE*fil, const Student*stu) {
   json_t* js = json_pack ("{siss}", 
                           "id", stu->id, 
                           "name", stu->name);
   int fail = json_dumpf (js, fil, JSON_INDENT(1));
   if (!fail) putc('\n', fil);
   json_decref (js); // will free the JSON
   return fail;  
}

Обратите внимание, что вам нужна функция для освобождения зоны malloc-ed Student, вот она:

void destroy_student(Student*st) {
   if (!st) return;
   free (st->name);
   free (st);
}

и вам может понадобиться макрос

#define DESTROY_CLEAR_STUDENT(st) do \
  { destroy_student(st); st = NULL; } while(0)

Теперь вот процедура декодирования JSON с использованием Jansson; он дает указатель Student в куче (который позже будет уничтожен вызывающей стороной с DESTROY_CLEAR_STUDENT).

Student* decode_student(FILE* fil) { 
   json_error_t jerr;
   memset (&jerr, 0, sizeof(jerr));
   json_t *js = json_loadf(fil, JSON_DISABLE_EOF_CHECK, &err);
   if (!js) {
      fprintf(stderr, "failed to decode student: %s\n", err.text);
      return NULL;
   }
   char* namestr=NULL;
   int idnum=0;
   if (json_unpack(js, "{siss}",  
                       "id", &idnum,
                       "name", &namestr)) {
       fprintf(stderr, "failed to unpack student\n");
       return NULL;
   };
   Student* res = malloc (sizeof(Student));
   if (!res) { perror("malloc student"); return NULL; };
   char *name = strdup(namestr);
   if (!name) { perror("strdup name"); free (res); return NULL; };
   memset(res, 9, sizeof(Student));
   res->id = id;
   res->name = name;
   json_decref(js);
   return res;
}

Вы также можете решить, что вы сериализуете в каком-то двоичном формате (я не рекомендую это делать). Затем вы должны определить свой формат сериализации и придерживаться его. Скорее всего, вам придется кодировать идентификатор студента, длину его имени, его имя....

Вы также можете (в C99) решить, что name учащегося является элемент гибкого массива, который объявляется

typedef struct _student {
   int id;
   char name[]; // flexible member array, conventionally \0 terminated
} Student;

Вы действительно хотите, чтобы имена учеников были разной длины. Тогда вы не сможете просто поместить записи разной длины в простой файл FILE. Вы можете использовать некоторую библиотеку индексированных файлов, например GDBM (каждая запись может быть в формате JSON). И вы, вероятно, захотите использовать Sqlite или настоящую базу данных, такую ​​как MariaDb или MongoDB.

person Basile Starynkevitch    schedule 16.03.2014
comment
ваш ответ имеет смысл. Как мне изменить свою программу, чтобы правильно записать значение. Мне нужно изменить мою структуру? - person BlueBird; 16.03.2014
comment
Я меняю указатель char в struct на массив char. теперь он работает нормально. Как вы думаете, это правильное решение? - person BlueBird; 16.03.2014
comment
Нет, вам не нужно это менять (но вы можете). Если это массив, сделайте его гибким членом массива. Но вы всегда должны правильно сериализовать. - person Basile Starynkevitch; 16.03.2014
comment
@BlueBird нет единственно правильного ответа, как и на большинство вопросов, например, как мне что-то сделать? В ответе Эмануэле Паолини говорится о двух возможностях. - person user253751; 16.03.2014
comment
в моем случае я не могу сериализовать, потому что мне нужно выполнить поиск в этом файле. Приведенный выше код не является моим настоящим кодом. Я создал это, чтобы смоделировать мою проблему в простом количестве строк. для приложения, которое я создаю, поиск обязателен. - person BlueBird; 16.03.2014
comment
Вам нужно имя ученика переменной длины. Таким образом, каждая запись должна иметь переменную длину; поэтому вы не можете легко искать в файле. - person Basile Starynkevitch; 16.03.2014
comment
Что, если я хочу использовать переменную длину без json? - person BlueBird; 16.03.2014
comment
Вы должны иметь дело с этим вручную, байт за байтом, вычисляя различные смещения и т. д. - person Basile Starynkevitch; 16.03.2014
comment
если вы видите в моем коде, я использую malloc для выделения памяти и memset для установки символа завершения, затем я копирую значение в указатель символа name. Теперь я обнаружил, что это неправильно где-то. Правильно? Если этот способ неверен, можете ли вы привести мне пример для этого. потому что в моем реальном приложении я пишу данные, которые берутся из базы данных mysql. - person BlueBird; 16.03.2014
comment
@BlueBird: покажите свой настоящий код (возможно, опубликовав его на github.com как бесплатную программу). Тогда задайте другой вопрос. То, что вы говорите в последних комментариях (mysql, MAC-адреса), действительно очень далеко от вашего первоначального вопроса. - person Basile Starynkevitch; 16.03.2014
comment
да, конечно, реальный код представляет собой комбинацию нескольких файлов, которые создают файл и который вызывает файл.. который выполняет поиск в файле... Чтобы сделать сценарий простым, я смоделировал студенческую БД, у которой есть моя реальная проблема. - person BlueBird; 16.03.2014
comment
надеюсь, mysqlite поможет мне. Я это тоже проверю. - person BlueBird; 16.03.2014

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

Либо поместите всю строку в свою структуру (не указатель на символ, а массив символов), либо вы должны отдельно записать строки в файл.

person Emanuele Paolini    schedule 16.03.2014

В read() вы никогда не выделяете память для name в своей структуре Student. (Ваша функция write() ведет себя в этом отношении гораздо лучше.)

Когда вы ссылаетесь на него в своем операторе printf, вы вызываете неопределенное поведение.

person Bathsheba    schedule 16.03.2014
comment
если вы заметили, когда я передаю аргумент --write .. после записи, я вызываю ту же функцию read(). то время это работает. переменные внутри read() функций полностью независимы. .. - person BlueBird; 16.03.2014
comment
хорошо .. я должен поместить malloc как std_reader->name = (char *) malloc(20); это в функцию чтения над while ? - person BlueBird; 16.03.2014
comment
Я не понимаю ваш первый комментарий: если вы выделяете новый Student, вам также нужно будет выделить память для name. Что касается вашего второго комментария: да, выделяйте столько памяти, сколько вы выделяете, когда пишете. И не забудьте нулевой терминатор. - person Bathsheba; 16.03.2014