Структуры с меняющимися размерами данных

В разных форумах по программированию на C++ регулярно задаются вопросы: как записать структуру с текстовыми полями в бинарный файл, а затем прочитать её оттуда? Типичной ошибкой для начинающих программистов является попытка записать напрямую в файл структуру, содержащую указатели на текстовые строки. В итоге в файл записываются лишь значения указателей, но не сами строки. Возьмём в качестве примера небольшую структуру, в которой используются классы CString для текстовых данных:

typedef struct _CUSTOM_DATA 
{ 
      CString strName;        // Имя 
      CString strFamily;      // Фамилия 
      CString strAddress;     // Адрес 
      CString strCompany;     // Компания 
      CString strMailBox;     // Почта 
} CUSTOM_DATA, *LPCUSTOM_DATA; 

В таком виде структура удобна для обмена данными внутри приложения, например: ввод информации через графический интерфейс, запись в таблицу СУБД, вывод в отчётный документ или экспорт в текстовый файл, и т.д. Но для записи в бинарный файл такая структура неприемлема, поскольку класс CString содержит лишь указатель на символьный массив, а сам текст находится вне класса. Тогда каким же образом текстовые данные можно записать из данной структуры в бинарный файл, а затем прочитать их из файла обратно в структуру? Для этого используются так называемые структуры с меняющимся размером данных, у которых фиксированный размер имеет лишь начало структуры (заголовок), а за ним располагаются сами данные в определённой последовательности. Выглядит структура так:

typedef struct _CUSTOM_DATA_PERSIST 
{ 
      DWORD dwDataSize; 
      WORD wOffsetName; 
      WORD wOffsetFamily; 
      WORD wOffsetAddress; 
      WORD wOffsetCompany; 
      WORD wOffsetMailBox; 
      TCHAR tcStrData[1]; 
} CUSTOM_DATA_PERSIST, *LPCUSTOM_DATA_PERSIST; 

Суть в том, что для инициализации такой структуры нужно сначала определить размер всех данных, которые будут записываться в файл. Т.е. необходимо узнать длину каждой строки вместе с нулевым символом на конце, сложить их вместе, прибавить к этому значению размер заголовка структуры, а затем выделить в памяти массив соответствующего размера, в котором разместятся эти данные. Текстовые строки в массиве располагаются последовательно одна за другой, разделённые нулевыми символами, а в заголовке структуры для каждой строки указываются смещения от начала первой строки. Ещё необходимо в заголовке структуры указать размер всего массива данных, чтобы потом при чтении данных из файла, можно было заранее выделить для них массив нужного размера. В таком виде эта структура записывается в файл. При чтении структуры из файла, чтобы получить указатель на нужную строку, надо прибавить к значению указателя на первую строку величину смещения для нужной строки. Как же тогда пользоваться такой структурой для обмена данными в приложении, если процесс её инициализации и обращения к ней выглядит таким сложным и трудоёмким? А не нужно её для этого использовать. Такие структуры следует применять лишь для хранения данных в файле, для передачи данных по сети или для локального обмена данными между приложениями. А для передачи данных внутри приложения следует пользоваться более простым и удобным вариантом первой структуры. Т.е. фактически для одних и тех же данных нужно определить два типа структур. При записи данных в файл и при чтении их из файла необходимо реализовать процедуру передачи данных из одной структуры в другую. Вот как это выглядит в коде на C++:

#include "stdafx.h" 
#include <stdio.h> 
 
typedef struct _CUSTOM_DATA 
{ 
      CString strName;        // Имя 
      CString strFamily;      // Фамилия 
      CString strAddress;     // Адрес 
      CString strCompany;     // Компания 
      CString strMailBox;     // Почта 
} CUSTOM_DATA, *LPCUSTOM_DATA; 
 
typedef struct _CUSTOM_DATA_PERSIST 
{ 
      DWORD dwDataSize; 
      WORD wOffsetName; 
      WORD wOffsetFamily; 
      WORD wOffsetAddress; 
      WORD wOffsetCompany; 
      WORD wOffsetMailBox; 
      TCHAR tcStrData[1]; 
} CUSTOM_DATA_PERSIST, *LPCUSTOM_DATA_PERSIST; 
 
int SaveDataToFile(LPCUSTOM_DATA data, LPCTSTR szFile) 
{ 
      ATLASSERT (data != NULL); 
      CUSTOM_DATA_PERSIST pers; 
      pers.wOffsetName = 0; 
      pers.wOffsetFamily = pers.wOffsetName + ::lstrlen(data->strName) + 1; 
      pers.wOffsetAddress = pers.wOffsetFamily + ::lstrlen(data->strFamily) + 1; 
      pers.wOffsetCompany = pers.wOffsetAddress + ::lstrlen(data->strAddress) + 1; 
      pers.wOffsetMailBox = pers.wOffsetCompany + ::lstrlen(data->strCompany) + 1; 
      DWORD dwStrLen = pers.wOffsetMailBox + ::lstrlen(data->strMailBox); 
      pers.dwDataSize = dwStrLen * sizeof(TCHAR) + sizeof(pers); 
      // Создание массива и заполнение его данными... 
      LPCUSTOM_DATA_PERSIST lpDataPers = 
            (LPCUSTOM_DATA_PERSIST)(new BYTE[pers.dwDataSize]); 
      *lpDataPers = pers; 
      ::lstrcpy(lpDataPers->tcStrData + pers.wOffsetName, data->strName); 
      ::lstrcpy(lpDataPers->tcStrData + pers.wOffsetFamily, data->strFamily); 
      ::lstrcpy(lpDataPers->tcStrData + pers.wOffsetAddress, data->strAddress); 
      ::lstrcpy(lpDataPers->tcStrData + pers.wOffsetCompany, data->strCompany); 
      ::lstrcpy(lpDataPers->tcStrData + pers.wOffsetMailBox, data->strMailBox); 
      // Запись массива в файл... 
      int nResult(0); 
      FILE *file(NULL); 
      _tfopen_s(&file, szFile, _T("wb")); 
      if (file != NULL) 
      { 
            fwrite(lpDataPers, 1, pers.dwDataSize, file); 
            fclose(file); 
      } else 
      { 
            nResult = -1; 
      } 
      delete lpDataPers; 
      // 
      return nResult; 
} 
 
int LoadDataFromFile(LPCUSTOM_DATA data, LPCTSTR szFile) 
{ 
      ATLASSERT (data != NULL); 
      CUSTOM_DATA_PERSIST pers; 
      // 
      int nResult(0); 
      FILE *file(NULL); 
      _tfopen_s(&file, szFile, _T("rb")); 
      if (file != NULL) 
      { 
            // Считываем заголовок структуры и создаём массив... 
            fread(&pers, sizeof(pers), 1, file); 
            LPCUSTOM_DATA_PERSIST lpDataPers = 
                  (LPCUSTOM_DATA_PERSIST)(new BYTE[pers.dwDataSize]); 
            *lpDataPers = pers; 
            // Потом дочитываем остальные данные... 
            fread((LPBYTE)lpDataPers + sizeof(pers), 1, 
                        pers.dwDataSize - sizeof(pers), file); 
            fclose(file); 
            // Передача данных в другую структуру... 
            data->strName = lpDataPers->tcStrData + lpDataPers->wOffsetName; 
            data->strFamily = lpDataPers->tcStrData + lpDataPers->wOffsetFamily; 
            data->strAddress = lpDataPers->tcStrData + lpDataPers->wOffsetAddress; 
            data->strCompany = lpDataPers->tcStrData + lpDataPers->wOffsetCompany; 
            data->strMailBox = lpDataPers->tcStrData + lpDataPers->wOffsetMailBox; 
            // 
            delete lpDataPers; 
      } else 
      { 
            nResult = -1; 
      } 
      return nResult; 
} 
 
// Для вывода текста на консоль... 
BOOL PrintText(LPCTSTR szText); 
 
// Применение функций записи в файл и чтения из файла, 
// отображение полученных данных... 
int _tmain(int argc, _TCHAR* argv[]) 
{        
      // Инициализация структуры... 
      CUSTOM_DATA data = 
      { 
            _T("Виталий Сергеевич"), 
            _T("Рычков"), 
            _T("129329, г. Москва, ул Кольская, д. 11"), 
            _T("WinMain &Co Ltd"), 
            _T("rychkov@inbox.ru") 
      }; 
 
      LPCTSTR szFile = _T("WinMain.bin"); 
 
      // Запись данных в файл... 
      SaveDataToFile(&data, szFile); 
 
      // Чтение данных из файла... 
      CUSTOM_DATA dataNew; 
      LoadDataFromFile(&dataNew, szFile); 
 
      // Вывод данных на консоль... 
      PrintText(dataNew.strFamily); 
      _puttc('\n', stdout); 
      PrintText(dataNew.strName); 
      _puttc('\n', stdout); 
      PrintText(dataNew.strAddress); 
      _puttc('\n', stdout); 
      PrintText(dataNew.strCompany); 
      _puttc('\n', stdout); 
      PrintText(dataNew.strMailBox); 
      _puttc('\n', stdout); 
 
      // Ожидание... 
      _gettc(stdin); 
 
      return 0; 
} 
 
BOOL PrintText(LPCTSTR szText) 
{ 
    static HANDLE hConsole = ::GetStdHandle(STD_OUTPUT_HANDLE); 
    // Вывод текста на консоль... 
    DWORD dw(0); 
    return ::WriteConsole(hConsole, szText, ::lstrlen(szText), &dw, NULL); 
} 

Представленный пример выполнен в среде Visual C++ 2005. Для его повторения нужно с помощью «визарда» создать проект приложения Win32 Console, включив опцию поддержки ATL.