Лекция 16, классы памяти. Димамическое выделение памяти (неполный конспект)

Классы памяти

Класс памяти “Время жизни” данных Метод выделения памяти размер класса памяти
Статический (глобальные и статические переменные, строковые константы) Всё время работы программы Память выделяет компилятор во время компиляции Фиксирован во время компиляции
Автоматический (он же стек) локальные переменные и аргументы функций С момента объявления до конца работы функции Программа выделяет память под локальные переменные автоматически Максимальный размер стека фиксирован, его нельзя поменять после запуска программы. Однако заполнение стека всё время меняется и зависит от того, сколько в данный момент вызвано функций и каков суммарный размер их локальных переменных.
Динамический (куча) Задаётся программистом. Начинается в момент выделения памяти и заканчивается в момент её освобождения. Программист выделяет память вручную. Выделение памяти – функция malloc, освобождение – free. Размер ограничен только объёмом физической памяти и настройками операционной системы. Размер кучи может увеличиваться во время выполнения программы.

Статический класс памяти

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

Глобальная переменная определяется на верхнем уровне определений, то есть вне функций, и видна всем функциям, идущим в исходном коде после неё.

int N = 10;

void print_N(void)
{
    printf("N = %d\n", N);
}

void change_N(int new_N)
{
    N = new_N;
}

Как видим, функции print_N и change_N могут обмениваться информацией через глобальную переменную N.

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

Поэтому при использовании глобальных переменных нужно соблюдать некоторые правила:

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

Статические переменные определяются внутри функций с добавлением слова static. Они так же живут всё время программы, но инициализируются только один раз, при первом запуске функции, и доступны только внутри этой функции.

// При первом вызове вернёт 0, при втором 1, при третьем 2.
int get_counter(void)
{
    static int counter = 0; // Инициализируется только при первом вызове.
    return counter++;
}

Компилятор подсчитывает размер статического класса памяти при компиляции программы и записывает в исполняемый файл с машинным кодом информацию о его размере. При запуске программы операционная система выделяет программе необходимое количество памяти и запускает функцию main. Размер статического класса на протяжении всей работы программы остаётся неизменным.

TODO: строковые константы

Автоматический класс памяти

В автоматическом классе хранятся локальные переменные и аргументы функций. Мы уже обсуждали их хранение на стеке вызова функций.

void foo(void)
{
    char s1[10] = "Hello!";
    // 10 байт на стеке.

    char *s2 = "Hello!";
    // sizeof(char *) байт на стеке для указателя,
    // 7 байт (6 букв + символ '\0') в статическом классе для строковой константы.
}
// При завершении функции foo будет освобождена память её кадра стека, то есть, переменная
// s1 целиком и переменная s2 частично, только указатель. Само содержимое строковой константы
// хранится в статическом КП и будет продолжать следующего вызова функции, чтобы вновь
// стать той областью памяти, на которую указывает s2.

Однако, так как теперь мы знакомы с указателями, стоит сделать важное замечание о времени жизни переменных внутри фукнции. Оно ограничено временем выполнения фукнции. Даже несмотря на то, что есть техническая возможность сохранить где-нибудь указатель на локальную переменную или вернуть его из фукнции, делать это категорически не стоит, ведь при вызове другой функции её данные могут быть расположены поверх старых данных других фукнций. Например, в коде

void a(void)
{
    b();
    c();
}

данные фукнции c будут распологаться поверх старых данных b. Очевидно, что пользоваться указателем на старые данные функции b нельзя. В то же время, никто не запрещает пользоваться указателем на локальные переменные, пока выполняется функция, что мы и делали каждый раз, когда вызывали scanf.

Ограничение на время жизни данных нам позволит снять динамический класс памяти.

Динамический класс памяти

Все переменные в языке Си хранятся либо в автоматическом (локальные), либо в статическом классах памяти (глобальные и статические). Получить доступ к динамической памяти можно только через указатели, хранящиеся в автоматическом и статическом классах памяти.

Прототипы функций для работы с динамической памятью находятся в заголовочном файле stdlib.h:

#include <stdlib.h>

Среди них есть функция для выделения памяти malloc (англ. memory allocation – выделение памяти):

void *malloc(size_t size);
// size -- размер необходимого куска памяти в байтах
// функция возвращает неинициализированный указатель;
// для работы с этой памятью нужно преобразовать указатель
// к нужному типу (char *, int *, double *, int ** и так далее)

malloc ищет среди свободных областей динамической памяти подходящие по размеру (размер куска >= size).

Если такая часть памяти найдена, то первые size байт помечаются как используемые и возвращается указатель на начало этой области.

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

Если ОС не сделает этого (недостаточно памяти или будет превышен максимальный размер памяти на один процесс), то malloc вернёт NULL.

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

Для освобождения памяти используется функция free:

void free(void *ptr);

При освобождении памяти она вновь помечается как готовая к использованию и может быть выделена одним из последующих вызовов malloc.

Пример использования динамического выделения памяти:

int arr[10]; // Автоматический класс памяти,
        // выделено sizeof(int) * 4 байт на стеке.

int *arr2 = (int *) malloc(sizeof(int) * 10);
// указатель arr2 размером sizeof(int *) в автоматическом классе памяти,
// и ещё sizeof(int) * 10 байт для массива в динамической памяти.

// ... работаем с этими двумя массивами

// Память из-под массива arr освободится автоматически,

// память из-под arr2 надо освободить вручную:
free(arr2);

В принципе, в этом примере недостаточно ясно, зачем же нам динамический класс памяти, так как мы просто могли бы воспользоваться обычным массивом. Но если нам нужно обойти ограничение на время жизни данных на стеке, то нужно обязательно использовать динамическую память:

char *mystrdup(char *src)
{
    int len = strlen(src) + 1;
    char *s = (char *) malloc(sizeof(char) * len);
    strcpy(s, src);
    return s;
}