Apontadores e memória

Endereços, aliases, limites, duração de vida, new e delete, propriedade e RAII.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

A função soma recebe um endereço e um tamanho. O endereço localiza o primeiro inteiro, mas não diz quantos elementos existem nem quem gere a memória.

Para cada acesso, confirma limites e duração de vida. Para cada recurso dinâmico, identifica o dono.

Endereço de e dereferenciação

&a obtém o endereço de a. Numa declaração, int* p define um apontador para int. Numa expressão, *p desreferencia o apontador, ou seja, acede ao objeto apontado. p = &b muda o endereço guardado; *p = 7 muda o objeto nesse endereço.

Dois apontadores para o mesmo objeto
#include <iostream>

int main() {
    int a = 3;
    int b = 8;
    int* p = &a;
    int* q = p;
    *q += 2;
    p = &b;
    *p -= 1;
    std::cout << a << " " << b << " " << *q << "\n";
}
Resultado

Executa o código para ver a saída e os gráficos.

Saída:

5 7 5

No início, p e q apontam para a, por isso *q += 2 altera a de 3 para 5. Depois, p = &b muda apenas a seta de p enquanto q continua em a, e *p -= 1 altera b de 8 para 7.

Os dois apontadores são objetos distintos mesmo quando guardam o mesmo endereço. O diagrama mostra o estado final, depois de ambas as setas terem começado em a.

Dois acessos ao mesmo objeto são aliases. Uma cópia de apontador copia um endereço, não duplica o objeto apontado. Imprimir um endereço pode ajudar a comparar identidades, mas o seu número depende da execução e não serve como saída fixa de um exercício.

Apontadores nulos e const

nullptr representa ausência de objeto. Podes compará-lo e testá-lo; não podes desreferenciá-lo.

int* p; deixa um apontador local sem inicialização. Em C++17, ler esse valor indeterminado ou tentar usá-lo não tem resultado válido em que possas confiar.

DeclaraçãoPode mudar o endereço?Pode alterar o int por esse acesso?
int* pSimSim
const int* pSimNão
int* const pNãoSim
const int* const pNãoNão

Lê cada declaração do * para o nome. Em const int* p, o int é constante por esse acesso: podes mudar a seta, mas não *p. Em int* const p, a seta é fixa: podes alterar *p, mas não p.

const int* p não torna necessariamente constante o objeto. Se o original não é const, outro acesso modificável pode alterá-lo. Retirar const para modificar um objeto originalmente constante tem comportamento indefinido.

Apontadores e arrays

Um array converte-se, em muitas expressões, num apontador para o primeiro elemento. p + i avança i elementos do tipo apontado, não i bytes. p[i] equivale a *(p + i) quando essa operação é válida.

// Excerto com um array vivo de quatro elementos.
int v[] = {10, 20, 30, 40};
int* inicio = v;
int* fim = v + 4;

*(inicio + 2) vale 30. fim é a posição imediatamente depois do array: válida para marcar o fim de um intervalo, mas não é um elemento e não se desreferencia; avançar para além dela também sai dos limites permitidos. Subtrair dois apontadores para elementos do mesmo array, ou para a posição depois do fim, dá a distância em elementos.

A aritmética de apontadores não serve para saltar entre objetos independentes. O intervalo habitual é [inicio, fim): inclui o início e exclui o fim. A página Templates e STL usa esta convenção nos algoritmos.

Campos e nós ligados

Para aceder a um campo através de um apontador, p->valor equivale a (*p).valor. Um nó pode guardar um valor e o endereço do nó seguinte. Os nós não precisam de estar contíguos: seguimos o campo seguinte, não somamos um ao apontador.

Percorrer três nós sem copiar objetos
#include <iostream>

struct No {
    int valor;
    No* seguinte;
};

int main() {
    No terceiro{3, nullptr};
    No segundo{2, &terceiro};
    No primeiro{1, &segundo};
    for (No* atual = &primeiro; atual != nullptr; atual = atual->seguinte)
        std::cout << atual->valor << " ";
    std::cout << "\n";
}
Resultado

Executa o código para ver a saída e os gráficos.

Saída:

1 2 3

O percurso lê 1, segue seguinte até ao segundo e ao terceiro, e para porque o último guarda nullptr; a condição impede a leitura por um apontador nulo.

Os três objetos são locais e mantêm-se vivos durante o percurso. Os campos são observadores: não destroem os nós. Não uses delete nestes endereços, porque os objetos não foram criados com new. Se os nós fossem dinâmicos, a destruição teria de guardar o seguinte antes de libertar o atual:

// Excerto: libertação segura de uma lista dinâmica.
while (primeiro != nullptr) {
    No* seguinte = primeiro->seguinte;
    delete primeiro;
    primeiro = seguinte;
}

Depois de delete primeiro, ler primeiro->seguinte seria um acesso a um objeto que já morreu, com comportamento indefinido; por isso guarda o endereço seguinte enquanto o nó ainda vive, liberta o atual e avança.

Pilha, área livre e duração de vida

Uma variável local normal tem duração automática, ou seja, é destruída ao sair do bloco. Um objeto criado com new tem duração dinâmica e permanece até à destruição explícita.

Pilha e área livre

A implementação costuma guardar objetos automáticos na pilha (stack) e objetos dinâmicos na área livre (heap). A norma descreve apenas durações de armazenamento.

// Código incorreto para análise. Não executar.
int* endereco_invalido() {
    int local = 5;
    return &local;
}

O objeto local morre ao sair da função. O endereço devolvido fica pendente, ou dangling. Não existe uma “saída esperada” para a leitura por esse endereço. Retorna o valor, ou usa um objeto com propriedade e duração adequadas.

Alocação dinâmica com new e delete

Usa reserva dinâmica quando o tamanho só é conhecido durante a execução, como n lido da entrada. Um array local exigiria um tamanho fixo em compilação. new T(...) cria um objeto e devolve o seu endereço. new T[n] cria um array. Usa delete para o primeiro e delete[] para o segundo. A troca tem comportamento indefinido. new normal lança std::bad_alloc se não conseguir reservar memória.

Reserva dinâmica com tamanho validado
#include <iostream>

int main() {
    int n = 0;
    if (!(std::cin >> n) || n < 0 || n > 1000) {
        std::cout << "Tamanho invalido\n";
        return 0;
    }
    int* valores = new int[n]{};
    long long soma = 0;
    for (int i = 0; i < n; ++i) {
        valores[i] = i + 1;
        soma += valores[i];
    }
    std::cout << "Soma: " << soma << "\n";
    delete[] valores;
}
Resultado

Executa o código para ver a saída e os gráficos.

Saída:

Soma: 10

O tamanho é limitado para evitar uma reserva descontrolada e a soma usa long long, que representa 500500, a soma máxima deste exemplo, mesmo num sistema com int de 16 bits. new int[n]{} inicializa os elementos a zero e o ciclo substitui-os por 1, 2, 3 e 4, somando 10. Para n = 0, a soma é 0 e delete[] continua válido, mas o endereço de um array vazio não autoriza qualquer indexação.

Fugas de memória e double-free

Há fuga quando um recurso reservado deixa de ter uma forma válida de ser libertado. Sobrescrever o único apontador dono perde essa possibilidade. Libertar duas vezes o mesmo objeto causa double-free; aceder depois da libertação é use-after-free. Ambos têm comportamento indefinido.

O essencial de RAII e smart pointers

RAII, Resource Acquisition Is Initialization, liga um recurso à duração de vida de um objeto dono. O construtor adquire ou recebe o recurso; o destrutor liberta-o. Ao sair normalmente de um âmbito, ou quando uma exceção sai dele, os donos automáticos são destruídos. Assim, a libertação não depende de repetir delete em cada saída.

std::vector e std::string já gerem a sua memória. std::unique_ptr<T> representa propriedade exclusiva de um objeto dinâmico; não se copia, mas pode transferir a propriedade por movimento. get() fornece um apontador observador, sem transferir propriedade.

Transferir propriedade exclusiva
#include <iostream>
#include <memory>
#include <utility>

int main() {
    auto primeiro = std::make_unique<int>(7);
    int* observador = primeiro.get();
    auto segundo = std::move(primeiro);
    std::cout << std::boolalpha << (primeiro == nullptr) << " "
              << *segundo << " " << *observador << "\n";
}
Resultado

Executa o código para ver a saída e os gráficos.

Saída:

true 7 7

Depois do movimento, primeiro está vazio e segundo é o dono. O inteiro não mudou de endereço e observador ainda é válido. Quando segundo morre, qualquer observador que sobreviva fica pendente.

std::shared_ptr permite vários donos e destrói o objeto quando termina a última propriedade forte. Tem custo de gestão e não resolve ciclos de propriedade fortes. Usa-o quando o modelo realmente exige propriedade partilhada, não para evitar decidir quem é o dono. std::weak_ptr permite observar essa propriedade sem a prolongar.

A seta do observador mantém-se, mas não participa na destruição. Se segundo.reset() destruir o inteiro, o observador fica pendente mesmo que continue a guardar o mesmo endereço.

Exercícios

Mudar um endereço ou um valor

Segue

int a = 2, b = 7; int* p = &a; int* q = p; p = &b; *q = 5;

. Indica os valores finais e os destinos dos dois apontadores.

Pista

Desenha uma seta por apontador, em separado.

Solução

No fim, a = 5 e b = 7. p aponta para b; q continua a apontar para a . Copiar p para q não cria uma ligação que faça as futuras atribuições a p aparecerem em q.

Confere a tua resposta:

  • Indiquei a = 5 e b = 7.
  • Desenhei p para b e q para a.
  • Distingui cópia do endereço de cópia do objeto.

Erros frequentes

Dois apontadores com o mesmo valor inicial continuam a ser objetos independentes.

Voltar à explicação
O que fica pendente

Uma sequência faz int* p = new int(8); int* q = p; delete p; p = nullptr;. Qual afirmação é correta?

A tua resposta

Pista

A libertação destrói o objeto, não os apontadores.

Solução

q conserva o antigo endereço, mas o inteiro já foi destruído. Não se pode ler *q nem repetir delete q. O valor nulo de p não repara q.

  • q está pendente e não pode ser desreferenciado.. O objeto morreu e q conserva o endereço antigo.
  • q passa também a nullptr.. A atribuição só modifica p.
  • O inteiro continua vivo enquanto q existir.. Um apontador cru não prolonga a vida do objeto.
  • delete q é necessário para libertar a cópia.. Não houve cópia do inteiro; seria uma segunda libertação.

Erros frequentes

Um endereço que ainda tem o mesmo número não garante que o objeto continue vivo.

Voltar à explicação
Copiar o texto e o terminador

Escreve char* duplicar(const char* texto). Recebe uma string de C válida, não nula, e devolve uma cópia num novo array criado com new[]. O chamador fica responsável por delete[]. Não alteres a origem. Esta resposta é de autoavaliação; relembra o terminador das strings de C.

Pista
Conta unidades até encontrar o terminador, sem o confundir com texto.
Mais uma pista

Para “Ana”, reserva quatro posições e copia A, n, a e \0. Para o vazio, ainda precisas da posição do terminador.

Solução
char* duplicar(const char* texto) {
    std::size_t n = std::strlen(texto);
    char* copia = new char[n + 1];
    for (std::size_t i = 0; i < n; ++i) copia[i] = texto[i];
    copia[n] = '\0';
    return copia;
}

Inclui <cstddef> e <cstring>. Verifica com char origem[] = “Ana”: depois de copiar, muda copia[0] para ‘E’. A cópia deve ser “Ena” e a origem continuar “Ana”. Uma entrada vazia devolve outro array com apenas \0. Liberta cada cópia com delete[] depois da última utilização.

Confere a tua resposta:

  • Reservei comprimento mais um e copiei o terminador nulo.
  • Obtive uma cópia independente, incluindo para a string vazia.
  • Identifiquei o chamador como dono e usei delete[] uma única vez.

Erros frequentes

Reservar só n posições não deixa espaço para o terminador. Devolver o endereço de um array local deixa um apontador pendente. Copiar só o endereço não cria texto independente. Esta função exige uma origem válida; não aceita nullptr nem um array sem terminador.

Voltar à explicação
Guardar o seguinte antes de libertar

Uma lista dinâmica tem três nós ligados por seguinte. Escreve o ciclo que liberta os três sem perder o resto da lista. Usa o tipo struct No { int valor; No* seguinte; };, com No* atual no primeiro nó e nullptr depois do último. Todos os nós foram criados separadamente com new e não há ciclos. Indica o que corre mal se o delete vier antes da leitura de seguinte.

Pista

Precisas de duas variáveis: a atual e a seguinte.

Solução
while (atual != nullptr) {
    No* seguinte = atual->seguinte;
    delete atual;
    atual = seguinte;
}

O ciclo guarda seguinte = atual->seguinte, liberta atual e avança para seguinte. Se o delete vier primeiro, a leitura de seguinte usa memória já libertada, com comportamento indefinido. Não há um resultado válido que permita continuar a procurar o nó seguinte.

Confere a tua resposta:

  • Guardei o seguinte antes de cada delete.
  • Percorri os três nós sem perder endereços.
  • Expliquei por que delete antes da leitura perde a lista.

Erros frequentes

Não uses delete em nós automáticos. A ordem só se aplica a nós criados com new.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.