Alocação Dinâmica de Memória em C++

C++Beginner
Pratique Agora

Introdução

Neste laboratório, você aprenderá como lidar com a alocação dinâmica de memória em C++. Começará alocando memória usando o operador new, e em seguida liberará a memória com o operador delete. Você também explorará a criação de arrays dinâmicos, o uso de smart pointers como shared_ptr e unique_ptr, e a verificação de vazamentos de memória (memory leaks). Essas habilidades são essenciais para o gerenciamento de memória eficaz na programação C++.

O laboratório cobre as seguintes etapas principais: alocação de memória usando o operador new, liberação de memória com o operador delete, criação de arrays dinâmicos com new[], exclusão de arrays usando delete[], implementação do smart pointer shared_ptr, uso de unique_ptr para propriedade exclusiva, verificação de vazamentos de memória e tratamento de falhas na alocação de memória.

Alocar Memória Usando o Operador new

Nesta etapa, você aprenderá como usar o operador new em C++ para alocar memória dinamicamente em tempo de execução. A alocação dinâmica de memória permite que você crie variáveis e arrays cujo tamanho é determinado durante a execução do programa, proporcionando mais flexibilidade em comparação com a alocação de memória estática.

Abra o WebIDE e crie um novo arquivo chamado dynamic_memory.cpp no diretório ~/project:

touch ~/project/dynamic_memory.cpp

Adicione o seguinte código a 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;
}

Vamos detalhar os conceitos-chave:

  1. int* dynamicInteger = new int;:

    • O operador new aloca memória para um inteiro
    • Retorna um ponteiro para a memória alocada
    • Cria a variável na heap (pilha de memória dinâmica), não na stack (pilha de memória estática)
  2. *dynamicInteger = 42;:

    • Usa o operador de desreferência * para atribuir um valor
    • Armazena o valor 42 na memória alocada dinamicamente

Compile e execute o programa:

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

Exemplo de saída:

Dynamically allocated integer value: 42
Memory address: 0x55f4e8a042a0

Pontos-chave sobre o operador new:

  • Aloca memória dinamicamente durante o tempo de execução
  • Retorna um ponteiro para a memória alocada
  • A memória é alocada na heap
  • O tamanho pode ser determinado em tempo de execução
  • Permite um gerenciamento de memória mais flexível

Observação: Nas próximas etapas, você aprenderá como liberar corretamente essa memória alocada dinamicamente para evitar vazamentos de memória (memory leaks).

Liberar Memória com o Operador delete

Nesta etapa, você aprenderá como usar o operador delete para liberar corretamente a memória alocada dinamicamente em C++. Não liberar a memória pode levar a vazamentos de memória (memory leaks), o que pode causar problemas de desempenho em seu programa.

Abra o WebIDE e crie um novo arquivo chamado memory_release.cpp no diretório ~/project:

touch ~/project/memory_release.cpp

Adicione o seguinte código a 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;
}

Pontos-chave sobre o operador delete:

  1. delete dynamicInteger;:

    • Libera a memória previamente alocada com new
    • Previne vazamentos de memória
    • Devolve a memória ao sistema
  2. dynamicInteger = nullptr;:

    • Define o ponteiro como nullptr após a exclusão
    • Ajuda a prevenir o uso acidental da memória liberada
    • Fornece uma verificação de segurança adicional

Compile e execute o programa:

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

Exemplo de saída:

Value before release: 42
Memory has been successfully released

Regras importantes de gerenciamento de memória:

  • Sempre use delete para memória alocada com new
  • Defina os ponteiros como nullptr após a exclusão
  • Nunca use um ponteiro após ter sido excluído
  • Cada new deve ter um delete correspondente

Criar Arrays Dinâmicos com new[]

Nesta etapa, você aprenderá como criar arrays dinâmicos usando o operador new[] em C++. Arrays dinâmicos permitem que você aloque memória para múltiplos elementos em tempo de execução, proporcionando mais flexibilidade do que arrays estáticos.

Abra o WebIDE e crie um novo arquivo chamado dynamic_array.cpp no diretório ~/project:

touch ~/project/dynamic_array.cpp

Adicione o seguinte código a 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;
}

Pontos-chave sobre arrays dinâmicos:

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

    • Aloca memória para um array de inteiros
    • arraySize determina o número de elementos
    • A memória é alocada na heap
    • O tamanho pode ser determinado em tempo de execução
  2. Inicialização e acesso ao array:

    • Use a indexação de array padrão dynamicArray[i]
    • Pode ser manipulado como um array regular
    • Permite dimensionamento dinâmico com base nas condições de tempo de execução

Compile e execute o programa:

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

Exemplo de saída:

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

Considerações importantes:

  • Arrays dinâmicos fornecem flexibilidade na alocação de memória
  • O tamanho pode ser determinado em tempo de execução
  • Lembre-se de usar new[] para alocação e delete[] para desalocação
  • Sempre combine os métodos de alocação e desalocação

Excluir Arrays Usando delete[]

Nesta etapa, você aprenderá como liberar corretamente a memória alocada para arrays dinâmicos usando o operador delete[]. O gerenciamento adequado da memória é crucial para evitar vazamentos de memória (memory leaks) e garantir o uso eficiente dos recursos.

Abra o WebIDE e crie um novo arquivo chamado delete_array.cpp no diretório ~/project:

touch ~/project/delete_array.cpp

Adicione o seguinte código a 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;
}

Pontos-chave sobre a exclusão de arrays dinâmicos:

  1. delete[] dynamicArray;:

    • Libera corretamente a memória para todo o array
    • Deve usar delete[] para arrays alocados com new[]
    • Previne vazamentos de memória
    • Chama o destrutor para cada elemento do array
  2. dynamicArray = nullptr;:

    • Define o ponteiro como nullptr após a exclusão
    • Impede o acesso acidental à memória liberada
    • Fornece uma verificação de segurança adicional

Compile e execute o programa:

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

Exemplo de saída:

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

Regras importantes de gerenciamento de memória:

  • Sempre use delete[] para arrays alocados com new[]
  • Nunca misture delete e delete[]
  • Defina os ponteiros como nullptr após a exclusão
  • Cada new[] deve ter um delete[] correspondente

Implementar Smart Pointer shared_ptr

Nesta etapa, você aprenderá sobre shared_ptr, um smart pointer da Biblioteca Padrão C++ que fornece gerenciamento automático de memória e contagem de referência. Smart pointers ajudam a prevenir vazamentos de memória e simplificam o gerenciamento de memória.

Abra o WebIDE e crie um novo arquivo chamado shared_pointer.cpp no diretório ~/project:

touch ~/project/shared_pointer.cpp

Adicione o seguinte código a 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;
}

Pontos-chave sobre shared_ptr:

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

    • Cria um ponteiro compartilhado com alocação dinâmica de memória
    • Inicializa o objeto com o argumento do construtor
    • Mais eficiente do que a criação separada de new e shared_ptr
  2. ptr1.use_count():

    • Retorna o número de instâncias shared_ptr apontando para o mesmo objeto
    • Ajuda a rastrear a contagem de referência do objeto
  3. Gerenciamento automático de memória:

    • A memória é liberada automaticamente quando nenhum shared_ptr referencia o objeto
    • Previne erros de gerenciamento manual de memória

Compile e execute o programa:

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

Exemplo de saída:

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

Características importantes de shared_ptr:

  • Gerenciamento automático de memória
  • Contagem de referência
  • Previne vazamentos de memória
  • Contagem de referência thread-safe

Usar unique_ptr para Propriedade Exclusiva

Nesta etapa, você aprenderá sobre unique_ptr, um smart pointer que fornece propriedade exclusiva de recursos alocados dinamicamente. Ao contrário de shared_ptr, unique_ptr garante que apenas um ponteiro pode possuir o recurso por vez.

Abra o WebIDE e crie um novo arquivo chamado unique_pointer.cpp no diretório ~/project:

touch ~/project/unique_pointer.cpp

Adicione o seguinte código a 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;
}

Pontos-chave sobre unique_ptr:

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

    • Cria um ponteiro único com alocação dinâmica de memória
    • Garante a propriedade exclusiva do recurso
  2. std::move(ptr1):

    • Transfere a propriedade do recurso
    • O ponteiro original se torna nullptr
    • Impede que vários ponteiros possuam o mesmo recurso
  3. Gerenciamento automático de memória:

    • O recurso é automaticamente excluído quando o unique_ptr sai do escopo
    • Nenhum gerenciamento manual de memória é necessário

Compile e execute o programa:

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

Exemplo de saída:

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

Características importantes de unique_ptr:

  • Propriedade exclusiva
  • Gerenciamento automático de memória
  • Não pode ser copiado, apenas movido
  • Previne vazamentos de recursos

Verificar Vazamentos de Memória

Nesta etapa, você aprenderá como detectar e prevenir vazamentos de memória em C++ usando o Valgrind, uma poderosa ferramenta de depuração de memória. Vazamentos de memória ocorrem quando a memória alocada dinamicamente não é liberada corretamente, fazendo com que seu programa consuma quantidades crescentes de memória.

Primeiro, instale o Valgrind no terminal:

sudo apt update
sudo apt install -y valgrind

Crie um exemplo de vazamento de memória no WebIDE criando memory_leak.cpp no diretório ~/project:

touch ~/project/memory_leak.cpp

Adicione o seguinte código a 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;
}

Compile o programa com símbolos de depuração:

g++ -g memory_leak.cpp -o memory_leak

Execute o Valgrind para detectar vazamentos de memória:

valgrind --leak-check=full ./memory_leak

Exemplo de saída do 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)

Como você pode ver, o Valgrind detectou um vazamento de memória de 12 bytes em 3 blocos. O objeto Resource criado em createMemoryLeak() não foi excluído, causando o vazamento de memória.

Para corrigir o vazamento de memória, modifique o código para excluir corretamente os recursos:

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

Compile e execute o programa novamente com o Valgrind para verificar se o vazamento de memória foi corrigido:

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)

Pontos-chave sobre vazamentos de memória:

  • Sempre delete a memória alocada dinamicamente
  • Use smart pointers como unique_ptr e shared_ptr
  • Use o Valgrind para detectar vazamentos de memória
  • Compile com a flag -g para obter melhores informações de depuração

Lidar com Falhas na Alocação de Memória

Nesta etapa, você aprenderá como lidar com falhas de alocação de memória em C++. Quando a alocação dinâmica de memória falha, o operador new lança uma exceção std::bad_alloc, que você pode capturar e tratar de forma adequada.

Abra o WebIDE e crie um novo arquivo chamado memory_allocation.cpp no diretório ~/project:

touch ~/project/memory_allocation.cpp

Adicione o seguinte código a 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;
}

Pontos-chave sobre o tratamento de falhas de alocação de memória:

  1. Tratamento de exceção com std::bad_alloc:

    • Bloco try-catch captura exceções de alocação de memória
    • Fornece informações detalhadas sobre o erro
    • Evita a falha do programa
  2. Alocação std::nothrow:

    • Impede o lançamento de exceções
    • Retorna nullptr em caso de falha na alocação
    • Permite o tratamento silencioso de falhas

Compile e execute o programa:

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

Exemplo de saída:

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

Estratégias importantes de alocação de memória:

  • Sempre verifique se há falhas na alocação
  • Use tratamento de exceção ou std::nothrow
  • Implemente mecanismos de fallback
  • Evite alocar blocos de memória extremamente grandes

Resumo

Neste laboratório, você aprenderá como lidar com a alocação dinâmica de memória em C++. Primeiro, você aprenderá a usar o operador new para alocar memória dinamicamente em tempo de execução, permitindo um gerenciamento de memória mais flexível em comparação com a alocação estática. Em seguida, você aprenderá como liberar corretamente essa memória alocada dinamicamente usando o operador delete para evitar vazamentos de memória. Além disso, você explorará a criação de arrays dinâmicos com new[] e sua exclusão com delete[]. O laboratório também aborda o uso de smart pointers, como shared_ptr e unique_ptr, para simplificar o gerenciamento de memória e evitar armadilhas comuns. Por fim, você aprenderá técnicas para verificar e lidar com falhas de alocação de memória.