Запись и чтение структур в/из файл(а)
От: IT Россия linq2db.com
Дата: 11.10.02 04:19
Оценка: 100 (18) +2 -2
#Имя: FAQ.cpp.struct2file
ПА>Ламерский вопрос

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

ПА>Есть у меня некая структура (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. Как сделать динамическую структуру записей в файле...
    У-у-у...
После всего этого возникает вполне законный вопрос — а может лучше сразу взять стандартную базу данных?
Шутю я, шутю.
Если нам не помогут, то мы тоже никого не пощадим.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.