Раздел 11 — Файлы и структуры

Как сохранить данные между запусками программы и как описать объект из нескольких полей

Прогресс курса Раздел 11 из 20

Что вы освоите в этом разделе

4 академических часа теории, практической работы нет. В конце — тест по разделу на портале: пока он не сдан на 70 баллов, раздел 12 закрыт. Все программы страницы — полные; собирайте их командой g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe и запускайте из каталога, где лежат их файлы данных.
01

Файл как поток

Всё, что программа хранит в переменных, исчезает, когда она завершается. Чтобы данные дожили до следующего запуска или попали в другую программу, их пишут в файл. В 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);чтение и запись одного файла—
02

Чтение чисел до конца файла

Сколько чисел в файле, заранее неизвестно. Читают, пока чтение удаётся: выражение 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 40Error: not a number after 2 numbersне создаётся
файла нетError: cannot open numbers.txtне создаётся

После цикла in.eof() отличает «файл кончился» от «встретилось не число». Переводы строк и пробелы для >> одинаковы — числа могут стоять как угодно.

Цикл while (!in.eof()) — ошибка. eof() становится истинным только после неудачной попытки прочитать за концом файла. Если после последнего числа есть перевод строки (а он почти всегда есть), тело выполнится лишний раз: чтение не удастся, а сумма или счётчик всё равно изменятся. Правильно — чтение в условии цикла: while (in >> x).
03

Строки и дописывание в конец

Текст читают строками: 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) — так работают почти все консольные утилиты.

04

Режимы открытия. Текст и байты

ФлагДействие
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 байта числа.

05

Структура

О студенте нужно хранить фамилию, группу и средний балл. Три отдельных массива 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
06

Стрелка и структура в функции

Для указателя на структуру поле пишется через стрелку: 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, а та заполняет поля.

07

Массив структур

Список студентов — массив структур: 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; — поля «едут» вместе.

08

Структура в двоичном файле

Массив структур можно сохранить одной операцией — байтами, как 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: их поля — числа и массивы фиксированного размера.

09

Ловушки раздела

ОшибкаПризнакСообщение 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 typep->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::app
  • Читают в условии цикла: while (in >> x), while (std::getline(in, line)); eof() — только после цикла
  • Двоичный режим пишет байты как есть: write/read с reinterpret_cast<char*> и sizeof
  • Структура — тип из нескольких полей; присваивание копирует все поля; сравнения целиком нет
  • Размер структуры — sizeof: компилятор добавляет байты выравнивания
  • p->x — поле по указателю; в функцию — const Student& для чтения
  • Для двоичного файла — поля фиксированного размера, без std::string и указателей
Связи раздела. Опирается на раздел 3 — побитовое «или» для флагов; раздел 6 — состояние потока и fail(); раздел 8 — массивы и сортировка; раздел 9 — указатели, * и приоритет; раздел 10 — массивы и строки в функциях, argv. Нужен для: раздела 12 — класс вырастает из структуры; разделов 14–19 — структуры Windows API (STARTUPINFO, PROCESS_INFORMATION, CHAR_INFO, SYSTEM_INFO, sockaddr_in) и файлы через CreateFile/ReadFile, которые устроены как двоичный режим.
Тест по разделу — на учебном портале. Тест, попытки и оценки — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 10: Массивы и строки в функциях Тест на портале