Введение

Си — мощный, но требовательный язык. Прямое управление памятью и отсутствие «защиты от дурака» делают его уязвимым к ошибкам, которые могут привести к падению программы, утечкам памяти или даже уязвимостям. В этой статье разберём типичные ошибки новичков и способы их избежать.

1. Утечки памяти: забытый free()

Динамическая память выделяется через malloc, calloc или realloc, но её нужно освобождать вручную.

Пример ошибки:

void create_array() {
    int *arr = malloc(10 * sizeof(int)); 
    // ... работа с массивом, но забыли free(arr)
}

После вызова функции память останется занятой до завершения программы.

Как исправить:

  • Всегда освобождайте память через free().

  • Используйте valgrind для поиска утечек.

Правильный код:

void create_array() {
    int *arr = malloc(10 * sizeof(int));
    if (arr == NULL) {
        // Обработка ошибки аллокации
        return;
    }
    // ... работа с массивом
    free(arr); // Освобождаем!
}

2. Use-After-Free: обращение к освобождённой памяти

Ошибка:

int *ptr = malloc(sizeof(int));
free(ptr);
*ptr = 42; // Опасно: запись в освобождённую память!

Это вызывает неопределённое поведение (UB).

Решение:

  • После free() присваивайте указателю NULL:

    free(ptr);
    ptr = NULL; // Теперь попытка *ptr = 42 вызовет segfault.

3. Выход за границы массива

Пример:

int arr[5];
for (int i = 0; i <= 5; i++) { // Ошибка: i <= 5
    arr[i] = i; // При i=5 выходим за границы
}

Как избежать:

  • Используйте константы или sizeof для определения размера:

    #define ARR_SIZE 5
    int arr[ARR_SIZE];
    for (int i = 0; i < ARR_SIZE; i++) { ... }
  • Для динамических массивов храните размер отдельно.

4. Неинициализированные указатели

Ошибка:

int *ptr; // Не инициализирован
*ptr = 10; // Запись в случайный адрес → UB!

Решение:

  • Всегда инициализируйте указатели:

    int *ptr = NULL; // Теперь попытка записи вызовет ошибку.
  • Проверяйте указатель перед использованием:

    if (ptr != NULL) {
        *ptr = 10;
    }

5. Динамическая память в функциях: кто отвечает за free?

Опасный пример:

int* create_array(int size) {
    int *arr = malloc(size * sizeof(int));
    return arr;
}

void main() {
    int *data = create_array(10);
    // ... забыли free(data) → утечка!
}

Как избежать:

  • Договоритесь, кто освобождает память: функция или вызывающий код.

  • Используйте комментарии:

    // Возвращает указатель на массив. Память должна быть освобождена через free()!
    int* create_array(int size);

6. Магия чисел и «забытый break в switch

Ошибка:

switch (status) {
    case 1: 
        printf("OK");
    case 2: // Забыли break!
        printf("Error");
        break;
}

Если status=1, выполнится и case 1, и case 2.

Решение:

  • Всегда добавляйте break, если это не преднамеренно.

  • Заменяйте «магические числа» на константы:

    #define STATUS_OK 1
    #define STATUS_ERROR 2

7. Переполнение буфера

Пример:

char buffer[10];
scanf("%s", buffer); // Если ввести больше 9 символов → переполнение!

Как исправить:

  • Используйте функции с ограничением размера:

    fgets(buffer, sizeof(buffer), stdin);
  • Или явно указывайте лимит в scanf:

    scanf("%9s", buffer); // Максимум 9 символов

Инструменты, которые спасут вас

  1. Valgrind — ищет утечки и невалидные операции с памятью:

    valgrind --leak-check=full ./your_program
  2. Compiler Warnings — включает предупреждения в GCC/Clang:

    gcc -Wall -Wextra -Werror your_code.c
  3. Cppcheck — статический анализатор:

    cppcheck --enable=all your_code.c

Заключение

  • Проверяйте указатели перед использованием.

  • Освобождайте память сразу, как она стала ненужной.

  • Тестируйте код с помощью Valgrind и включайте все предупреждения компилятора.

  • Избегайте магии — используйте константы и понятные имена переменных.

Си требует дисциплины, но взамен даёт полный контроль над системой. Учитесь на чужих ошибках, а не на своих!

Полезные ресурсы:

Комментарии (18)


  1. MonkeyWatchingYou
    19.02.2025 18:46

    Ещё не суйте сырые руки в щиток.
    Но! Если написать маркером "UNSAFE" на руке, то можно. Новомодные языки такое практикуют.


  1. voidptr0
    19.02.2025 18:46

    Тот, кто в 21 веке пишет на C, вероятно, знает что и зачем он делает.


    1. AndronNSK
      19.02.2025 18:46

      Далеко не всегда.

      Очень часто на Си пишут люди, которых нарекли программистамми и дали задание писать для микроконтррллеров.


  1. zatim
    19.02.2025 18:46

    В большинстве случаев на Си пишут для встраиваемых систем, а у них объем памяти фиксирован и заранее известен, поэтому нет необходимости использовать динамическое выделение памяти. По этой же причине можно использовать глобальные переменные и полностью отказаться от указателей. Большая часть описанных в статье проблем теряют актуальность.


    1. arielf
      19.02.2025 18:46

      Вы не поверите, но практически всё окружение и пользовательские программы в UNIX, почти все операционные системы, их компиляторы, многие научные и иные библиотеки написаны на C.

       TIOBE
      TIOBE

      C по популярности такой же как и Java.

       TIOBE
      TIOBE


  1. Jijiki
    19.02.2025 18:46

    код на С хорошо переносится.


  1. juramehanik
    19.02.2025 18:46

    Прекрасные примеры, которые поймает статический анализатор, а есть что поинтереснее?



  1. Serpentine
    19.02.2025 18:46

    Вы устали от:❌ Тайных утечек памяти, которые пожирают ресурсы.

    ❌ Загадочных падений программы без объяснения причин.

    ❌ Указателей-призраков, стреляющих в вас из темноты сегфолтов.

    Для кого:

    • Начинающие разработчики, которые хотят писать код, а не баги.

    • Те, кто считает, что free() — это про свободу, а не про память.

    • Все, кто устал гуглить «почему Си опять вылетает».

    Взаимоисключающие параграфы, тем более после прочтения статьи. Любой уважающий себя автор сишного букваря (или цикла уроков) эти "ошибки" озвучит в начале соответствующих тем.

    ChatGPT мне по этому промту (название статьи) информации больше накидал и подробнее. Только он не боялся слова "разыменование" и не путал тестирование с профилированием и отладкой.

    Выход за границы массива

    Как избежать:

    • Используйте константы

    Так?

    #define ARRAY_SIZE 10
    /* some code */
    int my_best_array[ARRAY_SIZE] = {0};
    for(int i = 0; i <= ARRAY_SIZE; i++)
      printf("%d\n", my_best_array[i]);
    /* some code else */

    или sizeof для определения размера:

    Сколько нулей выведет?

    void print_my_best_array(int best_array[])
    {
        for(int i = 0; i < sizeof(best_array); i++)
          printf("%d\n", best_array[i]);
    }
    
    int main()
    {
        int my_best_array[10] = {0};
        print_my_best_array(my_best_array);
        return 0;
    }

    Полезные ресурсы:

    Можно еще ссылок на документацию к GCC и Clang дать, в статье же про Compiler Warnings было.

    Ну и K&R отлично подойдет "для тех, кто считает, что free() — это про свободу".


  1. sic
    19.02.2025 18:46

    Никогда не освобождаю память (и у меня все работает). Как? Неужели стоит написать статью об этом? #NothingIsFree