Динамический массив структур. Запись и чтение из файла
Есть такая структура:
struct Rent
{
char* obj;
char* date;
unsigned int beg;
unsigned int end;
char* name;
};
Есть массив структур:
Rent* arr = new Rent[n];
И массив указателей на строковые константы:
const char* mas[8] = { "Велосипед", "400", "Самокат", "350", "Ролики", "300", "Скейтборд", "200" };
Запись в файл:
ofstream fout;
fout.open("rent.txt", ios::binary | ios::trunc);
fout.write(reinterpret_cast<char*>(mas), 8 * sizeof(const char*));
fout.write(reinterpret_cast<char*>(arr), n * sizeof(Rent));
fout.close();
Чтение из файла:
int n;
Rent* arr2;
const char* mas2[8];
ifstream fin;
std::ifstream num("rent.txt", std::ios::binary);
num.seekg(0, std::ios::end);
n = ((long)num.tellg()-8) / sizeof(Rent);
n--;
num.seekg(0, std::ios::beg);
num.close();;
arr2 = new Rent[n];
fin.open("rent.txt", std::ios::binary);
fin.read(reinterpret_cast<char*>(mas2), 8 * sizeof(const char*));
fin.read(reinterpret_cast<char*>(arr2), n * sizeof(Rent));
fin.close();
Смысл был такой: из консоли считываем массив структур, записываем файл, из этого файла считываем в другой массив. При чтении из консоли динамическая память выделяется, там все нормально. И для вторых массивов я память не выделяла (именно для char*), и все выводилась нормально. Я правильно понимаю, что из-за того, что в файле хранятся адреса строк? Мне нужно, чтобы в файле хранились именно строки, и их можно было считать в любой момент. Как это сделать?
Ответы (4 шт):
Нужно сделать для каждого класса 3 функции:
- уникальный ид типа
- сериализация
- десериализация
Соответственно в файл писать блоки вида: <ид_типа><данные_типа>
Делается такое примерно так:
#include <iostream>
#include <string>
#include <array>
#include <utility>
#include <vector>
#include <algorithm>
#include <cstdint>
#include <chrono>
#include <random>
#include <numeric>
#include <unordered_map>
#include <typeindex>
#include <functional>
#include <any>
#include <span>
#include <unordered_set>
#include <optional>
#include <cassert>
using namespace std;
// Basics
template<typename T>
void SerializeRawSimple(T const& val, vector<byte>& data)
{
union U
{
T val;
array<byte, sizeof T> b;
};
U u{ .val = val };
data.insert(data.end(), u.b.begin(), u.b.end());
}
void SerializeRaw(int32_t const& val, vector<byte>& data)
{
SerializeRawSimple(val, data);
}
void SerializeRaw(int64_t const& val, vector<byte>& data)
{
SerializeRawSimple(val, data);
}
void SerializeRaw(string const& val, vector<byte>& data)
{
SerializeRawSimple((int64_t)val.size(), data);
for (auto& c : val)
{
data.push_back((byte)c);
}
}
template<typename T>
T DeserializeRawSimple(span<const byte>const& data, int64_t& type_data_size)
{
union U
{
array<byte, sizeof T> b;
T val;
};
if (data.size() < sizeof T)
throw "Deserialize error";
U u;
copy(data.begin(), data.begin() + sizeof T, u.b.begin());
type_data_size = sizeof T;
return u.val;
}
template<typename T>
T DeserializeRaw(span<const byte>const& data, int64_t& type_data_size)
{
static_assert(false && "Need implement");
return T();
}
template<>
int32_t DeserializeRaw<int32_t>(span<const byte> const& data, int64_t& type_data_size)
{
return DeserializeRawSimple<int32_t>(data, type_data_size);
}
template<>
int64_t DeserializeRaw<int64_t>(span<const byte>const& data, int64_t& type_data_size)
{
return DeserializeRawSimple<int64_t>(data, type_data_size);
}
template<>
string DeserializeRaw<string>(std::span<const byte>const& data, int64_t& type_data_size)
{
auto data_span = data;
int64_t tmp_size;
type_data_size = 0;
string res;
auto str_len = DeserializeRaw<int64_t>(data_span, tmp_size);
data_span = data_span.subspan(tmp_size);
type_data_size += tmp_size;
if (data_span.size() < str_len)
throw "Deserialize error";
res.resize(str_len);
for (size_t i = 0; i < str_len; i++)
res[i] = (char)data_span[i];
type_data_size += str_len;
return res;
}
// Types
struct A
{
int32_t x = 8;
int32_t y = 9;
string z = "12345";
};
void SerializeRaw(A const& val, vector<byte>& data)
{
SerializeRaw(val.x, data);
SerializeRaw(val.y, data);
SerializeRaw(val.z, data);
}
template<>
A DeserializeRaw<A>(std::span<const byte>const& data, int64_t& type_data_size)
{
auto data_span = data;
int64_t tmp_size;
type_data_size = 0;
A res;
res.x = DeserializeRaw<int32_t>(data_span, tmp_size);
data_span = data_span.subspan(tmp_size);
type_data_size += tmp_size;
res.y = DeserializeRaw<int32_t>(data_span, tmp_size);
data_span = data_span.subspan(tmp_size);
type_data_size += tmp_size;
res.z = DeserializeRaw<string>(data_span, tmp_size);
data_span = data_span.subspan(tmp_size);
type_data_size += tmp_size;
return res;
}
struct B
{
int32_t x = -1;
A y;
};
void SerializeRaw(B const& val, vector<byte>& data)
{
SerializeRaw(val.x, data);
SerializeRaw(val.y, data);
}
template<>
B DeserializeRaw<B>(std::span<const byte>const& data, int64_t& type_data_size)
{
auto data_span = data;
int64_t tmp_size;
type_data_size = 0;
B res;
res.x = DeserializeRaw<int32_t>(data_span, tmp_size);
data_span = data_span.subspan(tmp_size);
type_data_size += tmp_size;
res.y = DeserializeRaw<A>(data_span, tmp_size);
data_span = data_span.subspan(tmp_size);
type_data_size += tmp_size;
return res;
}
// Serializer
class EpicSerializer
{
public:
EpicSerializer()
{
RegisterType<A>();
RegisterType<B>();
}
static auto Get()
{
static EpicSerializer i;
return &i;
}
template<typename T>
int32_t GetTypeId()
{
if (auto it = _types_map.find(typeid(T)); it != _types_map.end())
return it->second.id;
return 0;
}
template<typename T>
void Serialize(T const& val, vector<byte>& data)
{
auto it = _types_map.find(typeid(T));
if (it == _types_map.end())
throw "Unknown type serialize";
SerializeRaw(it->second.id, data);
it->second.serialize((void*)&val, data);
}
vector<any> Deserialize(vector<byte> const& data)
{
vector<any> res;
size_t pos = 0;
while (pos < data.size())
{
if (pos + sizeof(uint32_t) > data.size())
throw "Deserialize wrong";
span<const byte> uid_data(data.data() + pos, sizeof(uint32_t));
int64_t data_type_size;
auto uid = DeserializeRaw<int32_t>(uid_data, data_type_size);
pos += data_type_size;
auto it = _types_uid_map.find(uid);
if (it == _types_uid_map.end())
throw "Deserialize wrong";
span<const byte> t1{ data.data() + pos, data.size() - pos };
auto& cell = _types_map[it->second];
res.push_back(cell.deserialize(t1, data_type_size));
pos += data_type_size;
}
return res;
}
private:
struct Cell
{
int32_t id = 0;
function<void(void* val, vector<byte>& data)> serialize;
function<any(std::span<const byte>const& data, int64_t& type_data_size)> deserialize;
};
template<typename T>
static void InternalSerialize(void* val, vector<byte>& data)
{
SerializeRaw(*(T*)val, data);
}
template<typename T>
static any InternalDeserialize(std::span<const byte>const& data, int64_t& type_data_size)
{
return DeserializeRaw<T>(data, type_data_size);
}
template<typename T>
void RegisterType()
{
int32_t uid = _types_map.size() + 1;
Cell cell{
.id = uid,
.serialize = &InternalSerialize<T>,
.deserialize = &InternalDeserialize<T> };
assert(_types_map.find(typeid(T)) == _types_map.end());
_types_map.insert({ typeid(T), cell });
_types_uid_map.insert({ uid, typeid(T) });
}
unordered_map<type_index, Cell> _types_map;
unordered_map<int32_t, type_index> _types_uid_map;
};
int main()
{
A a;
B b;
vector<byte> data;
EpicSerializer::Get()->Serialize(a, data);
EpicSerializer::Get()->Serialize(b, data);
auto vec = EpicSerializer::Get()->Deserialize(data);
A a1 = any_cast<A>(vec[0]);
B b1 = any_cast<B>(vec[1]);
return 0;
}
В данном варианте не учитывается много чего. Например изменение полей типов, big-little endian итд. Реальное решение задачи сереализации гораздо сложнее(пока в C++ не завезли рефлексию).
Для массива структур в лоб нужно указатели char* obj; заменить на поля определенной длинны. Например char [30] obj. В этом случае структура будет содержать в себе все данные, хоть и ограниченные по размеру.
И читаться все будет в один read или простым циклом. Хотя можно и мемори мап сделать.
Если нужен перенос на другую архитектуру, то к интам ещё добавьте ntoh/hton
Если сделать массив фиксированным с запасом длины для каждого слова, то mas указывает на непрерывную область с размером sizeof(mas):
const char mas[8][10] = { "Велосипед", "400", "Самокат", "350",
"Ролики", "300", "Скейтборд", "200" };
/*и сохранять сразу*/
fout.write(mas, sizeof(mas));
При сохранении можно дописать после этого ещё и символ '\n', тогда считать можно будет сразу через gets();
Та же история со структурой. В нынешнем виде она предполагает, что будут выделяться области памяти под поля типа char* и укладываться в них в виде ссылок. Но, если не жалко немножко байтов, выделить фиксировано:
struct Rent{
char obj[10]; //Не знаю сколько надо
char date[10]; // Я бы делал просто таймстемп uint_32t
// или полноценный time_t
unsigned int beg;
unsigned int end;
char name[64]; //"Остап Сулейман Берта Мария Бендер Бей" влазит в 37
};
/*И вот тогда sizeof отработает корректно*/
fout.write(reinterpret_cast<char*>(arr), n * sizeof(Rent));
Запись и чтение из файла этих структур, увы, по ряду причин требует переработки, как их самих, так и кода доступа к ним, в частности:
- Поле
objможет указывать куда угодно: статические данные, память полученнаяmalloc()/strdup(), память полученнаяnewили память полученнаяnew char[]; - При чтении содержимое массива
const char* mas[]будет затруднительно разместить в сегменте только для чтения; - ...
Вопрос в минимизации этих изменений. С точки зрения размера примера, наверное минимальным будет что-то в духе:
// Boost C++ Serialization (simple text format)
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/vector.hpp>
struct Rent {
std::string obj;
std::string date;
unsigned int beg;
unsigned int end;
std::string name;
template<class Archive>
void serialize(Archive& ar, const unsigned int /*version*/) {
ar & obj & date & beg & end & name;
}
};
int main() {
const Rent test = {"Arthur", "29 March 1977", 42, 54, "Deep Thought"};
const std::vector<Rent> test_vr = { test, test };
const std::vector<std::string> test_vs = {
"Велосипед", "400", "Самокат", "350",
"Ролики", "300", "Скейтборд", "200"
};
{
const auto r = test;
const auto vr = test_vr;
const auto vs = test_vs;
std::ofstream ofs("filename");
boost::archive::text_oarchive oa(ofs);
oa << r;
oa << vr;
oa << vs;
std::cout << "Запись хорь\n";
}
{
Rent newr;
std::vector<Rent> newvr;
std::vector<std::string> newvs;
std::ifstream ifs("filename");
boost::archive::text_iarchive ia(ifs);
ia >> newr;
ia >> newvr;
ia >> newvs;
assert(newr.name == test.name && newr.end == test.end);
assert(newvr.size() == test_vr.size());
assert(newvs.size() == test_vs.size() && newvs[4] == test_vs[4]);
std::cout << "Чтение хорь\n";
}
}
Работу примера смотрите: https://godbolt.org/z/a1ojhMaah
Конечно, он немного длиннее варианта @Solt, но там опущены необходимые определения для максимальных длин полей и т.д. Зато такой же общий, как вариант @Никита Самоуков, и существенно его короче.
Для простоты используются std::string и std::vector, ввиду наличия стандартных конструкторов и деструкторов, но, при необходимости, их можно заменить на char * и массивы при условии формализации их создания, удаления, копирования или перемещения. Упрощённый вариант логики конструкторов/деструкторов для нуль-терминированных char *: https://godbolt.org/z/1dGqo931a