Gerenciamento de Memória Dinâmica em C e C++

  1. Distribuição de Memória em Programas C/C++ =============================================

A gestão de memória é um aspecto fundamental no desenvolvimento de software em C e C++. Compreender como a memória é organizada e alocada durante a execução de um programa é crucial para escrever código eficiente e livre de erros. Vamos analisar um exemplo para ilustrar a distribuição da memória:

#include <stdio.h>
#include <stdlib.h> // Para malloc, free

int variavelGlobal = 10;                // Variável global
static int variavelEstaticaGlobal = 20; // Variável estática global

void exemploMemoria() {
    static int variavelEstaticaLocal = 30; // Variável estática local
    int variavelLocal = 40;                 // Variável local na pilha
    int arrPilha[5] = {1, 2, 3, 4, 5};      // Array na pilha
    char textoPilha[] = "Exemplo";          // String na pilha
    const char* pTextoConst = "Constante";  // Ponteiro na pilha, string literal no segmento de código

    int* pHeap1 = (int*)malloc(sizeof(int) * 3); // Alocação na heap
    int* pHeap2 = (int*)calloc(2, sizeof(int));  // Alocação na heap, inicializada com zeros

    // As alocações na heap devem ser liberadas manualmente
    if (pHeap1) {
        pHeap1[0] = 100;
        printf("Conteúdo em pHeap1[0]: %d\n", pHeap1[0]);
        free(pHeap1);
    }
    if (pHeap2) {
        printf("Conteúdo em pHeap2[0] (calloc inicializa com 0): %d\n", pHeap2[0]);
        free(pHeap2);
    }
}

int main() {
    exemploMemoria();
    return 0;
}

A memória de um processo é tipicamente dividida em várias regiões lógicas:

  1. Pilha (Stack): Utilizada para variáveis locais (não estáticas), parâmetros de função e endereços de retorno. A pilha cresce "para baixo" (em direção a endereços de memória menores) em muitos sistemas. A alocação e desalocação são automáticas.

  2. Heap: Região para alocação dinâmica de memória em tempo de execução. A alocação é feita por funções como malloc e new, e a desalocação deve ser manual (free, delete). A heap geralmente cresce "para cima" (em direção a endereços de memória maiores).

  3. Segmento de Dados (Data Segment): Contém variáveis globais e estáticas. É subdividido em:

    • Dados Inicializados: Variáveis globais e estáticas que são inicializadas com um valor não zero.
    • Dados Não Inicializados (BSS - Block Started by Symbol): Variáveis globais e estáticas que são inicializadas com zero por padrão ou não são explicitamente inicializadas.
  4. Segmento de Código (Text Segment): Armazena o código executável do programa e constantes de string (literais). É tipicamente uma região somente leitura.

  5. Segmento de Mapeamento de Memória (Memory-Mapped Segment): Usado para carregar bibliotecas dinâmicas compartilhadas e para mapear arquivos ou dispositivos na memória. Pode ser usado para comunicação entre processos (IPC).

  6. Gerenciamento de Memória Dinâmica em C: malloc, calloc, realloc, free ================================================================================

A linguagem C oferece um conjunto de funções na biblioteca padrão para gerenciar a memória dinâmica na heap:

  • malloc(size_t size): Aloca um bloco de size bytes e retorna um ponteiro void* para o início do bloco. O conteúdo do bloco não é inicializado. Retorna NULL em caso de falha.
  • calloc(size_t num, size_t size): Aloca espaço para num elementos, cada um com size bytes. A memória é inicializada com zeros. Retorna NULL em caso de falha.
  • realloc(void* ptr, size_t new_size): Altera o tamanho do bloco de memória apontado por ptr para new_size. Pode mover o bloco para um novo local se necessário. Retorna um ponteiro para o novo bloco ou NULL em caso de falha.
  • free(void* ptr): Libera o bloco de memória apontado por ptr, que deve ter sido alocado anteriormente por malloc, calloc ou realloc.
  1. Gerenciamento de Memória em C++: Operadores new e delete ===============================================================

Embora as funções de C continuem válidas em C++, a linguagem oferece seus próprios operadores para gerenciamento de memória dinâmica: new e delete. Eles fornecem uma abordagem mais integrada ao sistema de tipos e classes de C++.

3.1. Operadores new e delete para Tipos Primitivos

Para tipos de dados built-in (int, float, etc.), new e delete funcionam de forma semelhante a malloc e free, mas com algumas diferenças importantes (como a forma de tratamento de falhas e a sintaxe).

  • Para um único elemento: tipo* p = new tipo; e delete p;
  • Para arrays: tipo* arr = new tipo[tamanho]; e delete[] arr;

É crucial usar delete para memória alocada com new e delete[] para arrays alocados com new[].

3.2. Operadores new e delete para Tipos Definidos pelo Usuário

A principal vantagem de new e delete para classes é que eles automaticamente chamam o construtor e o destrutor, respectivamente.

#include <iostream>
#include <cstdlib> // Para malloc, free

class Recurso {
public:
    Recurso(int id = 0) : identificador(id) {
        std::cout << "Construtor Recurso(" << identificador << "): " << this << std::endl;
    }
    ~Recurso() {
        std::cout << "Destrutor Recurso(" << identificador << "): " << this << std::endl;
    }
private:
    int identificador;
};

int main() {
    // Alocação estilo C para tipo definido pelo usuário (NÃO CHAMA CONSTRUTOR)
    Recurso* objC = (Recurso*)malloc(sizeof(Recurso));
    std::cout << "--- Memoria alocada via malloc para Recurso ---" << std::endl;
    // objC agora aponta para memoria bruta; sem inicializacao de Recurso

    // Alocação estilo C++ para tipo definido pelo usuário (CHAMA CONSTRUTOR)
    Recurso* objCpp = new Recurso(5);
    std::cout << "--- Memoria alocada via new para Recurso ---" << std::endl;

    free(objC);      // Libera memoria, mas NAO CHAMA DESTRUTOR para objC
    delete objCpp;   // Libera memoria E CHAMA DESTRUTOR para objCpp
    std::cout << "--- Objetos Recurso liberados ---" << std::endl;

    // Comparação para tipos built-in (comportamento similar na alocação)
    int* valC = (int*)malloc(sizeof(int));
    int* valCpp = new int; // Não inicializado; new int() inicializaria com 0
    *valCpp = 100;

    free(valC);
    delete valCpp;
    std::cout << "--- Tipos built-in liberados ---" << std::endl;

    // Arrays de tipos definidos pelo usuário
    // Alocação C-style para array (NÃO CHAMA CONSTRUTORES)
    Recurso* arrC = (Recurso*)malloc(sizeof(Recurso) * 3);
    std::cout << "--- Array via malloc para Recurso[3] ---" << std::endl;

    // Alocação C++-style para array (CHAMA CONSTRUTORES para cada elemento)
    Recurso* arrCpp = new Recurso[3]; // Chama o construtor padrão para 3 objetos
    std::cout << "--- Array via new[] para Recurso[3] ---" << std::endl;

    free(arrC);        // Libera memoria, mas NAO CHAMA DESTRUTORES para arrC
    delete[] arrCpp;   // Libera memoria E CHAMA DESTRUTORES para cada elemento em arrCpp
    std::cout << "--- Arrays Recurso liberados ---" << std::endl;

    return 0;
}

Observação importante: Ao alocar memória para tipos definidos pelo usuário, new invocará o construtor da classe e delete invocará o destrutor. malloc e free apenas alocam e liberam a memória bruta, sem interação com os construtores ou destrutores.

  1. Funções Globais operator new e operator delete =====================================================

É fundamental distinguir entre os operadores new/delete e as funções globais operator new/operator delete. Os operadores (new/delete) são as palavras-chave que o programador usa para gerenciar a memória dinâmica. Em sua implementação, eles invocam as funções globais operator new e operator delete.

  • A função global void* operator new(size_t size) é responsável por alocar a memória bruta. Internamente, ela geralmente utiliza malloc para obter a memória. Se a alocação falhar, ela tenta chamar um manipulador de alocação (se definido) ou lança uma exceção std::bad_alloc.
  • A função global void operator delete(void* ptr) é responsável por liberar a memória bruta. Intrenamente, ela geralmente utiliza free.

Portanto, o operador new realiza duas etapas: primeiro chama operator new para alocar memória e, em seguida, invoca o construtor do objeto. Similarmente, o operador delete chama o destrutor do objeto e, em seguida, operator delete para liberar a memória.

  1. Princípios de Implementação de new e delete ==================================================

5.1. Para Tipos Primitivos (Built-in)

Para tipos primitivos, new e delete são semelhantes a malloc e free, com as seguintes diferenças:

  • new pode inicializar a memória (e.g., new int() inicializa para 0).
  • new lança std::bad_alloc em caso de falha de alocação, enquanto malloc retorna NULL.
  • new[] e delete[] são usados para arrays.

5.2. Para Tipos Definidos pelo Usuário

Princípio do new Tipo:

  1. Chama a função global operator new(sizeof(Tipo)) para alocar a memória bruta necessária para o objeto.
  2. No espaço de memória alocado, invoca o construtor de Tipo para inicializar o objeto.

Princípio do delete ptr:

  1. Invoca o destrutor do objeto apontado por ptr para realizar a limpeza dos recursos.
  2. Chama a função global operator delete(ptr) para liberar a memória bruta.

Princípio do new Tipo[N]:

  1. Chama a função global operator new[](sizeof(Tipo) * N + metadata_size). A metadata_size é um espaço adicional que o compilador pode usar para armazenar o número de elementos do array, necessário para que delete[] saiba quantos destrutores chamar.
  2. No espaço alocado, invoca o construtor de Tipo N vezes, um para cada elemento do array.

Princípio do delete[] ptr:

  1. Para cada um dos N objetos no array, invoca o destrutor de Tipo.

  2. Chama a função global operator delete[](ptr) para liberar a memória bruta alocada para o array.

  3. Expressão new de Posicionamento (Placement New) ====================================================

A expressão placement new permite construir um objeto em um bloco de memória previamente alocado. Não aloca memória nova, mas sim inicializa um objeto em um endereço de memória fornecido.

Sintaxe: new (endereco_alvo) Tipo; ou new (endereco_alvo) Tipo(lista_inicializadores);

O endereco_alvo deve ser um ponteiro para um bloco de memória bruta.

Cenário de Uso: É comumente empregado em implementações de memory pools (gerenciadores de memória personalizados) ou para otimização em cenários onde a alocação de memória é separada da construção do objeto.

#include <iostream>
#include <new>       // Para placement new
#include <cstdlib>   // Para malloc, free

class ObjetoSimples {
public:
    ObjetoSimples(int val = 0) : dado(val) {
        std::cout << "Construtor ObjetoSimples(" << dado << "): " << this << std::endl;
    }
    ~ObjetoSimples() {
        std::cout << "Destrutor ObjetoSimples(" << dado << "): " << this << std::endl;
    }
private:
    int dado;
};

int main() {
    // 1. Alocar um bloco de memória bruta (sem construtores/destrutores)
    void* bufferBruto = malloc(sizeof(ObjetoSimples));
    if (!bufferBruto) {
        std::cerr << "Falha ao alocar memoria bruta." << std::endl;
        return 1;
    }
    std::cout << "Memoria bruta alocada em: " << bufferBruto << std::endl;

    // 2. Usar placement new para construir um objeto no buffer alocado
    // Isso CHAMA o construtor de ObjetoSimples no endereco de bufferBruto
    ObjetoSimples* objPlacado = new (bufferBruto) ObjetoSimples(42);
    std::cout << "Objeto construido via placement new em: " << objPlacado << std::endl;

    // 3. Destruir manualmente o objeto (CHAMA o destrutor)
    // O placement new não gerencia o tempo de vida da memória bruta.
    objPlacado->~ObjetoSimples();
    std::cout << "Destrutor do objeto chamado." << std::endl;

    // 4. Liberar a memória bruta alocada com malloc
    free(bufferBruto);
    std::cout << "Memoria bruta liberada." << std::endl;

    std::cout << "\n--- Exemplo com operator new e delete globais ---" << std::endl;

    // Outro exemplo, usando operator new global para alocar memoria bruta
    void* espacoGlobal = operator new(sizeof(ObjetoSimples));
    std::cout << "Memoria alocada via operator new global em: " << espacoGlobal << std::endl;
    
    ObjetoSimples* outroObj = new (espacoGlobal) ObjetoSimples(99);
    std::cout << "Outro objeto construido via placement new em: " << outroObj << std::endl;

    outroObj->~ObjetoSimples();
    std::cout << "Destrutor do outro objeto chamado." << std::endl;
    
    operator delete(espacoGlobal);
    std::cout << "Memoria liberada via operator delete global." << std::endl;

    return 0;
}

  1. Diferenças entre malloc/free e new/delete ====================================================

Ambos os pares de funções/operadores servem para gerenciar memória dinâmica na heap, mas possuem distinções cruciais:

  1. Natureza: malloc e free são funções da biblioteca C. new e delete são operadores da linguagem C++.
  2. Construção/Destruição: new invoca o construtor de classes ao alocar, e delete invoca o destrutor ao liberar. malloc e free apenas gerenciam a memória bruta e não interagem com construtores/destrutores.
  3. Tipagem: malloc retorna void* e requer um cast explícito para o tipo desejado. new retorna um ponteiro do tipo correto automaticamente, eliminando a necessidade de casts.
  4. Tamanho: malloc exige o tamanho em bytes como argumento (e.g., sizeof(Tipo) * N). new determina o tamanho automaticamente com base no tipo (e.g., new Tipo ou new Tipo[N]).
  5. Inicialização: malloc não inicializa a memória alocada (o conteúdo é lixo). new não inicializa por padrão para tipos primitivos (new int), mas new int() inicializa para zero. Para tipos de classe, o construtor é chamado. calloc é o equivalente a malloc que inicializa com zeros.
  6. Tratamento de Erros: Em caso de falha na alocação, malloc retorna NULL, exigindo verificações explícitas. new lança uma exceção do tipo std::bad_alloc por padrão, que pode ser capturada.
  7. Arrays: new e delete têm versões específicas para arrays (new[] e delete[]) que garantem a chamada correta de construtores/destrutores para cada elemento.

Tags: C++ gerenciamento de memória new delete malloc

Publicado em 8-11 08:18