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
-----------------------

 


Споріднені теми