Управление памятью

Глава 5. Продвинутые концепции #

5.2 Управление памятью в C++ #

Управление памятью — критически важный аспект программирования на C++, требующий понимания механизмов выделения и освобождения ресурсов.

1. Динамическое выделение памяти #

1.1 Базовые принципы #

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

// Простой пример динамического выделения памяти
int* dynamicInteger = new int(42);  // Выделение памяти для одного целого числа
int* dynamicArray = new int[10];    // Выделение памяти для массива из 10 целых чисел

// Освобождение памяти
delete dynamicInteger;        // Освобождение памяти для одиночного объекта
delete[] dynamicArray;        // Освобождение памяти для массива

1.2 Важные замечания #

  • Всегда освобождайте динамически выделенную память
  • Невыполнение этого правила приводит к утечкам памяти
  • Современный C++ рекомендует использовать умные указатели

2. Умные указатели #

2.1 Общие принципы #

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

2.2 unique_ptr #

Эксклюзивный указатель, владеющий единственной ссылкой на объект.

#include <memory>

// Создание unique_ptr
std::unique_ptr<int> ptr1(new int(42));

// Передача владения
std::unique_ptr<int> ptr2 = std::move(ptr1);  // ptr1 теперь nullptr

// Фабричный метод создания
auto ptr3 = std::make_unique<int>(100);

// Работа с массивами
auto arrayPtr = std::make_unique<int[]>(5);

Особенности unique_ptr: #

  • Запрещено копирование
  • Разрешено перемещение
  • Автоматическое освобождение памяти при выходе из области видимости
  • Минимальные накладные расходы

2.3 shared_ptr #

Указатель с подсчетом ссылок, позволяющий совместное владение объектом.

// Создание shared_ptr
auto sharedInt = std::make_shared<int>(42);

// Создание нескольких ссылок
auto sharedInt2 = sharedInt;  // Увеличение счетчика ссылок

// Количество владельцев
std::cout << sharedInt.use_count() << std::endl;  // 2

// Пользовательский deletor
auto customDeleter = [](int* p) { 
    std::cout << "Освобождение памяти" << std::endl; 
    delete p; 
};
std::shared_ptr<int> ptr(new int(100), customDeleter);

Характеристики shared_ptr: #

  • Атомарный подсчет ссылок
  • Больше накладных расходов по сравнению с unique_ptr
  • Автоматическое освобождение при последней ссылке

2.4 weak_ptr #

Слабая ссылка на объект, управляемый shared_ptr, не влияющая на подсчет ссылок.

// Пример использования weak_ptr
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;

// Проверка валидности
if (auto lockedPtr = weakPtr.lock()) {
    std::cout << "Объект существует: " << *lockedPtr << std::endl;
} else {
    std::cout << "Объект был удален" << std::endl;
}

Применение weak_ptr: #

  • Предотвращение циклических зависимостей
  • Слабые ссылки на кэшированные объекты
  • Проверка существования объекта без увеличения счетчика ссылок

3. Операторы new и delete #

3.1 Низкоуровневое управление памятью #

// Базовое использование new и delete
class MyClass {
public:
    MyClass() { std::cout << "Конструктор" << std::endl; }
    ~MyClass() { std::cout << "Деструктор" << std::endl; }
};

int main() {
    // Выделение памяти
    MyClass* obj1 = new MyClass();  // Вызов конструктора
    MyClass* obj2 = new MyClass[5]; // Массив объектов

    // Освобождение памяти
    delete obj1;        // Один объект
    delete[] obj2;      // Массив объектов

    return 0;
}

3.2 Перегрузка new и delete #

class CustomMemoryManager {
public:
    // Пользовательская реализация new
    static void* operator new(size_t size) {
        std::cout << "Пользовательский new" << std::endl;
        return ::operator new(size);
    }

    // Пользовательская реализация delete
    static void operator delete(void* ptr) noexcept {
        std::cout << "Пользовательский delete" << std::endl;
        ::operator delete(ptr);
    }
};

4. Утечки памяти #

4.1 Причины возникновения #

  • Забытые delete
  • Потерянные указатели
  • Неправильное использование динамической памяти
  • Циклические зависимости в указателях

4.2 Пример утечки памяти #

void leakyFunction() {
    int* leak = new int[1000];  // Память не освобождается
    // Нет delete - утечка памяти
}

4.3 Методы предотвращения #

  1. Использование умных указателей
  2. RAII (Resource Acquisition Is Initialization)
  3. Статический анализ кода
  4. Инструменты профилирования памяти

Заключение #

Рекомендации #

  • Предпочтительно использовать умные указатели
  • Избегать ручного управления памятью
  • Применять RAII
  • Использовать средства статического анализа

Типичные ошибки #

  1. Двойное освобождение памяти
  2. Использование освобожденной памяти
  3. Забытые delete
  4. Неправильное управление циклическими ссылками

Инструменты диагностики #

  • Valgrind
  • AddressSanitizer
  • Visual Studio Memory Profiler
  • CLion Memory Analysis

Дополнительные материалы #

  • C++ Core Guidelines по управлению памятью
  • Документация по std::unique_ptr, std::shared_ptr
  • Современные техники безопасного управления ресурсами