Apontadores e memória
Endereços, aliases, limites, duração de vida, new e delete, propriedade e RAII.
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.
#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";
}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ção | Pode mudar o endereço? | Pode alterar o int por esse acesso? |
|---|---|---|
int* p | Sim | Sim |
const int* p | Sim | Não |
int* const p | Não | Sim |
const int* const p | Não | Nã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.
#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";
}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.
#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;
}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.
#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";
}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.
O que fica pendente
Uma sequência faz
int* p = new int(8); int* q = p; delete p; p = nullptr;. Qual
afirmação é correta?
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.
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
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.
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.