Глиф Учебники

Урок 10. Динамическая память и файлы

Курс «C» · урок 10 из 10 · 4 минуты чтения

Обычный массив получает размер при объявлении, и этот размер не меняется. А если число элементов станет известно только во время работы программы — например, его введут с клавиатуры, — нужна динамическая память: программа просит у системы столько байт, сколько нужно, и возвращает их, когда они больше не нужны. Эта память выделяется в области, которая называется кучей (heap), и её гораздо больше, чем стека.

malloc и free

Функции работы с памятью объявлены в stdlib.h.

C
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    scanf("%d", &n);
    int *a = malloc(n * sizeof(int));
    if (a == NULL) {
        printf("Не хватило памяти\n");
        return 1;
    }
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
    }
    for (int i = n - 1; i >= 0; i--) {
        printf("%d ", a[i]);
    }
    printf("\n");
    free(a);
    return 0;
}
Ввод
5
1 2 3 4 5
Вывод
5 4 3 2 1
стек переменные main n 5 int a int * куча (heap) — память по запросу malloc(n * sizeof(int)) 5 × 4 = 20 байт 1 a[0] 2 a[1] 3 a[2] 4 a[3] 5 a[4] адрес malloc — взять память a[i] — как массив free(a) — вернуть без free — утечка памяти; после free к a[i] обращаться нельзя
Переменные n и a лежат в стеке функции main, а malloc выделяет в куче блок под 5 чисел по 4 байта и отдаёт его адрес в указатель a; после работы блок возвращают через free(a).

Память, полученная от malloc, не обнулена — в ней мусор. Если нужны нули, есть calloc(n, sizeof(int)): те же n элементов, но заполненные нулями.

Каждый malloc должен закончиться ровно одним free. Забытый free — утечка памяти: в длинной программе или в цикле она съедает всю память. Повторный free того же адреса и обращение к памяти после free — неопределённое поведение, обычно с падением программы. Подробно — в справке Утечка памяти.

Полезная привычка — после free(a) писать a = NULL;. Случайное обращение к NULL сразу уронит программу, и ошибку будет легко найти, а free(NULL) по стандарту ничего не делает.

realloc — изменить размер

realloc меняет размер выделенного блока, сохраняя его содержимое. Так делают массив, который растёт по мере чтения данных:

C
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int cap = 2, n = 0, x;
    int *a = malloc(cap * sizeof(int));
    if (a == NULL) return 1;
    while (scanf("%d", &x) == 1) {
        if (n == cap) {
            cap *= 2;
            int *tmp = realloc(a, cap * sizeof(int));
            if (tmp == NULL) {
                free(a);
                return 1;
            }
            a = tmp;
        }
        a[n++] = x;
    }
    printf("Прочитано %d чисел, место под %d\n", n, cap);
    free(a);
    return 0;
}
Ввод
7 1 8 2 8 1 8
Вывод
Прочитано 7 чисел, место под 8

Результат realloc сначала кладут во временный указатель. Если написать a = realloc(a, …) и памяти не хватит, realloc вернёт NULL, адрес старого блока потеряется, и его уже не освободить. Блок может переехать на новое место, поэтому после realloc пользуются только новым адресом.

ввод: 7 1 8 2 8 1 8 cap = 2 n = 2 7 1 cap = 4 n = 4 7 1 8 2 cap = 8 n = 7 7 1 8 2 8 1 8 n == cap: место кончилось — cap *= 2, realloc содержимое сохраняется, адрес блока может смениться свободно int *tmp = realloc(a, cap * sizeof(int)); сначала в tmp, потом a = tmp
При вводе 7 1 8 2 8 1 8 место кончается дважды: cap растёт с 2 до 4 и до 8, realloc сохраняет уже прочитанные числа, а адрес блока может смениться — поэтому результат сначала кладут в tmp.

Файлы: запись

Работа с файлом идёт в три шага: открыть (fopen), читать или писать, закрыть (fclose). Открытый файл представлен указателем на тип FILE.

C
#include <stdio.h>

int main(void) {
    FILE *f = fopen("numbers.txt", "w");
    if (f == NULL) {
        perror("numbers.txt");
        return 1;
    }
    for (int i = 1; i <= 5; i++) {
        fprintf(f, "%d\n", i * i);
    }
    fclose(f);
    printf("Файл записан\n");
    return 0;
}
Вывод
Файл записан

fprintf работает как printf, только первым аргументом получает файл. После запуска рядом с программой появится файл numbers.txt с числами 1, 4, 9, 16 и 25 — каждое на своей строке.

Второй аргумент fopen — режим:

РежимЧто делает
"r"чтение; файла нет — fopen вернёт NULL
"w"запись; файл создаётся, а если он был — очищается
"a"дописывание в конец; файла нет — создаётся

Режим "w" стирает всё содержимое существующего файла сразу при открытии, без вопросов. Перепутав "w" и "r", легко потерять данные. Чтобы добавить строки к файлу, нужен "a".

"r" — чтение 1 4 9 16 25 читается с начала нет файла — NULL "w" — запись пусто всё старое стирается нет файла — создаётся "a" — дописывание 1 4 9 16 25 новое — в конец файла нет файла — создаётся
Режим "r" только читает файл с начала, а если файла нет, fopen вернёт NULL; "w" при открытии стирает всё содержимое; "a" дописывает новое в конец; в "w" и "a" отсутствующий файл создаётся.

perror выводит причину ошибки понятным текстом, например numbers.txt: No such file or directory или Permission denied. Имя файла без пути ищется в текущей папке — той, из которой запущена программа. Среды разработки обычно запускают программу из папки проекта, поэтому файл нужно класть туда.

Файлы: чтение

C
#include <stdio.h>

int main(void) {
    FILE *out = fopen("data.txt", "w");
    if (out == NULL) return 1;
    fprintf(out, "12 7 30\n5\n");
    fclose(out);

    FILE *f = fopen("data.txt", "r");
    if (f == NULL) {
        perror("data.txt");
        return 1;
    }
    int x, sum = 0, count = 0;
    while (fscanf(f, "%d", &x) == 1) {
        sum += x;
        count++;
    }
    fclose(f);
    printf("Чисел: %d, сумма: %d\n", count, sum);
    return 0;
}
Вывод
Чисел: 4, сумма: 54

Программа сначала сама создаёт файл, чтобы пример работал без подготовки, а потом читает его. fscanf — тот же scanf, но для файла; цикл while (fscanf(…) == 1) читает числа до конца файла.

Файл по строкам читает fgets(line, sizeof(line), f): он возвращает NULL, когда строки кончились, поэтому цикл чтения выглядит так: while (fgets(line, sizeof(line), f) != NULL) { … }.

Частые ошибки с файлами

Коротко

Проверьте себя

1. Сколько байт выделит malloc(10 * sizeof(int)), если int занимает 4 байта?
2. Что случится с существующим файлом при fopen("log.txt", "w")?
3. Почему результат realloc кладут сначала во временный указатель?
4. Что нужно сделать с памятью, полученной от malloc, когда она больше не нужна?