БлогNot. Структура со строками string и файловые чтение/запись массива таких структур

Структура со строками string и файловые чтение/запись массива таких структур

В отличие от этого примера, используем в структурном типе данных более удобные в обращении и современные строки string и файловые потоки вместо классических си-строк char * и файлов из <cstdio>. Лекции по всем этим темам можно найти в оглавлении.

В итоге должна получиться программка, которая покажет непосредственное задание значений полям структурной переменной и ввод значений полей с консоли, а также запись файла структур и последующее его контрольное чтение. Проверяться она будет в консоли Visual Studio 2015, проект создан как вот здесь.

В начале файла укажем нужные библиотеки и директивы, в комментариях написано, для чего какая служит:

#define _CRT_SECURE_NO_WARNINGS
#include <iostream> /*ввод-вывод с консоли*/
#include <string> /*работа со string*/
#include <cstring> /*strcpy*/
#include <fstream> /*работа с файлом через поточные методы*/
#include <iomanip> /*setw*/
#include <cstdlib> /*exit*/
#include <windows.h> /*system*/
#include <clocale> /*setlocale*/
using namespace std; /*чтобы не писать std::cin или std::string*/

const int name_max_len = 23, date_max_len = 10; //Предельные длины строк string

Опишем структурный тип данных, включающий в себя идентификатор (номер) записи, две строки для хранения имени и даты рождения, а также вещественное поле money для хранения, например, зарплаты:

struct note {
 int id; string name,date; double money;
};

Мы не указываем какие-либо ограничения на длину строки, потому что сами будем управлять этим.

Так как программа может завершиться тремя способами - нормально, при ошибке записи файла и при ошибке его чтения, напишем функцию error с аргументом n (номер ошибки), которая будет за это отвечать. Ошибка номер ноль, как принято в C++, будет означать нормальное завершение. Вообще такую "общую точку выхода" зачастую полезно делать в процедурно-ориентированном коде:

void error(int n) {
 string msg[] = { "OK. Normal shutdown", "Can't open file to write!", "Can't open file to read!" };
 cout << endl << msg[n];
 system("pause>nul"); exit(n);
}

Функция для ввода данных с консоли input получает аргументами адрес структуры a, куда нужно заносить данные (это может быть, в том числе, и адрес элемента массива структур) и идентификатор записи i, остальные поля записи она запрашивает у пользователя.

В реальности стоило бы снабдить код большим количеством проверок корректности данных и вводом их в некую буферную запись, откуда потом скорректированные данные могут быть скопированы в массив или в файл.

Также обратите внимание, что мы не должны "резать" поля структуры кодом вроде

if (a.name.size()>23) a.name.resize(23);

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

Со всеми этими оговорками, простейшая функция ввода записи получилась такой:

void input(note& a, int i) {
 int d, m, y; char c;
 a.id = i;
 string name;
 cout << endl << "Name="; getline(cin, name);
 if (name.size() > name_max_len) name.resize(name_max_len);
 a.name = name;
 cout << endl << "Date dd.mm.yyyy=";  cin >> d >> c >> m >> c >> y;
 if (d < 1 || d>31 || m < 1 || m>12 || y < 1) { d = m = 1; y = 1970; }
 char buf[12]; sprintf(buf, "%02d.%02d.%04d", d, m, y); a.date = buf;
 cout << endl << "Money="; cin >> a.money;
 if (a.money < 0 || a.money>1e9) a.money = 0;
}

Функция output, соответственно, выводит в консоль поля записи a, переданной аргументом:

void output(note a) {
 cout.precision(2);
 cout << endl << setw(3) << a.id << setw(name_max_len+1) 
  << a.name << setw(date_max_len+1) << a.date << setw(10) << 
  fixed << a.money;
}

При корректных данных должны получиться столбцы правильной ширины.

Главной программе осталось русифицировать консоль и создать массив записей, для простоты предусмотрим там всего 2 элемента:

int main() {
 setlocale(LC_ALL,".1251"); SetConsoleCP(1251); SetConsoleOutputCP(1251);
 const int n = 2;
 note notes[n];

Первый элемент с id, равным нулю, зададим программно, а второй введём с консоли:

 notes[0] = { 0, "Ivanov V.P.", "12.11.2000", 40000 };
 input (notes[1],1);

Откроем файл notes.dat для записи и поместим туда данные, заодно печатая их в консоль:

 //Откроем файл notes.dat для записи и поместим туда данные, заодно печатая их в консоль :
 ofstream ofile("notes.dat", ios::binary); if (!ofile) error(1);
 char buf[name_max_len+1];
 for (int i = 0; i < n; i++) {
  output(notes[i]);
  //Строка string - составной объект, писать структуры 
  //с ней одним оператором не стоит
  ofile.write((char *)&notes[i].id, sizeof(int));
  strcpy(buf, notes[i].name.c_str());
  buf[name_max_len] = '\0';
  ofile.write(buf, name_max_len+1);
  strcpy(buf, notes[i].date.c_str());
  buf[date_max_len] = '\0';
  ofile.write(buf, date_max_len+1);
  ofile.write((char *)&notes[i].money, sizeof(double));
 }
 ofile.close();

Обратите внимание, что применение sizeof к составному объекту string некорректно, приходится писать в бинарный файл отдельными полями.

После закрытия файла, откроем его вновь для чтения и покажем прочитанные в цикле записи на экране, после чего можно сделать нормальный выход из программы:

 //После закрытия файла, откроем его вновь для чтения и покажем прочитанные в цикле записи на экране, 
 //после чего можно сделать нормальный выход из программы :
 ifstream ifile("notes.dat", ios::binary); if (!ifile) error(2);
 note note; 
 cout << endl << "Reading from file";
 while (1) {
  ifile.read((char*)&note.id, sizeof(int));
  if (!ifile || ifile.eof()) break;
  //Читать строки string тоже придётся сначала в буфер char[]
  ifile.read(buf, name_max_len+1);
  note.name = buf;
  ifile.read(buf, date_max_len+1);
  note.date = buf;
  ifile.read((char*)&note.money, sizeof(double));
  output(note);
 }
 ifile.close();

 error(0);
}

Вы понимаете, что наш файл - двоичный, и открывать его текстовым редактором бессмысленно.

Вот лог работы нашей программы:

Name=Петров Вася

Date dd.mm.yyyy=22.03.2000

Money=35000

  0             Ivanov V.P.  12.11.2000  40000.00
  1             Петров Вася  22.03.2000  35000.00
Reading from file
  0             Ivanov V.P.  12.11.2000  40000.00
  1             Петров Вася  22.03.2000  35000.00
OK. Normal shutdown
скриншот 16-ричного вида файла (см. комменты)
скриншот 16-ричного вида файла (см. комменты)

P.S. Если мы хотим именно хранить строки string переменной длины в бинарном файле и читать/писать их, то путей три:

  • писать и читать только string.c_str() в бинарные файлы, сведя задачу к предыдущей ссылке (но тогда не нужны и string, а можно делать классически на char *);
  • всё же отказаться от бинарных, а ограничиться текстовыми файлами и парсить полученные через getline строки;
  • предусмотреть в формате файла одновременное сохранение длины строковых данных объекта string.

В последнем случае имеем примерно такой подход (проверен не на структурах, а на паре строк).

Программа для записи набора string в бинарный файл:

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

int main() {
 ofstream outfile("data.dat", ofstream::binary | ios::out);

 string text("Редис, текст, данные");
 size_t size = text.size();
 outfile.write((char*)&size, sizeof(size));
 outfile.write(&text[0], size);

 string text2("Another one byte the dust");
 size_t size2 = text2.size();
 outfile.write((char*)&size2, sizeof(size2));
 outfile.write(&text2[0], size2);

 outfile.close();
 cin.get(); return 0;
}

Потом читаем это (возможно, с точностью до кодировки):

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

int main() {
 ifstream infile("data.dat", ifstream::binary | ios::in);
 string text; size_t size;

 for (int i=0; i<2; i++) { 
  //В реальности мы не знаем количества записей в файле или 
  //дополнительно храним его где-то ещё в формате файла
  infile.read((char *)&size, sizeof(size));
  text.resize(size);
  infile.read(&text[0], size);
  cout << text << endl;
 }

 infile.close();
 cin.get(); return 0;
}

В моей консоли Visual Studio 2015 обе программки сработали. Итак, если строка string является полем структуры, разницы в подходах нет.

P.P.S. Ну и, раз пошла такая пьянка, теперь разрешим строкам string в массиве структур иметь произвольную длину и содержать пробелы (по умолчанию чтение string из файлового потока прервётся на первом пробельном символе), а сохранять всё будем в текстовом файле, и читать из него же.

Чтобы всё работало, нам пришлось переписать оператор >> для своей структуры.

Программа создаст и затем прочитает такой файл:

2
1 Это текстовая строка любой    длины
2 Another one byte the dust

При чтении код сам избавится от лишних пробелов в строках между лексемами, следующими после числа.

/*
 А теперь разрешим строкам string в массиве структур иметь произвольную длину, 
 сохранять будем в текстовом файле
*/
#include <iostream>
#include <string>
#include <fstream>
#include <clocale>
#include <Windows.h>
using namespace std;

struct note {
 int id; 
 string name; 
 note *next;
 friend istream & operator >> (istream &, note &);
};

istream &operator >> (istream &is, note &dt) {
 //Чтобы с объектами note работал оператор >> , его пришлось переписать для структуры
 is >> dt.id >> dt.name;
 string buf;
 bool first = true; //не потерять первый пробел
 while (1) { 
  int p = is.peek();
  if (p == '\n' || p == EOF) break;
  //и учесть, что в формате строки могут быть пробелы
  is >> buf;
  if (first) { first = false; buf = " " + buf; }
  if (p == ' ') buf += " ";
  dt.name += buf;
 }
 return is;
}

note* add1 (note *head, note *src) { //Добавление записи в начало списка
 note *current = new note();
 current->id = src->id; 
 current->name = src->name;
 if (head == NULL) current->next = NULL;
 else {
  current->next = head;
  head = current;
 }
 return current;
}

int show (note* head) {
 int count = 0;
 if (!head) return 0;
 while (1) {
  cout << head->id << " " << head->name << endl;
  count++;
  if (head->next == NULL) break;
  head = head->next;
 }
 cout << count << " note(s)";
 return count;
}


int main() {
 setlocale(LC_ALL, ".1251"); SetConsoleCP(1251); SetConsoleOutputCP(1251);
 note temp; //Временная буферная запись

 //Пишем 2 записи в файл: 
 ofstream outfile("data.txt", ios::out);
 outfile << 2 << endl; //В начале файла предусмотрим строку с количеством записей
 
 string text("Это текстовая строка любой    длины");
 temp.id = 1;
 temp.name = text;
 outfile << temp.id << " " << temp.name << endl;

 string text2("Another one byte the dust");
 size_t size2 = text2.size();
 outfile << 2 << " " << text2;
 outfile.close();

 //Читаем записи из текстового файла
 ifstream infile("data.txt", ios::in);
 size_t n;
 infile >> n; //Прочитали размерность
 if (!infile.good()) {
  cout << "Неверный формат файла data.txt - нет количества записей";
  return 1;
 }

 cout << "Формируем список" << endl;
 note *head = nullptr, *current = &temp;
 for (int i = 0; i < n; i++) { //Читаем файл, формируем список структур, выводим
  infile >> temp;
  temp.id = i + 1;
  cout << temp.id << " " << temp.name << endl;
  head = add1 (head, &temp);
 }
 infile.close();

 cout << "Ещё раз читаем список из памяти" << endl;
 show (head); //Ещё раз показать список из памяти
 cin.get(); return 0;
}


теги: программирование c++ список учебное textprocessing форматы

показать комментарии (3)

01.12.2018, 12:35; рейтинг: 1870