Проанализируйте следующий C-код и укажите все возможные проблемы с памятью: char* make_copy(const char* s){ char* p = malloc(strlen(s)); strcpy(p, s); return p; } void use(){ char* a = make_copy("hi"); free(a); printf("%s", a); } Объясните, как избежать ошибок (выбор интерфейса, проверка размеров, владение памятью), и сравните стратегию управления памятью в C с моделями владения в Rust и автоматической сборкой мусора

23 Окт в 12:45
2 +1
0
Ответы
1
Основные проблемы в приведённом коде
- Неправильный размер буфера при выделении:
- В вызове используется `malloc(strlen(s));` — для копирования требуется место ещё и для завершающего нуль-байта. Нужно выделять strlen(s)+1 \text{strlen}(s) + 1 strlen(s)+1 байт.
- Риск целочисленного переполнения при вычислении размера (маловероятно для коротких строк, но нужно учитывать в общем виде).
- Отсутствует проверка результата `malloc`:
- Если `malloc` вернёт NULLNULLNULL, то `strcpy(p, s)` вызовет UB.
- Использование после освобождения (use-after-free):
- В `use()` вызывается `free(a); printf("%s", a);` — чтение после `free` — неопределённое поведение.
- Интерфейс/владение не документированы:
- Неясно, кто отвечает за `free` возвращённого указателя — в такой форме вызывающий должен освобождать, но это не явно.
Как избежать ошибок — рекомендации и исправление
1) Правильное выделение и проверка:
- Выделять strlen(s)+1 \text{strlen}(s) + 1 strlen(s)+1 байт и проверять результат `malloc`.
- Копирование можно делать через `memcpy` или `strcpy` после проверки, либо использовать `strdup` (если доступна).
Пример безопасной реализации:
char* make_copy(const char* s) {
if (s == NULL) return NULL;
size_t len = strlen(s);
char* p = malloc(len + 1); // нужно выделить strlen + 1
if (p == NULL) return NULL;
memcpy(p, s, len + 1);
return p;
}
(в тексте числовые выражения: выделять len+1 \text{len} + 1 len+1 байт, проверять p==NULL p == NULL p==NULL)
2) Проверки на переполнение (для больших размеров):
- Если вычисляете размер по формулам, проверяйте, что сумма/произведение не превышают `SIZE_MAX`. Например, если вычисляете n=a+bn = a + bn=a+b, убедиться, что a≤SIZE_MAX−ba \le SIZE\_MAX - baSIZE_MAXb.
3) Интерфейс владения:
- Документируйте, кто владеет памятью: функция либо возвращает новый владеющий буфер (и вызывающий обязан `free`), либо принимает буфер и его размер для заполнения (в этом случае вызывающий управляет памятью).
- Альтернатива: возвращать статус и записывать в буфер-аргумент: `int copy_to(char *dst, size_t dstsize, const char *src)`.
4) Правильный порядок освобождения:
- Не использовать указатель после `free`. В `use()` делать `printf("%s", a); free(a);` или после `free(a)` присвоить `a = NULL`.
Сравнение стратегий управления памятью
- C (ручное управление):
- Преимущества: детерминированное освобождение, низкий накладной расход.
- Недостатки: легко допустить утечки, use-after-free, двойное освобождение, ошибки размеров и переполнения. Требует явных соглашений об ответственности (владении).
- Rust (система владения и займов — ownership / borrow checker):
- Компилятор гарантирует отсутствие use-after-free, двойного free и гонок данных (в случае отсутствия `unsafe`).
- Передача владения делает вызов безопасным: при выходе владельца память освобождается автоматически; временные заимствования (borrows) проверяются по времени жизни.
- Для C-интеропа используются `CString`, `Box`, явные преобразования владения — ошибки памяти сводятся к минимуму, но есть большая строгость API.
- Языки с автоматической сборкой мусора (Java, Go, C# и др.):
- Память освобождается автоматически GC, что исключает большинство use-after-free и двойных free.
- Минусы: непредсказуемое время освобождения, накладные затраты на GC, возможные удержания объектов (утечки через живые ссылки).
- Для взаимодействия с системными ресурсами всё равно требуется явное закрытие/освобождение (файлы, сокеты), обычно через финализаторы/контекстные менеджеры.
Краткие практические выводы
- Исправьте выделение: выделять strlen(s)+1 \text{strlen}(s) + 1 strlen(s)+1 и проверять p==NULL p == NULL p==NULL.
- Не читать память после `free`.
- Выбирайте понятный интерфейс владения (возвращаемый буфер = вызывающий освобождает, или буфер-аргумент = вызывающий выделяет).
- Рассмотрите использование более безопасных абстракций (strdup, библиотечные функции с ограничением размера, или переход на Rust / GC-язык при возможности), если важна защита от классов ошибок.
23 Окт в 13:13
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир