грешка при четене/запис на двоичен файл, съдържащ 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-ed указател.

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

Като цяло правенето на I/O върху указатели е много лошо. Има смисъл само за същия процес.

Това, което искате да направите, се нарича сериализация. Поради софтуерно инженерство причини препоръчвам използването на текстов формат за сериализиране (напр. 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 за задаване на символ за прекратяване, след което копирам стойността в указателя char 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

Забележете, че не пишете името на ученика, който трябва да бъде файл. Вие само пишете указателя към този низ. Това със сигурност не е това, което искате. Когато четете файла, вие четете показалеца, който вече не е валиден.

Или поставете целия низ във вашата структура (не char указател, а char масив), или в противен случай трябва отделно да запишете низовете във файла.

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