Вектор. Логическое описание и физическое представление: указатель, размер и realloc

Модуль "Файл и вектор" • урок 4 из 6 • ~8 минут

Суть урока. Логически вектор - структура из двух полей: {указатель на данные, текущий размер}. Физически - сплошной блок памяти из кучи (malloc), доступ по индексу за O(1). Изменение размера берет на себя realloc: расширяет блок на месте либо выделяет новый и копирует. Освобождение - free.

Вектор. Логическое описание на C

Вектор. Логическое описание и физическое представление

В прошлом уроке вектор был набором из восьми абстрактных операций. Теперь реализуем их на C. В классическом Паскале встроенных динамических массивов нет, а в C есть все необходимое: ручное управление памятью через malloc и realloc.

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

typedef struct {
    T* data;  /* указатель на начало блока данных */
    int size; /* текущее количество элементов */
} Vector;
Вектор. Логическое описание и физическое представление: структура Vector с полями data и size, указывающая на сплошной блок памяти
Логическое и физическое представление вектора

Создание и простые операции

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

void Create(Vector* v, int sz) {
    v->size = sz;
    v->data = malloc(sizeof(T) * v->size);
}

bool Empty(Vector* v) {
    return v->size == 0;
}

int Size(Vector* v) {
    return v->size;
}

Доступ к элементам: не забываем про границы

Чтение (Load) и запись (Save) обязаны проверять, что индекс лежит в допустимом диапазоне от 0 до size - 1: C сам за границами не следит, а обращение мимо блока - неопределенное поведение:

T Load(Vector* v, int i) {
    if ((i >= 0) && (i < v->size))
        return v->data[i];
    /* сюда попадать не должны: индекс вне границ */
}

void Save(Vector* v, int i, T t) {
    if ((i >= 0) && (i < v->size))
        v->data[i] = t;
}

Изменение длины, сравнение, уничтожение

Копировать элементы вручную не нужно: realloc сначала пытается расширить блок на месте, а если за ним нет свободной памяти - выделяет новый, переносит данные и сам освобождает старый блок. Это та самая операция ИЗМЕНИТЬ_ДЛИНУ за O(N) из спецификации:

void Resize(Vector* v, int sz) {
    v->size = sz;
    v->data = realloc(v->data, sizeof(T) * v->size);
}

bool Equal(Vector* l, Vector* r) {
    if (l->size != r->size)
        return false;
    for (int i = 0; i < l->size; i++) {
        if (l->data[i] != r->data[i])
            return false;
    }
    return true;
}

void Destroy(Vector* v) {
    v->size = 0;
    free(v->data); /* без free - утечка памяти */
}

Более совершенные реализации - например, std::vector в C++ - выделяют память с запасом: емкость (capacity) больше реального размера (size), поэтому частые добавления в конец не дергают аллокатор каждый раз.

А как в Python? Вся работа с malloc и realloc скрыта внутри интерпретатора, но устроена так же: CPython-список - это структура с указателем на сплошной блок и счетчиком размера. А вызывать free вручную не нужно: память возвращает сборщик мусора, когда на список не остается ссылок.

Коротко: главное

  • Логический тип: struct с указателем на данные и текущим размером.
  • Физически элементы лежат сплошным блоком - отсюда доступ по индексу за O(1).
  • Создание - malloc; чтение и запись - с проверкой границ [0, size - 1].
  • Изменение размера - realloc: расширяет на месте или выделяет новый блок и копирует.
  • После работы память обязательно возвращается системе вызовом free.

Проверьте себя

Вектору нужно вырасти, но память сразу за его блоком занята. Что сделает realloc?

Как вам урок? Спасибо за отзыв!