← Назад к вопросам

Какие знаешь проблемы с утечками памяти?

1.7 Middle🔥 181 комментариев
#Умные указатели и управление памятью

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

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблемы с утечками памяти в C++

Утечка памяти — это ситуация, когда выделенная динамическая память больше не используется, но не освобождена. Для C/C++ Backend разработчика понимание причин и способов диагностики утечек памяти критично для создания надёжных систем, особенно в долгоживущих серверных приложениях.

1. Базовые утечки памяти

Забытый delete:

int main() {
    int* ptr = new int(42);
    // ... использование ptr ...
    // ОШИБКА: delete не вызван
    // Память остаётся выделенной до завершения программы
    return 0;
}

Нарушение правила RAII (Resource Acquisition Is Initialization):

class FileHandle {
private:
    FILE* handle;
public:
    FileHandle(const char* filename) {
        handle = fopen(filename, "r");
    }
    // ОШИБКА: деструктор не закрывает файл
    // При удалении объекта файловый дескриптор утекает
};

int main() {
    FileHandle* file = new FileHandle("data.txt");
    // ... работа с файлом ...
    delete file;  // Файл остаётся открытым!
    return 0;
}

2. Циклические ссылки в умных указателях

Одна из наиболее коварных проблем:

#include <memory>
#include <iostream>

struct Node {
    int value;
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // Циклическая ссылка!
    
    ~Node() { std::cout << "Node destroyed" << std::endl; }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;  // node2 refcount = 2
    node2->prev = node1;  // node1 refcount = 2
    
    // refcount(node1) = 1 (только node2->prev)
    // refcount(node2) = 1 (только node1->next)
    // Когда выходим из scope, оба объекта имеют refcount > 0
    // Оба остаются в памяти НАВЕЧНО — утечка памяти!
    
    return 0;
}

Решение: Используйте std::weak_ptr для обратных ссылок:

struct Node {
    int value;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Слабая ссылка не увеличивает refcount
    
    ~Node() { std::cout << "Node destroyed" << std::endl; }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;
    node2->prev = node1;  // weak_ptr не удерживает объект
    
    // Когда выходим из scope:
    // refcount(node1) = 1, refcount(node2) = 1
    // Оба объекта удаляются корректно
    
    return 0;
}

3. Исключения и утечки памяти

Проблема: Если исключение выброшено между new и delete, памя утекает:

int main() {
    int* data = new int[1000];
    
    try {
        // Код, который может выбросить исключение
        risky_operation();
    } catch (...) {
        // Oops! data не удалена
        throw;  // Переброс исключения
    }
    
    delete[] data;  // Может не быть выполнено
    return 0;
}

Решение: Используйте RAII и умные указатели:

int main() {
    std::unique_ptr<int[]> data(new int[1000]);
    // или
    auto data = std::make_unique<int[]>(1000);
    
    try {
        risky_operation();
    } catch (...) {
        // Даже при исключении destructor unique_ptr очистит память
        throw;
    }
    
    // Гарантированное удаление при выходе из scope
    return 0;
}

4. Двойное удаление (Double Delete)

int main() {
    int* ptr = new int(42);
    delete ptr;       // Первое удаление
    delete ptr;       // ОШИБКА! Undefined behavior
    return 0;
}

Решение: После delete устанавливайте указатель в nullptr:

int main() {
    int* ptr = new int(42);
    delete ptr;
    ptr = nullptr;    // Безопасно
    delete ptr;       // OK: delete nullptr безопасно
    return 0;
}

Оптимально — использовать умные указатели, которые автоматически обнуляются.

5. Утечки при перегрузке операторов

class String {
private:
    char* data;
    int size;
public:
    String(const char* str) : size(strlen(str)) {
        data = new char[size + 1];
        strcpy(data, str);
    }
    
    ~String() { delete[] data; }
    
    // ОШИБКА: нет operator=
    // String a = "hello";
    // String b = "world";
    // a = b;  // Shallow copy! data указывает на чужую память
};

Решение: Реализуйте Rule of Five (или Rule of Zero с умными указателями):

class String {
private:
    std::unique_ptr<char[]> data;
    int size;
public:
    String(const char* str) : size(strlen(str)) {
        data = std::make_unique<char[]>(size + 1);
        strcpy(data.get(), str);
    }
    
    // Деструктор автоматически
    // Copy constructor автоматически
    // Copy assignment автоматически
    // Move constructor автоматически
    // Move assignment автоматически
};

6. Утечки в библиотеках третьих сторон

// Внешняя библиотека
void external_lib_allocate(void** ptr) {
    *ptr = malloc(1024);
}

int main() {
    void* data;
    external_lib_allocate(&data);
    // Если библиотека требует специального способа удаления
    // и мы используем обычный delete — утечка
    delete data;  // ОШИБКА! Библиотека использовала malloc
    
    // Правильно:
    free(data);
    return 0;
}

Инструменты обнаружения утечек

1. Valgrind (Linux):

valgrind --leak-check=full --show-leak-kinds=all ./program

2. AddressSanitizer (ASAN):

g++ -fsanitize=address -g program.cpp -o program
./program

3. LeakSanitizer (LSAN):

g++ -fsanitize=leak -g program.cpp -o program
./program

4. Debug heap в Windows:

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>

int main() {
    _CrtDumpMemoryLeaks();  // В конце программы
    return 0;
}

Лучшие практики

1. Используйте умные указатели вместо raw pointers:

std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
std::shared_ptr<MyClass> shared = std::make_shared<MyClass>();

2. Следуйте RAII:

class Resource {
public:
    Resource() { acquire(); }     // Захват в конструкторе
    ~Resource() { release(); }    // Освобождение в деструкторе
};

3. Избегайте raw new/delete:

// ❌ Плохо
MyClass* ptr = new MyClass();
// ... код ...
delete ptr;

// ✅ Хорошо
auto ptr = std::make_unique<MyClass>();
// Автоматическое освобождение

4. Используйте слабые ссылки для циклов:

std::shared_ptr<Node> parent;
std::weak_ptr<Node> child;  // Не удерживает объект

5. Проверяйте регулярно:

make leak-check  # или собственный скрипт с Valgrind/ASAN

Почему это важно

Утечки памяти в долгоживущих приложениях (серверы, системные сервисы) приводят к:

  • Постепенному исчерпанию доступной памяти
  • Снижению производительности из-за давления на swap
  • Краху приложения после некоторого времени работы
  • Финансовым потерям при работе на облачных серверах

Вывод: Всегда используйте RAII, умные указатели и инструменты анализа. Это сэкономит часы отладки в продакшене.