Запись и чтение структур в/из файл(а)

IT IT
ПА>Ламерский вопрос

Почему ламерский? Нормальный вопрос, который обычно встречается большинству программеров.

ПА>Есть у меня некая структура (struct).

ПА>Требуется создать файл, куда писать данные в виде этих структур. А также читать. А также перемещаться по файлу.
ПА>Как это сделать? (Плиз с ма-аленьким примером или в какую сторону копать).

Всё зависит от многих вещей, как всегда

Для начала давай объявим твою структуру:

struct mystruct {
    int    i;
    char   buf[20];
    double d;
};


Теперь, допусти, нам нужно положить её в файл средствами C/C++.

Семейсво функций FILE рассматривать не будет в связи с её архаичносью, и начнём сразу с варианта, который не входит в стандарт, но присутствует во многих компиляторах.

#include <sys/stat.h>
#include <memory.h>
#include <fcntl.h>
#include <stdio.h>
#include <io.h>

struct mystruct {
    int i;
    char buf[5];
    double d;
};

int main(int argc, char* argv[])
{
    // открываем файл
    int fh = _open("file.dat",_O_RDWR | _O_BINARY);
    if (fh == -1)
    {
        // или при необходимости создаём новый
        fh = _creat("file.dat",_S_IREAD | _S_IWRITE);
        if (fh == -1)
            // не шмагла :xz:
            return 1;
    }

    // готовим структуру для записи
    mystruct ms;
    memset(&ms,0,sizeof ms);
    ms.i = 1;
    ms.d = 2;

    // позиционируемся в конец файла
    _lseek(fh,0,SEEK_END);

    // добавляем новую структуру
    _write(fh,&ms,sizeof ms);

    // позиционируемся в начало
    _lseek(fh,0,SEEK_END);

    // читаем первую записанную структуру 
    _read(fh,&ms,sizeof ms);

    return 0;
}


Эта программа открывает файл (либо создаёт его при необходимости) и добавляет в него новую структуру, затем читает первый экземпляр.

Всё казалось бы нормально, но если ты посмотришь размер созддаваемого файла, то он всегда будет кратен 24 байтам (вариант Visual C++), хотя размер структуры равен 4+5+8=17 байт. Это происходит потому, что компиляторы по умолчанию выравнивают размер структур в целях оптимизации. Следовательно, наша первая задача отменить это поведение по умолчанию. Стандартных средств сделать это нет, но как правило компиляторы содержат специальную опцию коммандной строки и/или прагму, позволяющую это делать.

Ещё одной неверной деталью в нашем примере является использование типа переменной int. Для разных версий операционных систем размер инта может быть разным и лучше явно указать размер используемого типа — short или long.

Изменим описание структуры:

#pragma pack(push,1)
struct mystruct {
    long   i;
    char   buf[5];
    double d;
};
#pragma pack(pop)


Теперь запись в файл даст вполне ожидаемый результат.

Здесь можно отметить ещё одну деталь. В качестве строки я использовал массив char[5]. Использование классов типа CString std::string не приведёт ни к чему хорошему. Фактически ты сохранишь не саму строку, а содержимое класса, который её реализует. Допустим, класс CMyString реализован следующим образом:

class CMyString {
public:
    int   len;
    char *str;
    // ....
};


Объявление такой структуры как

struct mystruct {
    long      i;
    CMyString str;
    double    d;
};


будет фактически соответствовать следующему варианту:

struct mystruct {
    long   i;
    int    str_len;
    char  *str_str;
    double d;
};


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

Теперь рассмотрим вариант с потоками. Вообще-то, лучше конечно использовать новую версию <fstream>, но у меня она до сих не вызывает никакого доверия. По-этому, воспользуемся старым вариантом:

#include <memory.h>
#include <fstream.h>

#pragma pack(push,1)
struct mystruct {
    long   i;
    char   buf[5];
    double d;
};
#pragma pack(pop)

int main(int argc, char* argv[])
{
    // создаём или открываем файл
    fstream f("file.dat",ios::binary|ios::in|ios::out);

    // готовим структуру для записи
    mystruct ms;
    memset(&ms,0,sizeof ms);
    ms.i = 1;
    ms.d = 2;

    // позиционируемся в конец файла
    f.seekp(0,ios::end);

    // добавляем новую структуру
    f.write((unsigned char*)&ms,sizeof ms);

    // позиционируемся в начало
    f.seekp(0,ios::beg);

    // читаем первую записанную структуру 
    f.read((unsigned char*)&ms,sizeof ms);

    return 0;
}


Ниже вариант использования Windows API вместо фуекций CRTL:

#include <windows.h>

#pragma pack(push,1)
struct mystruct {
    long   i;
    char   buf[5];
    double d;
};
#pragma pack(pop)

int main(int argc, char* argv[])
{
    // создаём или открываем файл
    HANDLE fh = ::CreateFile(
        TEXT("file.dat"),
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    // готовим структуру для записи
    mystruct ms;
    memset(&ms,0,sizeof ms);
    ms.i = 1;
    ms.d = 2;

    // позиционируемся в конец файла
    ::SetFilePointer(fh,0,0,FILE_END);

    // добавляем новую структуру
    DWORD dw=0;
    ::WriteFile(fh,&ms,sizeof ms,&dw,NULL);

    // позиционируемся в начало
    ::SetFilePointer(fh,0,0,FILE_BEGIN);

    // читаем первую записанную структуру 
    ::ReadFile(fh,&ms,sizeof ms,&dw,NULL);

    return 0;
}


И т.д.

Еще можно привести вариант исользования класса CFile из MFC, но, в принципе, он будет не очень сильно отличаться.

Может я немного увлёкся, но главное чтобы было понятно

ЗЫ. Далше тебя ждут другие вопросы:

  1. Как узнать количество записанных структу в файле?
    Правильный ответ — не вычислять это по размеру файла, а добавить в начало заголовок (специальную структуру), содержащую необходимую служебную информацию: фактический размер файла, версию формата, число записей, смещение к первому блоку и т.п.
  2. Как добавлять записи переменной длины?
    Можно к каждой записи добавить свой заголовок, описывающий её структуру.
  3. Как удалять ненужные записи из файла?
    Можно просто помечать их как удалённые, а в последствии организовать упаковку файла. Можно организовать список удалённых страниц и использовать их в дальнейшем вместо добавления новых в конец.
  4. Как обеспечить совместный доступ к файлу из нескольких программ.
    Блокировки, отдельный сервер доступа и ещё куча всяких вариантов.
  5. Как сделать динамическую структуру записей в файле...
    У-у-у...
После всего этого возникает вполне законный вопрос — а может лучше сразу взять стандартную базу данных?
Шутю я, шутю.
Панкратов Александр
Панкратов Александр Про файлы...
11.10.2002 04:43
Огромное спасибо, очень исчерпывающе.
IT>После всего этого возникает вполне законный вопрос — а может лучше сразу взять стандартную базу данных?
IT>Шутю я, шутю.
Если бы речь шла об обычных виндах — я бы так и сделал
Однако все происходит в WinCE. Да еще на обкоцанном варианте Casio. Поэтому с ADOCE возникают некоторые проблемы...
orangy
orangy
11.10.2002 06:19
Здравствуйте Панкратов Александр, Вы писали:

IT>>После всего этого возникает вполне законный вопрос — а может лучше сразу взять стандартную базу данных?

ПА>Однако все происходит в WinCE. Да еще на обкоцанном варианте Casio. Поэтому с ADOCE возникают некоторые проблемы...
Тогда у тебя возникает еще ряд проблем:
— CE система юникодная, осторожнее со строками при записи и чтении
— не помню какой Endian на Casio (это кажется был MIPS), осторожнее с переносом файлов с win32
— нельзя игнорировать alignment, точнее он должен быть на 4 байта. MIPS не позволяет адресовать 32-битное число по невыровненному адресу
это важно, если ты собираешься читать массивами структур

I would recommend using WINAPI CreateFile, ReadFile, ... functions тьфу блин, совсем зарапортавался...

Используй WINAPI CreateFile, ReadFile, ... — в winCE другого может и не быть, лучше всего завернуть это дело в портабельную оболочку.
Всегда указывай полный путь до файла, в WinCE нет понятия "текущая директория", если нужно — возьми у текущего модуля.
TCHAR buf[_MAX_PATH+30];
GetModuleFileName(hInstance, buf, _MAX_PATH);

Если объёмы данных небольшие — используй XML. Была где-то библиотека портированная для CE, поищи.
Янус 1.0 alpha 10: Orangy
Vi2
Vi2 Переносимость short и long ?
11.10.2002 04:52
Здравствуйте IT, Вы писали:

IT>Ещё одной неверной деталью в нашем примере является использование типа переменной int. Для разных версий операционных систем размер инта может быть разным и лучше явно указать размер используемого типа — short или long.


Интересно, неужели для разных версий операционных систем размеры short или long, в отличие от int, одинаковы?
Насколько я помню, есть sizeof(short int) <= sizeof(int) <= sizeof(long int) в стандарте и без всяких гарантий о точных значениях.

Так что и использование short или long не даст переносимости.
Павел Кузнецов
Павел Кузнецов
11.10.2002 08:13
Здравствуйте Vi2, Вы писали:

IT>>Ещё одной неверной деталью в нашем примере является использование типа переменной int. Для разных версий операционных систем размер инта может быть разным и лучше явно указать размер используемого типа — short или long.


Здесь IT был не вполне точен.

Vi2>Интересно, неужели для разных версий операционных систем размеры short или long, в отличие от int, одинаковы?


Не только для разных операционных систем, но и для разных компиляторов на одной операционной системе размеры short, long и int могут различаться.

Vi2>Насколько я помню, есть sizeof(short int) <= sizeof(int) <= sizeof(long int) в стандарте и без всяких гарантий о точных значениях.


Кроме указанного соотношения есть еще требования к минимальному диапазону представляемых значений:

short       -32767  . . .       +32767 (минимум 16 бит)
int         -32767  . . .       +32767 (минимум 16 бит)
long   -2147483647  . . .  +2147483647 (минимум 32 бита)
IT
IT
11.10.2002 11:31
Здравствуйте Vi2, Вы писали:

Vi2>Интересно, неужели для разных версий операционных систем размеры short или long, в отличие от int, одинаковы?


Ну если считать Windows 3.1 и Windows 95 разными версиями то да К тому же MS выпустила там чего-то 64 разрядное кажется, там уже и long поплывёт.

Vi2>Насколько я помню, есть sizeof(short int) <= sizeof(int) <= sizeof(long int) в стандарте и без всяких гарантий о точных значениях.


Vi2>Так что и использование short или long не даст переносимости.


Ладно придираться, я только обозначил проблему.
AndrewS42
AndrewS42
06.06.2005 06:58
Здравствуйте, IT, Вы писали:

IT>Всё зависит от многих вещей, как всегда


Это точно! Несмотря на то, что топику уже несколько лет, я хотел бы вставить свои 5 копеек в обсуждаемую тему, тем более, что эта тема находится в статьях сайта, а значит не имеет срока давности.
Наиболее правильным и переносимым (но медленным) будет вообще не сохранять в файл структуру целиком. Более того, не желательно даже int записывать в файл напрямую. Одна из причин уже была озвучена в топике: несовпадающие размеры int в разных компиляторах даже на одной платформе. Другая причина кроется в переносимости. Не факт, что ваша программа (или даже её отдельный модуль) никогда не переедут под UNIX. В отличие от Windows, UNIX существует на большом зоопарке платформ. Некоторые из них имеют big-endian представления чисел (первым идёт старший байт). Если на такой платформе сохранить int в файл как есть, а затем прочитать этот файл на Intel-платформе, то мы получим совершенно другое число. Например (пусть размер int будет 32 бита), если на big-endian сохранили 0x12345678, то в little-endian прочитаем 0x78563412.
Для большинства эти детали не существенны, так как не так уж и много программистов (надо признаться, и я в том числе) пишут сразу для нескольких платформ. Но, по крайней мере, задуматься о потенциальных проблемах переносимости надо.
Как же сохранять данные в файл, чтобы и файл, и программа его читающая были переносимы? Ответ уже был в этом топике: сохранять всё в xml или другом текстовом файле. Однако, если объём данных велик, и от двоичных данных никуда не деться, то сохранять все данные следует побайтно. Пример сохранения 32-битного int (предполагаем, что система, где char имеет размер, не равный 8 бит, нам не попадётся):

#include <stdio.h>

const char filename[] = "file.dat";

void read_int(FILE* f, int* d)
{
  unsigned char buf[4];
  unsigned char* p = buf;
  
  fread(buf, sizeof(buf), 1, f); // portable
  *d = *p++;
  *d += *p++ << 8;
  *d += *p++ << 16;
  *d += *p++ << 24;
//  fread(d, sizeof(*d), 1, f); // not portable
} // read_int

void write_int(FILE* f, int d)
{
  unsigned char buf[4];
  unsigned char* p = buf;
  
  *p++ = d & 0xff;
  *p++ = (d >> 8) & 0xff;
  *p++ = (d >> 16) & 0xff;
  *p = (d >> 24) & 0xff;
  fwrite(buf, sizeof(buf), 1, f); // portable
//  fwrite(&d, sizeof(d), 1, f); // not portable
} // write_in

void help() 
{
  fprintf(stderr, "Usage: fileio {w|r}\n");
  exit(1);
} // help()

int main(int argc, char *argv[])
{
  const int data = 0x12345678;
  FILE* fi;
  FILE* fo;
  int tmp;
  char err_msg[255];
  
  if (argc != 2) help();
  if (! strcmp(argv[1], "w")) {
    fo = fopen(filename, "w");
    if (! fo) {
      sprintf(err_msg, "Can not open file '%s'", filename);
      perror(err_msg);
      exit(1);
    }
    write_int(fo, data);
    fclose(fo);
  }
  else if (! strcmp(argv[1], "r")) {
    fo = fopen(filename, "r");
    if (! fo) {
      sprintf(err_msg, "Can not open file '%s'", filename);
      perror(err_msg);
      exit(1);
    }
    read_int(fo, &tmp);
    fclose(fo);
    printf("Readed %#08x. Must be %#08x\n", tmp, data);
  }
  else 
    help();
  return 0;
} // main()



Если гложут вопросы быстродействия, особенно в свете того, что big-endian систем не много и ради этой редкости мы замедляем считывание файлов, то необходимо ввести условную компиляцию: на little-endian системе читать int целиком, на big-endian – побайтно. Опять же, нельзя забывать про отличия размера int на разных платформах.
Хочу отметить, что я далеко не гуру в этом вопросе, и все мои замечания основаны лишь на изучении и портировании Open source ПО на компьютер RM200 (RISC-процессор R4000 (big-endian), ОС SINIX).
MaximE
MaximE
06.06.2005 07:20
AndrewS42 wrote:

[]

> Как же сохранять данные в файл, чтобы и файл, и программа его читающая были переносимы? Ответ уже был в этом топике: сохранять всё в xml или другом текстовом файле. Однако, если объём данных велик, и от двоичных данных никуда не деться, то сохранять все данные следует побайтно. Пример сохранения 32-битного int (предполагаем, что система, где char имеет размер, не равный 8 бит, нам не попадётся):


http://rsdn.ru/Forum/?mid=1104166
http://rsdn.ru/Forum/?mid=1110419

--
Maxim Yegorushkin
Posted via RSDN NNTP Server 1.9