C++. Примеры сохранения/чтения объектов класса. Сериализация

Примеры сохранения/чтения объектов класса. Сериализация

Данная тема содержит примеры сериализации объектов классов. В наиболее общем случае сериализация это свойство объекта сохранять свое состояние в файловом потоке и (по требованию) восстанавливать его.


Содержание


Поиск на других ресурсах:

1. Пример разработки класса, содержащий средства записи в файл и чтение из файла. Класс реализует массив чисел типа double*

В примере показано свойство экземпляра класса сохранять свое состояние в файле и восстанавливать его из файла. Под состоянием объекта понимается текущее значение внутренних данных (внутренних полей) объекта.

Разработан класс, содержащий следующие составляющие:

  • A – массив чисел типа double;
  • count – текущее количество элементов в массиве;
  • конструктор ArrayNumbers(double*, int) – инициализирует внутренний массив внешними данными;
  • конструктор ArrayNumbers() – создает массив нулевой длины;
  • конструктор копирования ArrayNumbers(const ArrayNumbers&);
  • операторная функция operator=(const ArrayNumbers&);
  • деструктор ~ArrayNumbers();
  • метод Print() – выводит содержимое текущего экземпляра;
  • метод SaveToFile(char*) – записывает текущий экземпляр в файл;
  • метод ReadFromFile(char*) – считывает текущий экземпляр из файла.

 

#include <iostream>
#include <fstream>
using namespace std;

// Массив чисел
class ArrayNumbers
{
private:
  // Внутренние поля
  double* A;
  int count;

public:
  // Конструкторы
  ArrayNumbers(double* _A, int _count)
  {
    try
    {
      count = _count;
      A = new double[count];
      for (int i = 0; i < _count; i++)
        A[i] = _A[i];
    }
    catch (bad_alloc e)
    {
      cout << e.what() << endl;
    }
  }

  // Конструктор без параметров
  ArrayNumbers()
  {
    count = 0;
  }

  // Конструктор копирования
  ArrayNumbers(const ArrayNumbers& obj)
  {
    try
    {
      count = obj.count;
      A = new double[count];
      for (int i = 0; i < count; i++)
        A[i] = obj.A[i];
    }
    catch (bad_alloc e)
    {
      cout << e.what() << endl;
    }
  }

  // Оператор копирования
  ArrayNumbers& operator=(const ArrayNumbers& obj)
  {
    // освободить предварительно выделенную память
    if (count > 0)
      delete[] A;

    try
    {
      count = obj.count;
      A = new double[count];
      for (int i = 0; i < count; i++)
        A[i] = obj.A[i];
    }
    catch (bad_alloc e)
    {
      cout << e.what() << endl;
    }
  }

  // Деструктор
  ~ArrayNumbers()
  {
    if (count > 0)
      delete[] A;
  }

  // Функция вывода содержимого текущего экземпляра
  void Print(string msg)
  {
    cout << msg;
    cout << ": A => { ";
    for (int i = 0; i < count; i++)
    {
      cout << A[i] << " ";
    }
    cout << "}" << endl;
  }

  // Методы сериализации
  // Запись текущего экземпляра в файл
  bool SaveToFile(const char* filename)
  {
    // 1. Создать экземпляр класса ofstream
    ofstream outF(filename, ios::out | ios::binary);

    if (!outF)
      return false;

    // 2. Записать количество элементов в массиве
    outF.write((char*)&count, sizeof(count));

    // 3. Записать непосредственно массив
    for (int i = 0; i < count; i++)
    {
      outF.write((char*)&A[i], sizeof(A[i]));
    }

    // 4. Закрыть файл
    outF.close();

    // 5. Вернуть true
    return true;
  }

  // Чтение массива из файла и запись в текущий экземпляр
  bool ReadFromFile(const char* filename)
  {
    // 1. Создать экземпляр класса ifstream, попытка открытия файла
    ifstream inF(filename, ios::in | ios::binary);

    if (!inF)
      return false;

    // 2. Сначала освободить предварительно выделенную память для массива
    if (count > 0)
      delete[] A;

    // 3. Считать количество элементов в массиве
    inF.read((char*)&count, sizeof(int));

    try
    {
      // 4. Попытка выделить память для элементов массива
      A = new double[count];
    }
    catch (bad_alloc e)
    {
      // 5. Если попытка неудачная, то выйти из функции
      cout << e.what();
      inF.close(); // закрыть файл
      return false;
    }

    // 6. Прочитать массив поэлементно
    double x;
    for (int i = 0; i < count; i++)
    {
      inF.read((char*)&x, sizeof(double)); // прочитать 1 элемент типа double
      A[i] = x;
    }

    // 7. Закрыть файловый поток
    inF.close();

    // 8. Выход с кодом true - файл прочитан
    return true;
  }
};

void main()
{
  // 1. Создать тестовый массив
  double AD[] = { 2.8, 3.5, 1.4, 7.0, 2.5 };

  // 2. Создать экземпляр A1 класса ArrayNumbers
  //    и записать его в файл file1.bin
  ArrayNumbers A1(AD, 5);
  A1.Print("A1"); // A1 = { 2.8, 3.5, 1.4, 7.0, 2.5 }
  A1.SaveToFile("file1.bin");

  // 3. Создать экземпляр A2 класса ArrayNumbers который содержит нулевой массив
  // и заполнить его данными из файла file1.bin
  ArrayNumbers A2;
  A2.Print("A2"); // A2 = { }
  A2.ReadFromFile("file1.bin");
  A2.Print("A2"); // A2 = { 2.8, 3.5, 1.4, 7.0, 2.5 }
}

Результат выполнения программы

A1: A => { 2.8 3.5 1.4 7 2.5 }
A2: A => { }
A2: A => { 2.8 3.5 1.4 7 2.5 }

 

2. Пример записи/чтения объекта класса, содержащий поля типа string

Чтобы записать строку типа string в бинарный файл, нужно выполнить следующую последовательность действий:

  • получить длину строки (число символов). Это целое число типа int;
  • записать длину строки в файл;
  • с помощью цикла записать каждый символ строки в файл.

Чтобы считать строку из файла в объект типа string, нужно следовать следующей последовательности:

  • считать длину строки из файла как тип int;
  • в цикле посимвольно считать всю строку в некоторый участок памяти или использовать добавление символа в конец строки в каждой итерации цикла.

В примере реализуется сериализация объекта типа BOOK. Класс BOOK описывает книгу, содержащую следующие поля:

  • author – название автора книги. Это поле типа string;
  • title – заголовок книги, поле типа string;
  • year – год издания;
  • price – стоимость книги.

Класс также содержит ряд методов оперирования данными о книге и сохранения/чтения этих данных:

  • BOOK() – параметризированный конструктор;
  • Author() – метод, возвращающий имя автора книги;
  • Title() – заголовок книги;
  • Year() – год издания;
  • Price() – стоимость книги;
  • SaveToFile() – метод, записывающий текущее значение внутренних полей в файл;
  • ReadFromFile() – метод, считывающий значение полей из файла.

Текст демонстрационной программы следующий.

 

#include <iostream>
#include <fstream>
using namespace std;

// Клас, описывающий книгу
class BOOK
{
private:
  // Внутренние поля
  string author; // автор
  string title; // название книги
  int year; // год издания
  float price; // стоимость

public:
  // Конструктор
  BOOK(string _author, string _title, int _year, float _price)
  {
    author = _author;
    title = _title;
    year = _year;
    price = _price;
  }

  // Методы чтения данных
  string Author() { return author; }
  string Title() { return title; }
  int Year() { return year; }
  float Price() { return price; }

  // Метод, выводящий значения полей (внутреннее состояние объекта)
  void Print(string msg)
  {
    cout << msg << endl;
    cout << "Author = " << author << endl;
    cout << "Title = " << title << endl;
    cout << "Year = " << year << endl;
    cout << "Price = " << price << endl;
    cout << "-----------------------------" << endl;
  }

  // Методы записи/чтения текущих данных (сериализации)
  bool SaveToFile(const char* filename)
  {
    // 1. Создать экземпляр класса ofstream
    ofstream outF(filename, ios::out | ios::binary);

    // 2. Проверка, создан ли файл,
    //    если не создан то выход с кодом false
    if (!outF.is_open()) return false;

    // 3. Записать данные полей
    // 3.1. Поле author типа string
    // 3.1.1. Определить длину строки типа string
    int len = author.length();

    // 3.1.2. Записать длину строки author
    outF.write((char*)&len, sizeof(int));

    // 3.1.3. Записать каждый символ строки в цикле
    for (int i = 0; i < author.length(); i++)
      outF.write((char*)&author[i], sizeof(author[i]));

    // 3.2. Поле title типа string
    // 3.2.1. Получить длину строки
    len = title.length();

    // 3.2.2. Записать длину строки title
    outF.write((char*)&len, sizeof(int));

    // 3.2.3. Записать посимвольно строку
    for (int i = 0; i < title.length(); i++)
      outF.write((char*)&title[i], sizeof(title[i]));

    // 3.3. Поле year типа int
    outF.write((char*)&year, sizeof(int));

    // 3.4. Поле price типа float
    outF.write((char*)&price, sizeof(float));

    // 4. Закрыть файл
    outF.close();

    // 5. Код возврата
    return true;
  }

  // Чтение массива из файла и запись в текущий экземпляр
  bool ReadFromFile(const char* filename)
  {
    // 1. Создать экземпляр класса ifstream
    ifstream inF(filename, ios::in | ios::binary);

    // 2. Проверка, открыт ли файл успешно
    if (!inF.is_open()) return false;

    // 3. Чтение полей объекта
    // 3.1. Поле author
    // 3.1.1. Считать количество элементов в поле author
    int len;
    inF.read((char*)&len, sizeof(int));

    // 3.1.2. Считать строку в цикле
    char c;
    author = "";
    for (int i = 0; i < len; i++)
    {
      inF.read((char*)&c, sizeof(c));
      author += c;
    }

    // 3.2. Поле title типа string
    // 3.2.1. Получить длину строки
    inF.read((char*)&len, sizeof(int));

    // 3.2.2. Считать строку в цикле
    title = "";
    for (int i = 0; i < len; i++)
    {
      inF.read((char*)&c, sizeof(c));
      title += c;
    }

    // 3.3. Поле year типа int
    inF.read((char*)&year, sizeof(int));

    // 3.4. Поле price типа float
    inF.read((char*)&price, sizeof(float));

    // Закрыть файл
    inF.close();

    // Код возврата
    return true;
  }
};

void main()
{
  // 1. Создать объект типа BOOK
  BOOK obj1("Author", "Title", 2000, 2.55);

  // 2. Сохранить объект obj1
  obj1.Print("obj1");

  // 3. Записать в файл obj1 => "obj1.bin"
  obj1.SaveToFile("obj1.bin");

  // 4. Создать другой объект типа BOOK
  BOOK obj2("--", "--", 3000, 10.88);
  obj2.Print("obj2");

  // 5. Считать данные из файла obj1.bin в объект obj2
  // "obj1.bin" => obj2
  obj2.ReadFromFile("obj1.bin");

  // 6. Вывести прочитанный объект
  obj2.Print("obj2 <= obj1.bin");
}

Результат выполнения программы

obj1
Author = Author
Title = Title
Year = 2000
Price = 2.55
-----------------------------
obj2
Author = --
Title = --
Year = 3000
Price = 10.88
-----------------------------
obj2 <= obj1.bin
Author = Author
Title = Title
Year = 2000
Price = 2.55
-----------------------------

 

3. Пример записи/чтения объекта класса, содержащий вложенные объекты другого класса

Данный пример рассматривает случай, когда в классе объявляются объекты (экземпляры) другого класса, и эти объекты нужно корректно сохранять и считывать. По приведенному примеру можно сохранять и считывать структурные переменные (структуры).

Чтобы сохранить вложенные объекты в классе, нужно получить непосредственно данные этих объектов в виде примитивных типов (int, doulbe, char и т.п.).

При записи и считывании данных (внутренних полей) объектов важно соблюдать следующее правило:

  • последовательность записываемых данных должна совпадать с последовательностью считываемых данных. Так, например, если записываются данные x, y, z, то в том же порядке эти данные должны считываться.

В примере демонстрируется сохранение экземпляра класса Line, в котором есть два встроенных объекта типа Point. Для записи/чтения в классе Line соответственно реализовано два метода SaveToFile() и ReadFromFile().

Текст демонстрационной программы следующий

#include <iostream>
#include <fstream>
using namespace std;

// Класс, описывающий точку на координатнойй плоскости
class Point
{
private:
  // Внутренние поля
  double x, y;

public:
  // Конструктор
  Point(double _x, double _y) : x(_x), y(_y) { }

  // Методы доступа
  void SetXY(double _x, double _y)
  {
    x = _x; y = _y;
  }
  double X() { return x; }
  double Y() { return y; }

  // Метод, выводящий значения x,y
  void Print(string msg)
  {
    cout << msg << " => x = " << x << ", y = " << y << endl;
  }
};

// Класс, описывающий линию
class Line
{
private:
  Point p1; // первая точка
  Point p2; // вторая точка

public:
  // Конструктор
  Line(double _x1, double _y1, double _x2, double _y2) :
    p1(_x1, _y1), p2(_x2, _y2) { }

  // Методы доступа
  Point P1() { return p1; }
  Point P2() { return p2; }

  // Метод, выводящий координаты точек линии
  void Print(string msg)
  {
    cout << msg << endl;
    p1.Print("p1");
    p2.Print("p2");
    cout << "-----------------------" << endl;
  }

  // Методы, которые записывают/считывают данные
  bool SaveToFile(const char* filename)
  {
    // 1. Создать экземпляр outF, который связан с файлом filename,
    // здесь:
    // - ios::out - файл открывается для записи;
    // - ios::binary - файл открывается в двоичном режиме.
    ofstream outF(filename, ios::out | ios::binary);

    // 2. Проверка, создан ли файл
    if (!outF.is_open()) return false;

    // 3. Записать данные полей p1, p2 типа Point
    double x1 = p1.X();
    double y1 = p1.Y();
    double x2 = p2.X();
    double y2 = p2.Y();

    outF.write((char*)&x1, sizeof(x1));
    outF.write((char*)&y1, sizeof(y1));
    outF.write((char*)&x2, sizeof(x2));
    outF.write((char*)&y2, sizeof(double)); // можно и так

    // 4. Закрыть файл
    outF.close();
  }

  bool ReadFromFile(const char* filename)
  {
    // 1. Создать экземпляр inF, который связан с файлом filename
    ifstream inF(filename, ios::in | ios::binary);

    // 2. Проверка, открыт ли файл успешно
    if (!inF.is_open()) return false;

    // 3. Чтение координат линии во временные переменные
    double x1, y1, x2, y2;

    // порядок чтения полей должен совпадать с порядком записи
    inF.read((char*)&x1, sizeof(double));
    inF.read((char*)&y1, sizeof(double));
    inF.read((char*)&x2, sizeof(x2));
    inF.read((char*)&y2, sizeof(y2));

    // 4. Запись координат во внутренние поля текущего объекта
    p1.SetXY(x1, y1);
    p2.SetXY(x2, y2);

    // 5. Закрыть файл
    inF.close();
  }
};

void main()
{
  // 1. Создать первую линию и вывести ее
  Line L1(2.5, 3.8, 4.1, 7.9);
  L1.Print("L1");

  // 2. Создать вторую линию и вывести ее
  Line L2(1.0, 1.0, 2.0, 2.0);
  L2.Print("L2");

  // 3. Записать первую линию в файл "line.bin"
  L1.SaveToFile("line.bin");

  // 4. Считать данные из файла "line.bin" и записать их в объект L2
  L2.ReadFromFile("line.bin");

  // 5. Вывести значение объекта L2
  L2.Print("L2 <= L1:");
}

Результат работы программы

L1
p1 => x = 2.5, y = 3.8
p2 => x = 4.1, y = 7.9
-----------------------
L2
p1 => x = 1, y = 1
p2 => x = 2, y = 2
-----------------------
L2 <= L1:
p1 => x = 2.5, y = 3.8
p2 => x = 4.1, y = 7.9
-----------------------

 


Связанные темы