Как сохранить данные между запусками программы и как описать объект из нескольких полей
g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe и запускайте из каталога, где лежат их файлы данных.Всё, что программа хранит в переменных, исчезает, когда она завершается. Чтобы данные дожили до следующего запуска или попали в другую программу, их пишут в файл. В C++ с файлом работают так же, как с консолью: через поток. std::cin и std::cout связаны с клавиатурой и экраном, а файловые потоки из заголовка <fstream> — с файлом на диске:
| Тип | Для чего | Как консоль |
|---|---|---|
std::ifstream in("a.txt"); | чтение из файла | как std::cin: in >> x, std::getline(in, s) |
std::ofstream out("b.txt"); | запись в файл; файла нет — создаётся, есть — очищается | как std::cout: out << x |
std::fstream f("c.dat", std::ios::in | std::ios::out); | чтение и запись одного файла | — |
if (!in.is_open()). Файла может не быть, не хватает прав, путь указан неверно. Неоткрытый поток не завершает программу — он просто в состоянии ошибки, и все чтения из него молча ничего не делают."a.txt") ищется в рабочем каталоге — там, откуда запущена программа, а не там, где лежит .exe или исходник. Запуск из консоли в каталоге проекта — самый предсказуемый. Путь в строке C++ пишется с двойной обратной чертой: "C:\\data\\a.txt", или с прямой: "C:/data/a.txt" — Windows понимает обе.in.close() или автоматически, когда переменная потока исчезает — в конце блока или функции. Пока поток записи не закрыт, часть данных может лежать в буфере и ещё не попасть на диск.Сколько чисел в файле, заранее неизвестно. Читают, пока чтение удаётся: выражение in >> x в условии истинно, если число прочитано, и ложно — если файл кончился или встретилось не число.
// Числа из файла numbers.txt — сумма, количество и среднее в файл result.txt
#include <iostream>
#include <fstream>
int main()
{
std::ifstream in("numbers.txt");
if (!in.is_open()) {
std::cout << "Error: cannot open numbers.txt\n";
return 1;
}
long long sum = 0;
int count = 0;
int x;
while (in >> x) { // false — конец файла или не число
sum += x;
++count;
}
if (!in.eof()) {
std::cout << "Error: not a number after " << count << " numbers\n";
return 1;
}
in.close();
std::ofstream out("result.txt");
if (!out.is_open()) {
std::cout << "Error: cannot create result.txt\n";
return 1;
}
out << "count " << count << '\n';
out << "sum " << sum << '\n';
if (count > 0) {
out << "average " << static_cast<double>(sum) / count << '\n';
}
std::cout << "Done: " << count << " numbers\n";
return 0;
}
numbers.txt | Консоль | result.txt |
|---|---|---|
12 7 -3 40 5 9 | Done: 6 numbers | count 6 sum 70 average 11.6667 |
12 7 x 40 | Error: not a number after 2 numbers | не создаётся |
| файла нет | Error: cannot open numbers.txt | не создаётся |
После цикла in.eof() отличает «файл кончился» от «встретилось не число». Переводы строк и пробелы для >> одинаковы — числа могут стоять как угодно.
while (!in.eof()) — ошибка. eof() становится истинным только после неудачной попытки прочитать за концом файла. Если после последнего числа есть перевод строки (а он почти всегда есть), тело выполнится лишний раз: чтение не удастся, а сумма или счётчик всё равно изменятся. Правильно — чтение в условии цикла: while (in >> x).Текст читают строками: std::getline(in, line) возвращает поток, и его тоже можно ставить в условие цикла. Чтобы не стирать файл, а дописывать в него, поток записи открывают с флагом std::ios::app — так ведут журналы:
// Нумерация строк: файл из аргумента командной строки читается построчно, строки дописываются в log.txt
#include <iostream>
#include <fstream>
#include <string>
int main(int argc, char* argv[])
{
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " file\n";
return 1;
}
std::ifstream in(argv[1]);
if (!in.is_open()) {
std::cout << "Error: cannot open " << argv[1] << '\n';
return 1;
}
std::ofstream logFile("log.txt", std::ios::app); // дописать в конец, а не стереть
std::string line;
int number = 0;
int longest = 0;
while (std::getline(in, line)) {
++number;
std::cout << number << ": " << line << '\n';
if (static_cast<int>(line.size()) > longest) {
longest = static_cast<int>(line.size());
}
}
logFile << argv[1] << ": " << number << " lines, longest " << longest << '\n';
return 0;
}
> lines text.txt
1: first line
2:
3: third, longest line
После двух запусков в log.txt — две одинаковые строки text.txt: 3 lines, longest 19: вторая дописана после первой. Пустая строка файла читается как пустая строка — getline её не пропускает, в отличие от >>. Имя файла пришло из argv (раздел 10) — так работают почти все консольные утилиты.
| Флаг | Действие |
|---|---|
std::ios::in | чтение (у ifstream — по умолчанию) |
std::ios::out | запись (у ofstream — по умолчанию, вместе с очисткой файла) |
std::ios::app | каждая запись — в конец файла, старое содержимое сохраняется |
std::ios::trunc | очистить файл при открытии |
std::ios::binary | двоичный режим: байты без преобразования |
Флаги объединяются операцией | (раздел 3, побитовые операции): std::ios::out | std::ios::binary.
Текстовый режим — по умолчанию. out << 1000000 пишет семь символов-цифр, и файл можно открыть в Блокноте. В Windows конец строки в текстовом файле — два байта \r\n; в текстовом режиме поток сам превращает '\n' в эти два байта при записи и обратно при чтении. Двоичный режим ничего не превращает: байты попадают в файл как есть. Байты переменной пишет функция write, читает — read:
int x = 1000000;
std::ofstream b("x.bin", std::ios::binary);
b.write(reinterpret_cast<const char*>(&x), sizeof(x)); // 4 байта — как int лежит в памяти
int y = 0;
std::ifstream r("x.bin", std::ios::binary);
r.read(reinterpret_cast<char*>(&y), sizeof(y)); // y == 1000000
write и read принимают адрес начала байтов как char* и их количество. Адрес int — это int*; reinterpret_cast<char*> говорит компилятору: «смотри на эти байты как на массив char». Такое преобразование — сознательный обход типов; в курсе оно встречается только здесь и в буферах Windows API. Файл x.bin в Блокноте — «мусор»: там не цифры, а 4 байта числа.
О студенте нужно хранить фамилию, группу и средний балл. Три отдельных массива names, groups, averages работают, но при сортировке придётся переставлять все три синхронно (раздел 8, ключ «едет» со строкой). Структура объединяет разнотипные данные об одном объекте под одним именем:
// Структура: несколько полей разных типов под одним именем; копирование — целиком, со всеми полями
#include <iostream>
constexpr int NAME_SIZE = 32;
struct Student {
char name[NAME_SIZE];
int group;
double average;
};
int main()
{
Student a = {"Ivanov", 21, 4.5};
Student b = a; // копия всех полей, и массива name тоже
b.average = 3.8;
b.name[0] = 'P';
std::cout << a.name << ' ' << a.group << ' ' << a.average << '\n';
std::cout << b.name << ' ' << b.group << ' ' << b.average << '\n';
Student* p = &b;
p->group = 22; // то же, что (*p).group = 22
std::cout << b.group << ' ' << (*p).group << '\n';
std::cout << "sizeof(Student) = " << sizeof(Student)
<< " = " << sizeof(a.name) << " + " << sizeof(a.group) << " + 4 + " << sizeof(a.average) << '\n';
return 0;
}
Ivanov 21 4.5
Pvanov 21 3.8
22 22
sizeof(Student) = 48 = 32 + 4 + 4 + 8
struct Student { … }; — описание нового типа; память не выделяется, пока не объявлена переменная Student a;. После закрывающей скобки — точка с запятой: без неё expected ';' after struct definition. Структуры описывают вне функций, перед прототипами.a.group. Поле — обычная переменная своего типа: a.name — массив char, со всеми правилами раздела 10. Присвоить ему строку нельзя: a.name = "Petrov" — ошибка incompatible types in assignment; копируют функцией.a == b — no match for 'operator=='; сравнивают по полям.double с адреса, кратного 8, поэтому компилятор вставляет после group 4 пустых байта — выравнивание. Размер структуры — всегда sizeof, а не сумма полей.Для указателя на структуру поле пишется через стрелку: p->group — это (*p).group. Скобки обязательны: точка выполняется раньше звёздочки, и *p.group означает *(p.group) — ошибка request for member 'group' in 'p', which is of pointer type; g++ сам подсказывает: maybe you meant to use '->'.
| Параметр | Что получает функция | Когда |
|---|---|---|
void f(Student s) | копию всех 48 байт | редко: копия дорогая и изменения теряются |
void f(const Student& s) | саму структуру, только чтение | правило курса для функций, которые читают |
void f(Student& s) | саму структуру, с изменением | функция заполняет или меняет поля |
void f(Student* s) | адрес; поля — s->group; может быть nullptr | функции Windows API: они получают адрес структуры и заполняют её поля |
Student f() | функция возвращает структуру целиком | нужно вернуть несколько значений — вместо нескольких параметров-ссылок |
Последняя строка таблицы — то, чего не умеют массивы: структуру можно вернуть через return, она скопируется. В разделе 14 вы встретите структуры Windows API — STARTUPINFO, PROCESS_INFORMATION: программа объявляет переменную-структуру и передаёт её адрес в CreateProcess, а та заполняет поля.
Список студентов — массив структур: Student list[100]. list[i] — одна структура, list[i].average — её поле. В функцию массив структур передаётся, как любой массив (раздел 10): адрес и число элементов.
// Массив структур: студенты читаются из текстового файла, обрабатываются функциями
#include <iostream>
#include <fstream>
#include <iomanip>
constexpr int NAME_SIZE = 32;
constexpr int MAX_STUDENTS = 100;
struct Student {
char name[NAME_SIZE];
int group;
double average;
};
int readStudents(const char fileName[], Student list[], int capacity);
void printStudent(const Student& s);
int bestIndex(const Student list[], int n);
double groupAverage(const Student list[], int n, int group);
int main()
{
Student list[MAX_STUDENTS];
const int n = readStudents("students.txt", list, MAX_STUDENTS);
if (n <= 0) {
std::cout << "Error: no students read\n";
return 1;
}
std::cout << std::fixed << std::setprecision(2);
for (int i = 0; i < n; ++i) {
printStudent(list[i]);
}
std::cout << "Best: ";
printStudent(list[bestIndex(list, n)]);
std::cout << "Group 21 average: " << groupAverage(list, n, 21) << '\n';
return 0;
}
// Читает строки «фамилия группа средний_балл»; возвращает число прочитанных, -1 — файл не открылся
int readStudents(const char fileName[], Student list[], int capacity)
{
std::ifstream in(fileName);
if (!in.is_open()) {
return -1;
}
int n = 0;
while (n < capacity && in >> std::setw(NAME_SIZE) >> list[n].name >> list[n].group >> list[n].average) {
++n;
}
return n;
}
// Одна строка: фамилия, группа, балл
void printStudent(const Student& s)
{
std::cout << std::left << std::setw(12) << s.name << std::right << std::setw(4) << s.group
<< std::setw(6) << s.average << '\n';
}
// Индекс студента с наибольшим баллом (первого из равных)
int bestIndex(const Student list[], int n)
{
int best = 0;
for (int i = 1; i < n; ++i) {
if (list[i].average > list[best].average) {
best = i;
}
}
return best;
}
// Средний балл по группе; 0 — в группе никого
double groupAverage(const Student list[], int n, int group)
{
double sum = 0;
int count = 0;
for (int i = 0; i < n; ++i) {
if (list[i].group == group) {
sum += list[i].average;
++count;
}
}
return count == 0 ? 0 : sum / count;
}
students.txt | Вывод |
|---|---|
Ivanov 21 4.5 Petrova 22 4.8 Sidorov 21 3.9 Kuznetsova 22 4.8 | Ivanov 21 4.50 Petrova 22 4.80 Sidorov 21 3.90 Kuznetsova 22 4.80 Best: Petrova 22 4.80 Group 21 average: 4.20 |
Одна строка файла — одна запись; чтение всех трёх полей стоит в условии цикла, и неполная последняя строка не попадёт в массив. n < capacity проверяется первым: при полном массиве чтение в list[capacity] не начнётся. Сортировка массива структур — алгоритмы раздела 8, только обменивают целые структуры: Student temp = list[i]; list[i] = list[j]; list[j] = temp; — поля «едут» вместе.
Массив структур можно сохранить одной операцией — байтами, как int в пункте 04:
// Двоичный файл: массив структур записывается и читается байтами, одной операцией
#include <iostream>
#include <fstream>
constexpr int NAME_SIZE = 32;
struct Student {
char name[NAME_SIZE];
int group;
double average;
};
int main()
{
const Student list[3] = {
{"Ivanov", 21, 4.5},
{"Petrova", 22, 4.8},
{"Sidorov", 21, 3.9}
};
std::ofstream out("students.bin", std::ios::binary);
out.write(reinterpret_cast<const char*>(list), sizeof(list));
out.close();
Student loaded[3];
std::ifstream in("students.bin", std::ios::binary);
in.read(reinterpret_cast<char*>(loaded), sizeof(loaded));
if (in.gcount() != static_cast<std::streamsize>(sizeof(loaded))) {
std::cout << "Error: file is shorter than expected\n";
return 1;
}
for (int i = 0; i < 3; ++i) {
std::cout << loaded[i].name << ' ' << loaded[i].group << ' ' << loaded[i].average << '\n';
}
in.seekg(0, std::ios::end); // в конец файла: позиция = размер в байтах
std::cout << "File size: " << in.tellg() << " bytes\n";
return 0;
}
Ivanov 21 4.5
Petrova 22 4.8
Sidorov 21 3.9
File size: 144 bytes
gcount() — сколько байт реально прочитал последний read: так проверяется, что файл не короче ожидаемого. seekg переставляет позицию чтения, tellg сообщает её. Файл из записей одного размера позволяет прочитать сразу запись номер k: in.seekg(k * sizeof(Student)) — без чтения предыдущих.
| Текстовый файл | Двоичный файл | |
|---|---|---|
| Читает человек | да, в Блокноте | нет |
| Размер записи | разный: «Ivanov 21 4.5» и «Kuznetsova 22 4.8» | всегда sizeof — 48 байт |
| Переход к записи k | только чтением всех предыдущих | seekg(k * sizeof(Student)) |
| Переносимость | любая программа, любой компьютер | только программа с той же структурой, тем же компилятором и разрядностью: выравнивание и размер int могут отличаться |
Поле-указатель или std::string | пишется значением | в файл попадёт адрес, а не строка — после чтения он недействителен |
Поэтому в структурах для двоичных файлов строки — массивы char фиксированной длины, а не std::string. Так же устроены структуры Windows API: их поля — числа и массивы фиксированного размера.
| Ошибка | Признак | Сообщение g++ | Как избежать |
|---|---|---|---|
| открытие не проверено | программа молча выводит нули или ничего | нет | if (!in.is_open()) — сообщение и код 1 |
| файл не там, где его ищут | «cannot open», хотя файл есть | нет | запуск из каталога с данными; путь целиком |
while (!in.eof()) | последнее значение учтено дважды или лишняя итерация | нет | чтение в условии: while (in >> x) |
ofstream без app для журнала | старые записи пропали | нет | std::ios::app |
нет ; после структуры | не собирается | expected ';' after struct definition | }; |
| точка у указателя | не собирается | request for member 'x' in 'p', which is of pointer type | p->x |
сравнение структур a == b | не собирается | no match for 'operator==' | сравнение по полям, своей функцией |
| присваивание строки полю-массиву | не собирается | incompatible types in assignment of 'const char [7]' to 'char [32]' | копирование функцией (раздел 10) |
| размер структуры — сумма полей | неверные смещения в двоичном файле | нет | только sizeof |
| структура по значению в функцию, которая её меняет | изменения теряются | нет | Student&; для чтения — const Student& |
std::string в структуре для двоичного файла | после чтения — аварийное завершение | нет | массив char фиксированной длины |
ifstream и ofstream работают как cin и cout; открытие всегда проверяетсяofstream очищает файл; дописывать — std::ios::appwhile (in >> x), while (std::getline(in, line)); eof() — только после циклаwrite/read с reinterpret_cast<char*> и sizeofsizeof: компилятор добавляет байты выравниванияp->x — поле по указателю; в функцию — const Student& для чтенияstd::string и указателейfail(); раздел 8 — массивы и сортировка; раздел 9 — указатели, * и приоритет; раздел 10 — массивы и строки в функциях, argv. Нужен для: раздела 12 — класс вырастает из структуры; разделов 14–19 — структуры Windows API (STARTUPINFO, PROCESS_INFORMATION, CHAR_INFO, SYSTEM_INFO, sockaddr_in) и файлы через CreateFile/ReadFile, которые устроены как двоичный режим.