Урок 10. Динамическая память и файлы
Обычный массив получает размер при объявлении, и этот размер не меняется. А если число элементов станет известно только во время работы программы — например, его введут с клавиатуры, — нужна динамическая память: программа просит у системы столько байт, сколько нужно, и возвращает их, когда они больше не нужны. Эта память выделяется в области, которая называется кучей (heap), и её гораздо больше, чем стека.
malloc и free
Функции работы с памятью объявлены в stdlib.h.
#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 55 4 3 2 1malloc(n * sizeof(int))выделяет место подnцелых чисел и возвращает адрес начала. Размер считается в байтах, поэтому число элементов умножают наsizeofэлемента.- Результат кладут в указатель и дальше работают с ним как с массивом:
a[i]. - Если памяти не хватило,
mallocвозвращаетNULL— это нужно проверять. free(a)возвращает память системе. После этого обращаться кa[i]нельзя.
Память, полученная от malloc, не обнулена — в ней мусор. Если нужны нули, есть calloc(n, sizeof(int)): те же n элементов, но заполненные нулями.
Каждый malloc должен закончиться ровно одним free. Забытый free — утечка памяти: в длинной программе или в цикле она съедает всю память. Повторный free того же адреса и обращение к памяти после free — неопределённое поведение, обычно с падением программы. Подробно — в справке Утечка памяти.
Полезная привычка — после free(a) писать a = NULL;. Случайное обращение к NULL сразу уронит программу, и ошибку будет легко найти, а free(NULL) по стандарту ничего не делает.
realloc — изменить размер
realloc меняет размер выделенного блока, сохраняя его содержимое. Так делают массив, который растёт по мере чтения данных:
#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 пользуются только новым адресом.
Файлы: запись
Работа с файлом идёт в три шага: открыть (fopen), читать или писать, закрыть (fclose). Открытый файл представлен указателем на тип FILE.
#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".
perror выводит причину ошибки понятным текстом, например numbers.txt: No such file or directory или Permission denied. Имя файла без пути ищется в текущей папке — той, из которой запущена программа. Среды разработки обычно запускают программу из папки проекта, поэтому файл нужно класть туда.
Файлы: чтение
#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) { … }.
Частые ошибки с файлами
- Не проверен результат
fopen. Файла нет или он в другой папке —fopenвернулNULL, а следующийfscanf(NULL, …)роняет программу сsegmentation fault. - Забыт
fclose. Записанное может остаться в буфере (промежуточной памяти, откуда данные пишутся на диск порциями) и не попасть в файл, если программа завершится аварийно. Да и число одновременно открытых файлов ограничено. - Путь в Windows. В строке C обратная косая черта — начало управляющей последовательности, поэтому путь пишут с двойной чертой
"C:\\data\\in.txt"или с прямой"C:/data/in.txt"— Windows понимает оба. - Проверка конца файла через
feof. Циклwhile (!feof(f))выполняется на один раз больше, чем нужно: конец файла обнаруживается только после неудачного чтения. Проверяйте результат самой функции чтения, как в примерах.
Коротко
malloc(n * sizeof(тип))— память подnэлементов; результат проверяют наNULL; в конце —free.calloc— то же с нулями,realloc— изменить размер (результат — во временный указатель).- После
freeпамять не трогают; забытыйfree— утечка. - Файл:
fopen→fprintf/fscanf/fgets→fclose; результатfopenпроверяют наNULL. - Режим
"w"очищает файл,"a"дописывает,"r"только читает.