- 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:
-
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.
-
Heap: Região para alocação dinâmica de memória em tempo de execução. A alocação é feita por funções como
mallocenew, 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). -
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.
-
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.
-
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).
-
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 desizebytes e retorna um ponteirovoid*para o início do bloco. O conteúdo do bloco não é inicializado. RetornaNULLem caso de falha.calloc(size_t num, size_t size): Aloca espaço paranumelementos, cada um comsizebytes. A memória é inicializada com zeros. RetornaNULLem caso de falha.realloc(void* ptr, size_t new_size): Altera o tamanho do bloco de memória apontado porptrparanew_size. Pode mover o bloco para um novo local se necessário. Retorna um ponteiro para o novo bloco ouNULLem caso de falha.free(void* ptr): Libera o bloco de memória apontado porptr, que deve ter sido alocado anteriormente pormalloc,callocourealloc.
- Gerenciamento de Memória em C++: Operadores
newedelete===============================================================
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;edelete p; - Para arrays:
tipo* arr = new tipo[tamanho];edelete[] 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.
- Funções Globais
operator neweoperator 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 utilizamallocpara obter a memória. Se a alocação falhar, ela tenta chamar um manipulador de alocação (se definido) ou lança uma exceçãostd::bad_alloc. - A função global
void operator delete(void* ptr)é responsável por liberar a memória bruta. Intrenamente, ela geralmente utilizafree.
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.
- Princípios de Implementação de
newedelete==================================================
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:
newpode inicializar a memória (e.g.,new int()inicializa para 0).newlançastd::bad_allocem caso de falha de alocação, enquantomallocretornaNULL.new[]edelete[]são usados para arrays.
5.2. Para Tipos Definidos pelo Usuário
Princípio do new Tipo:
- Chama a função global
operator new(sizeof(Tipo))para alocar a memória bruta necessária para o objeto. - No espaço de memória alocado, invoca o construtor de
Tipopara inicializar o objeto.
Princípio do delete ptr:
- Invoca o destrutor do objeto apontado por
ptrpara realizar a limpeza dos recursos. - Chama a função global
operator delete(ptr)para liberar a memória bruta.
Princípio do new Tipo[N]:
- Chama a função global
operator new[](sizeof(Tipo) * N + metadata_size). Ametadata_sizeé um espaço adicional que o compilador pode usar para armazenar o número de elementos do array, necessário para quedelete[]saiba quantos destrutores chamar. - No espaço alocado, invoca o construtor de
TipoNvezes, um para cada elemento do array.
Princípio do delete[] ptr:
-
Para cada um dos
Nobjetos no array, invoca o destrutor deTipo. -
Chama a função global
operator delete[](ptr)para liberar a memória bruta alocada para o array. -
Expressão
newde 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;
}
- Diferenças entre
malloc/freeenew/delete====================================================
Ambos os pares de funções/operadores servem para gerenciar memória dinâmica na heap, mas possuem distinções cruciais:
- Natureza:
mallocefreesão funções da biblioteca C.newedeletesão operadores da linguagem C++. - Construção/Destruição:
newinvoca o construtor de classes ao alocar, edeleteinvoca o destrutor ao liberar.mallocefreeapenas gerenciam a memória bruta e não interagem com construtores/destrutores. - Tipagem:
mallocretornavoid*e requer um cast explícito para o tipo desejado.newretorna um ponteiro do tipo correto automaticamente, eliminando a necessidade de casts. - Tamanho:
mallocexige o tamanho em bytes como argumento (e.g.,sizeof(Tipo) * N).newdetermina o tamanho automaticamente com base no tipo (e.g.,new Tipoounew Tipo[N]). - Inicialização:
mallocnão inicializa a memória alocada (o conteúdo é lixo).newnão inicializa por padrão para tipos primitivos (new int), masnew int()inicializa para zero. Para tipos de classe, o construtor é chamado.callocé o equivalente amallocque inicializa com zeros. - Tratamento de Erros: Em caso de falha na alocação,
mallocretornaNULL, exigindo verificações explícitas.newlança uma exceção do tipostd::bad_allocpor padrão, que pode ser capturada. - Arrays:
newedeletetêm versões específicas para arrays (new[]edelete[]) que garantem a chamada correta de construtores/destrutores para cada elemento.