Динамическое выделение памяти в C++

C++Beginner
Практиковаться сейчас

Введение

В этом лабораторном занятии вы научитесь работать с динамическим выделением памяти в C++. Вы начнете с выделения памяти с помощью оператора new, а затем освободите память с помощью оператора delete. Вы также изучите создание динамических массивов, использование умных указателей, таких как shared_ptr и unique_ptr, и проверку на утечки памяти. Эти навыки являются важными для эффективного управления памятью в программировании на C++.

В лабораторном занятии рассматриваются следующие ключевые шаги: выделение памяти с помощью оператора new, освобождение памяти с помощью оператора delete, создание динамических массивов с помощью new[], удаление массивов с помощью delete[], реализация умного указателя shared_ptr, использование unique_ptr для эксклюзивного владения, проверка на утечки памяти и обработка ошибок выделения памяти.

Выделение памяти с использованием оператора new

На этом шаге вы узнаете, как использовать оператор new в C++ для динамического выделения памяти во время выполнения программы. Динамическое выделение памяти позволяет создавать переменные и массивы, размер которых определяется во время выполнения программы, что обеспечивает большую гибкость по сравнению со статическим выделением памяти.

Откройте WebIDE и создайте новый файл с именем dynamic_memory.cpp в директории ~/project:

touch ~/project/dynamic_memory.cpp

Добавьте следующий код в файл dynamic_memory.cpp:

#include <iostream>

int main() {
    // Dynamically allocate an integer using new
    int* dynamicInteger = new int;

    // Assign a value to the dynamically allocated integer
    *dynamicInteger = 42;

    // Print the value of the dynamically allocated integer
    std::cout << "Dynamically allocated integer value: " << *dynamicInteger << std::endl;

    // Print the memory address of the dynamically allocated integer
    std::cout << "Memory address: " << dynamicInteger << std::endl;

    return 0;
}

Разберем основные концепции:

  1. int* dynamicInteger = new int;:

    • Оператор new выделяет память для целого числа
    • Возвращает указатель на выделенную память
    • Создает переменную в куче (heap), а не на стеке (stack)
  2. *dynamicInteger = 42;:

    • Использует оператор разыменования * для присвоения значения
    • Сохраняет значение 42 в динамически выделенной памяти

Скомпилируйте и запустите программу:

g++ dynamic_memory.cpp -o dynamic_memory
./dynamic_memory

Пример вывода:

Dynamically allocated integer value: 42
Memory address: 0x55f4e8a042a0

Основные моменты об операторе new:

  • Выделяет память динамически во время выполнения программы
  • Возвращает указатель на выделенную память
  • Память выделяется в куче (heap)
  • Размер можно определить во время выполнения программы
  • Позволяет более гибко управлять памятью

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

Освобождение памяти с использованием оператора delete

На этом шаге вы узнаете, как использовать оператор delete для правильного освобождения динамически выделенной памяти в C++. Если память не будет освобождена, это может привести к утечкам памяти, которые могут вызвать проблемы с производительностью вашей программы.

Откройте WebIDE и создайте новый файл с именем memory_release.cpp в директории ~/project:

touch ~/project/memory_release.cpp

Добавьте следующий код в файл memory_release.cpp:

#include <iostream>

int main() {
    // Dynamically allocate an integer
    int* dynamicInteger = new int;

    // Assign a value to the dynamically allocated integer
    *dynamicInteger = 42;

    // Print the value before releasing memory
    std::cout << "Value before release: " << *dynamicInteger << std::endl;

    // Release the dynamically allocated memory using delete
    delete dynamicInteger;

    // Set the pointer to nullptr after deletion
    dynamicInteger = nullptr;

    // Demonstrate safe pointer usage
    if (dynamicInteger == nullptr) {
        std::cout << "Memory has been successfully released" << std::endl;
    }

    return 0;
}

Основные моменты об операторе delete:

  1. delete dynamicInteger;:

    • Освобождает память, ранее выделенную с помощью new
    • Предотвращает утечки памяти
    • Возвращает память системе
  2. dynamicInteger = nullptr;:

    • Устанавливает указатель в nullptr после удаления
    • Помогает избежать случайного использования освобожденной памяти
    • Предоставляет дополнительную проверку безопасности

Скомпилируйте и запустите программу:

g++ memory_release.cpp -o memory_release
./memory_release

Пример вывода:

Value before release: 42
Memory has been successfully released

Важные правила управления памятью:

  • Всегда используйте delete для памяти, выделенной с помощью new
  • Устанавливайте указатели в nullptr после удаления
  • Никогда не используйте указатель после его удаления
  • Каждый new должен иметь соответствующий delete

Создание динамических массивов с использованием new[]

На этом шаге вы узнаете, как создавать динамические массивы с помощью оператора new[] в C++. Динамические массивы позволяют выделять память для нескольких элементов во время выполнения программы, обеспечивая большую гибкость, чем статические массивы.

Откройте WebIDE и создайте новый файл с именем dynamic_array.cpp в директории ~/project:

touch ~/project/dynamic_array.cpp

Добавьте следующий код в файл dynamic_array.cpp:

#include <iostream>

int main() {
    // Declare the size of the dynamic array
    int arraySize = 5;

    // Create a dynamic integer array using new[]
    int* dynamicArray = new int[arraySize];

    // Initialize array elements
    for (int i = 0; i < arraySize; ++i) {
        dynamicArray[i] = i * 10;
    }

    // Print array elements
    std::cout << "Dynamic Array Contents:" << std::endl;
    for (int i = 0; i < arraySize; ++i) {
        std::cout << "Element " << i << ": " << dynamicArray[i] << std::endl;
    }

    return 0;
}

Основные моменты о динамических массивах:

  1. int* dynamicArray = new int[arraySize];:

    • Выделяет память для массива целых чисел
    • arraySize определяет количество элементов
    • Память выделяется в куче (heap)
    • Размер можно определить во время выполнения программы
  2. Инициализация и доступ к массиву:

    • Используйте стандартную индексацию массива dynamicArray[i]
    • Можно манипулировать как обычным массивом
    • Позволяет динамически изменять размер в зависимости от условий выполнения программы

Скомпилируйте и запустите программу:

g++ dynamic_array.cpp -o dynamic_array
./dynamic_array

Пример вывода:

Dynamic Array Contents:
Element 0: 0
Element 1: 10
Element 2: 20
Element 3: 30
Element 4: 40

Важные аспекты:

  • Динамические массивы обеспечивают гибкость в выделении памяти
  • Размер можно определить во время выполнения программы
  • Не забудьте использовать new[] для выделения и delete[] для освобождения памяти
  • Всегда используйте соответствующие методы выделения и освобождения памяти.

Удаление массивов с использованием delete[]

На этом шаге вы узнаете, как правильно освобождать память, выделенную для динамических массивов, с помощью оператора delete[]. Правильное управление памятью является важным для предотвращения утечек памяти и обеспечения эффективного использования ресурсов.

Откройте WebIDE и создайте новый файл с именем delete_array.cpp в директории ~/project:

touch ~/project/delete_array.cpp

Добавьте следующий код в файл delete_array.cpp:

#include <iostream>

int main() {
    // Declare the size of the dynamic array
    int arraySize = 5;

    // Create a dynamic integer array using new[]
    int* dynamicArray = new int[arraySize];

    // Initialize array elements
    for (int i = 0; i < arraySize; ++i) {
        dynamicArray[i] = i * 10;
    }

    // Print array elements before deletion
    std::cout << "Array Contents Before Deletion:" << std::endl;
    for (int i = 0; i < arraySize; ++i) {
        std::cout << "Element " << i << ": " << dynamicArray[i] << std::endl;
    }

    // Release the dynamically allocated array using delete[]
    delete[] dynamicArray;

    // Set the pointer to nullptr after deletion
    dynamicArray = nullptr;

    // Demonstrate safe pointer usage
    if (dynamicArray == nullptr) {
        std::cout << "Dynamic array has been successfully deleted" << std::endl;
    }

    return 0;
}

Основные моменты при удалении динамических массивов:

  1. delete[] dynamicArray;:

    • Правильно освобождает память для всего массива
    • Необходимо использовать delete[] для массивов, выделенных с помощью new[]
    • Предотвращает утечки памяти
    • Вызывает деструктор для каждого элемента массива
  2. dynamicArray = nullptr;:

    • Устанавливает указатель в nullptr после удаления
    • Предотвращает случайный доступ к освобожденной памяти
    • Предоставляет дополнительную проверку безопасности

Скомпилируйте и запустите программу:

g++ delete_array.cpp -o delete_array
./delete_array

Пример вывода:

Array Contents Before Deletion:
Element 0: 0
Element 1: 10
Element 2: 20
Element 3: 30
Element 4: 40
Dynamic array has been successfully deleted

Важные правила управления памятью:

  • Всегда используйте delete[] для массивов, выделенных с помощью new[]
  • Никогда не смешивайте delete и delete[]
  • Устанавливайте указатели в nullptr после удаления
  • Каждый new[] должен иметь соответствующий delete[]

Реализация умного указателя shared_ptr

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

Откройте WebIDE и создайте новый файл с именем shared_pointer.cpp в директории ~/project:

touch ~/project/shared_pointer.cpp

Добавьте следующий код в файл shared_pointer.cpp:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int value) : data(value) {
        std::cout << "Constructor called. Value: " << data << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor called. Value: " << data << std::endl;
    }

    int getData() const { return data; }

private:
    int data;
};

int main() {
    // Create a shared_ptr to MyClass
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(42);

    // Create another shared_ptr pointing to the same object
    std::shared_ptr<MyClass> ptr2 = ptr1;

    // Print reference count
    std::cout << "Reference Count: " << ptr1.use_count() << std::endl;

    // Access object through shared_ptr
    std::cout << "Data from ptr1: " << ptr1->getData() << std::endl;
    std::cout << "Data from ptr2: " << ptr2->getData() << std::endl;

    // Pointers will automatically free memory when they go out of scope
    return 0;
}

Основные моменты о shared_ptr:

  1. std::make_shared<MyClass>(42):

    • Создает общий (shared) указатель с динамическим выделением памяти
    • Инициализирует объект с аргументом конструктора
    • Более эффективно, чем отдельное создание new и shared_ptr
  2. ptr1.use_count():

    • Возвращает количество экземпляров shared_ptr, указывающих на один и тот же объект
    • Помогает отслеживать количество ссылок на объект
  3. Автоматическое управление памятью:

    • Память автоматически освобождается, когда на объект не ссылается ни один shared_ptr
    • Предотвращает ошибки при ручном управлении памятью

Скомпилируйте и запустите программу:

g++ -std=c++11 shared_pointer.cpp -o shared_pointer
./shared_pointer

Пример вывода:

Constructor called. Value: 42
Reference Count: 2
Data from ptr1: 42
Data from ptr2: 42
Destructor called. Value: 42

Важные характеристики shared_ptr:

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

Использование unique_ptr для эксклюзивного владения

На этом шаге вы узнаете о unique_ptr — умном указателе, который обеспечивает эксклюзивное владение динамически выделенными ресурсами. В отличие от shared_ptr, unique_ptr гарантирует, что в каждый момент времени только один указатель может владеть ресурсом.

Откройте WebIDE и создайте новый файл с именем unique_pointer.cpp в директории ~/project:

touch ~/project/unique_pointer.cpp

Добавьте следующий код в файл unique_pointer.cpp:

#include <iostream>
#include <memory>

class Resource {
public:
    Resource(int value) : data(value) {
        std::cout << "Resource created. Value: " << data << std::endl;
    }

    ~Resource() {
        std::cout << "Resource destroyed. Value: " << data << std::endl;
    }

    int getData() const { return data; }

private:
    int data;
};

void processResource(std::unique_ptr<Resource> resource) {
    std::cout << "Processing resource with value: " << resource->getData() << std::endl;
    // Resource will be automatically deleted when function ends
}

int main() {
    // Create a unique_ptr
    std::unique_ptr<Resource> ptr1 = std::make_unique<Resource>(42);

    // Print resource data
    std::cout << "Resource value: " << ptr1->getData() << std::endl;

    // Transfer ownership using std::move
    std::unique_ptr<Resource> ptr2 = std::move(ptr1);

    // ptr1 is now nullptr
    if (ptr1 == nullptr) {
        std::cout << "ptr1 is now null after moving ownership" << std::endl;
    }

    // Pass unique_ptr to a function (ownership is transferred)
    processResource(std::move(ptr2));

    return 0;
}

Основные моменты о unique_ptr:

  1. std::make_unique<Resource>(42):

    • Создает уникальный указатель с динамическим выделением памяти
    • Гарантирует эксклюзивное владение ресурсом
  2. std::move(ptr1):

    • Передает владение ресурсом
    • Исходный указатель становится nullptr
    • Предотвращает ситуацию, когда несколько указателей владеют одним и тем же ресурсом
  3. Автоматическое управление памятью:

    • Ресурс автоматически удаляется, когда unique_ptr выходит из области видимости
    • Не требуется ручное управление памятью

Скомпилируйте и запустите программу:

g++ -std=c++14 unique_pointer.cpp -o unique_pointer
./unique_pointer

Пример вывода:

Resource created. Value: 42
Resource value: 42
ptr1 is now null after moving ownership
Processing resource with value: 42
Resource destroyed. Value: 42

Важные характеристики unique_ptr:

  • Эксклюзивное владение
  • Автоматическое управление памятью
  • Не может быть скопирован, только перемещен
  • Предотвращает утечки ресурсов

Проверка на утечки памяти

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

Сначала установите Valgrind в терминале:

sudo apt update
sudo apt install -y valgrind

Создайте пример утечки памяти в WebIDE, создав файл memory_leak.cpp в директории ~/project:

touch ~/project/memory_leak.cpp

Добавьте следующий код в файл memory_leak.cpp:

#include <iostream>

class Resource {
public:
    Resource(int value) : data(value) {
        std::cout << "Resource created: " << data << std::endl;
    }

    ~Resource() {
        std::cout << "Resource destroyed: " << data << std::endl;
    }

private:
    int data;
};

void createMemoryLeak() {
    // This creates a memory leak because the resource is not deleted
    Resource* leak = new Resource(42);
    // Missing: delete leak;
}

int main() {
    // Simulate multiple memory leaks
    for (int i = 0; i < 3; ++i) {
        createMemoryLeak();
    }

    return 0;
}

Скомпилируйте программу с символами отладки:

g++ -g memory_leak.cpp -o memory_leak

Запустите Valgrind для обнаружения утечек памяти:

valgrind --leak-check=full./memory_leak

Пример вывода Valgrind:

==1988== Memcheck, a memory error detector
==1988== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1988== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==1988== Command:./memory_leak
==1988==
Resource created: 42
Resource created: 42
Resource created: 42
==1988==
==1988== HEAP SUMMARY:
==1988==     in use at exit: 12 bytes in 3 blocks
==1988==   total heap usage: 5 allocs, 2 frees, 73,740 bytes allocated
==1988==
==1988== 12 bytes in 3 blocks are definitely lost in loss record 1 of 1
==1988==    at 0x4849013: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1988==    by 0x109241: createMemoryLeak() (memory_leak.cpp:19)
==1988==    by 0x109299: main (memory_leak.cpp:26)
==1988==
==1988== LEAK SUMMARY:
==1988==    definitely lost: 12 bytes in 3 blocks
==1988==    indirectly lost: 0 bytes in 0 blocks
==1988==      possibly lost: 0 bytes in 0 blocks
==1988==    still reachable: 0 bytes in 0 blocks
==1988==         suppressed: 0 bytes in 0 blocks
==1988==
==1988== For lists of detected and suppressed errors, rerun with: -s
==1988== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Как вы можете видеть, Valgrind обнаружил утечку памяти в размере 12 байт в 3 блоках. Объект Resource, созданный в функции createMemoryLeak(), не был удален, что привело к утечке памяти.

Чтобы исправить утечку памяти, измените код так, чтобы правильно удалять ресурсы:

void createMemoryLeak() {
    // Properly delete the dynamically allocated resource
    Resource* leak = new Resource(42);
    delete leak;  // Add this line to prevent memory leak
}

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

g++ -g memory_leak.cpp -o memory_leak
valgrind --leak-check=full./memory_leak
==2347== Memcheck, a memory error detector
==2347== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2347== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==2347== Command:./memory_leak
==2347==
Resource created: 42
Resource destroyed: 42
Resource created: 42
Resource destroyed: 42
Resource created: 42
Resource destroyed: 42
==2347==
==2347== HEAP SUMMARY:
==2347==     in use at exit: 0 bytes in 0 blocks
==2347==   total heap usage: 5 allocs, 5 frees, 73,740 bytes allocated
==2347==
==2347== All heap blocks were freed -- no leaks are possible
==2347==
==2347== For lists of detected and suppressed errors, rerun with: -s
==2347== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Основные моменты о утечках памяти:

  • Всегда освобождайте динамически выделенную память с помощью delete
  • Используйте умные указатели, такие как unique_ptr и shared_ptr
  • Используйте Valgrind для обнаружения утечек памяти
  • Компилируйте с флагом -g для получения более подробной информации для отладки

Обработка ошибок выделения памяти

На этом шаге вы узнаете, как обрабатывать ошибки выделения памяти в C++. Когда динамическое выделение памяти завершается неудачно, оператор new выбрасывает исключение std::bad_alloc, которое можно перехватить и обработать корректно.

Откройте WebIDE и создайте новый файл с именем memory_allocation.cpp в директории ~/project:

touch ~/project/memory_allocation.cpp

Добавьте следующий код в файл memory_allocation.cpp:

#include <iostream>
#include <new>
#include <limits>

void demonstrateMemoryAllocation() {
    try {
        // Attempt to allocate a large amount of memory
        const size_t largeSize = 1000000000000; // 1 trillion integers
        int* largeArray = new int[largeSize];

        // This line will not be reached if allocation fails
        std::cout << "Memory allocation successful" << std::endl;

        // Clean up allocated memory
        delete[] largeArray;
    }
    catch (const std::bad_alloc& e) {
        // Handle memory allocation failure
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    }
}

void safeMemoryAllocation() {
    // Use std::nothrow to prevent exception throwing
    int* safeArray = new(std::nothrow) int[1000000];

    if (safeArray == nullptr) {
        std::cerr << "Memory allocation failed silently" << std::endl;
        return;
    }

    // Use the allocated memory
    std::cout << "Safe memory allocation successful" << std::endl;

    // Clean up allocated memory
    delete[] safeArray;
}

int main() {
    std::cout << "Demonstrating memory allocation failure handling:" << std::endl;

    // Method 1: Using exception handling
    demonstrateMemoryAllocation();

    // Method 2: Using std::nothrow
    safeMemoryAllocation();

    return 0;
}

Основные моменты при обработке ошибок выделения памяти:

  1. Обработка исключений с использованием std::bad_alloc:

    • Блок try-catch перехватывает исключения, связанные с выделением памяти
    • Предоставляет подробную информацию об ошибке
    • Предотвращает аварийное завершение программы
  2. Выделение памяти с использованием std::nothrow:

    • Предотвращает выбрасывание исключений
    • Возвращает nullptr при неудачном выделении памяти
    • Позволяет обрабатывать ошибки без выбрасывания исключений

Скомпилируйте и запустите программу:

g++ memory_allocation.cpp -o memory_allocation
./memory_allocation

Пример вывода:

Demonstrating memory allocation failure handling:
Memory allocation failed: std::bad_alloc
Safe memory allocation successful

Важные стратегии выделения памяти:

  • Всегда проверяйте успешность выделения памяти
  • Используйте обработку исключений или std::nothrow
  • Реализуйте механизмы отката
  • Избегайте выделения чрезвычайно больших блоков памяти

Резюме

В этом практическом занятии вы узнаете, как работать с динамическим выделением памяти в C++. Сначала вы научитесь использовать оператор new для динамического выделения памяти во время выполнения программы, что позволяет более гибко управлять памятью по сравнению со статическим выделением. Затем вы узнаете, как правильно освобождать динамически выделенную память с помощью оператора delete, чтобы избежать утечек памяти. Кроме того, вы изучите создание динамических массивов с использованием new[] и их удаление с помощью delete[]. В рамках практического занятия также рассматривается использование умных указателей, таких как shared_ptr и unique_ptr, для упрощения управления памятью и избежания распространенных ошибок. Наконец, вы научитесь методам проверки и обработки ошибок выделения памяти.