Работа с динамической памятью

Динамическая память является ресурсом операционной системой и выделяется по явному запросу процесса.

Контроль за выделением и освобождением динамической памяти — дело рук программиста.

Компилятор снимает с себя ответственность за то, что память выделена, но не освобождена, или же не выделена к необходимому моменту.

Ошибка сегментации (Segmentation fault)

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

Пример

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

void foo(int *pointer)
{
    *pointer = 0; //потенциальный Segmentation fault
}

int main()
{
    int *p;
    int x;
    *NULL = 10; //совсем очевидный Segmentation fault
    *p = 10; //достаточно очевидный Segmentation fault
    foo(NULL); //скрытый Segmentation fault
    scanf("%d", x); //скрытый и очень популярный у новичков на Си Segmentation fault
   
    return 0;
}
 

Утечка памяти (Memory leak)

Если процесс попросил у ОС память, а затем про нее забыл и более не использует, это называется утечкой памяти.

Утечки памяти не являются критической ошибкой и в небольшом масштабе допустимы, если процесс работает очень недолго (секунды). Однако при разработке сколько-нибудь масштабируемого и выполняющегося продолжительное время приложения, допущение даже маленьких утечек памяти — серьезная ошибка.

Пример

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

void swap_arrays(int *A, int *B, size_t N)
{
    int * tmp = (int *) malloc(sizeof(int)*N); //временный массив
    for(size_t i = 0; i < N; i++)
        tmp [i] = A[i];
    for(size_t i = 0; i < N; i++)
        A[i] = B[i];
    for(size_t i = 0; i < N; i++)
        B[i] = tmp [i];
    //выходя из функции, забыли освободить память временного массива
}

int main()
{
    int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int B[10] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
    swap_arrays(A, B, 10); //функция swap_arrays() имеет утечку памяти

    int *p;
    for(int i = 0; i < 10; i++) {
        p = (int *)malloc(sizeof(int)); //выделение памяти в цикле 10 раз
        *p = 0;
    }
    free(p); //а освобождение вне цикла - однократное. Утечка!

    return 0;
}
 

Как избежать ошибок работы с динамической памятью?

  1. Во-первых, быть аккуратным и внимательным.
  2. Во-вторых, если память выделена на одном уровне, освобождение должно быть совершено на том же уровне. Например, если функция выделила память, она же должна ее освободить перед выходом.
    В исключительных ситуациях могут существовать "порождающие" функции, но их нужно "знать в лицо", их имена должны говорить об этом.
    С этой точки зрения пример плохой порождающий функции: стандартная функция Си создания дубликата строки strdup(). По ее имени не очевидно, что при этом выделяется динамическая память, которая обязательно должна быть освобождена.
  3. В-третьих, существуют специальные программные средства, которые позволяют искать утечки памяти, например Valgrind.

Работа с динамической памятью в Си и С++ различается.

Хотя в С++ также в конечном счете используются системные вызовы по выделению и освобождению памяти, они "обернуты" в операторы new и delete. В С++ не рекомендуется использовать механизм  malloc() и free() без насущной необходимости обратной совместимости.