Структура со строками 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 || y > 9999) { 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 *)¬es[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 *)¬es[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*)¬e.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*)¬e.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-ричного вида файла (см. комменты)
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; }
01.12.2018, 12:35 [8031 просмотр]