
Prof. Paulo Rettore
Universidade Federal de Lavras
GAC124 — Introdução aos Algoritmos
new/delete e conhecer malloc/free.A ideia central: primeiro entendemos onde os dados estão; depois, como procurar e processar esses dados.
Uma variável possui um valor, um tipo, um tamanho e um endereço.
numero = 25
É o dado que o programa utiliza.
int
Define como interpretar os bits.
&numero
Indica onde o valor está guardado.
int numero = 25;
cout << numero << endl; // valor
cout << &numero << endl; // endereço, por exemplo 0x7ffd...
O endereço muda entre execuções. O importante é entender que o computador consegue localizar a variável por esse endereço.
Um ponteiro é uma variável que armazena o endereço de outra variável.
int numero = 20;
int* ptrNumero = №
cout << ptrNumero << endl; // endereço de numero
cout << *ptrNumero << endl; // valor guardado nesse endereço
&Obtém o endereço de uma variável.
*Desreferencia: acessa o valor apontado.
int*Declara um ponteiro para int.
O ponteiro não cria uma cópia de numero. Ele oferece outro caminho até a mesma variável.
int numero = 5;
int* p = №
cout << *p << endl; // 5
*p = 8; // altera numero por indireção
cout << numero << endl; // 8
numero = 5
*p aponta para numero
numero = 8
int* p;Destaca o tipo do ponteiro.
int *p;Destaca o nome da variável.
int* p, q;Apenas p é ponteiro; q é um int.
int *p1, *p2; // os dois são ponteiros
int* p3, p4; // p3 é ponteiro; p4 é inteiro
Uma declaração por linha é uma boa prática quando o código ainda está sendo aprendido.
Em muitas expressões, o nome de um vetor representa o endereço do seu primeiro elemento. O vetor não deixa de ser um vetor: ele apenas pode ser acessado por outro caminho.
int vetor[3] = {10, 20, 30};
int* p = vetor; // equivalente a &vetor[0]
cout << vetor[1] << endl; // 20
cout << p[1] << endl; // 20
cout << *(p + 1) << endl; // 20
p[i] é uma forma conveniente de escrever *(p + i). A aritmética avança considerando o tamanho do tipo apontado.
Passar um ponteiro copia o endereço, mas permite alterar a variável original por indireção.
void dobro(int* valor) {
if (valor != nullptr)
*valor = 2 * (*valor);
}
int n = 7;
dobro(&n);
cout << n; // 14
dobro(&n) envia o endereço de n.
*valor acessa o próprio n.
Quando um ponteiro aponta para uma estrutura, o operador -> acessa diretamente seus campos.
struct Ponto {
int x, y;
};
Ponto p1{3, 4};
Ponto* ptr = &p1;
cout << p1.x << endl;
cout << (*ptr).y << endl;
cout << ptr->x << endl; // forma mais legível
ptr->x é equivalente a (*ptr).x.
int* p = nullptr;
if (p != nullptr) {
cout << *p;
} else {
cout << "Ainda não aponta para um dado";
}
nullptrNão aponta para uma variável válida.
&numeroAponta para o endereço de numero.
*pSó pode ser usado quando p é válido.
Nunca desreferencie um ponteiro não inicializado ou nulo.
int* p;
*p = 10; // comportamento indefinidoint* p;
p = 240; // não faça issoint v[3];
v[3] = 10; // último índice é 2int* p = new int;
delete p;
*p = 10; // ponteiro pendenteEsses erros podem compilar e falhar somente durante a execução. Por isso, compilação bem-sucedida não garante acesso seguro à memória.
int* p = new int(10);
int* copia = p;
delete p;
p = nullptr;
// copia ainda guarda o endereço liberado: ponteiro pendente
Continua guardando um endereço depois de delete.
O programa perde o último endereço de um bloco que ainda não foi liberado.
Ao liberar, invalide os ponteiros que possam ser reutilizados. Evite cópias de ponteiros sem definir quem será responsável pelo delete.
nullptr ou com o endereço de uma variável válida.delete para um objeto e delete[] para um vetor.nullptr ao ponteiro principal.int n;new int[n]Alocação dinâmica oferece controle sobre o momento e o tamanho da reserva, mas exige responsabilidade.
int tam;
cin >> tam;
int vetor[tam]; // parece resolver...
Essa declaração é conhecida como VLA (variable-length array).
Em C++, um vetor comum precisa ter tamanho conhecido em tempo de compilação. Alguns compiladores aceitam VLA como extensão, mas ele não faz parte do C++ padrão.
int tam;
cin >> tam;
if (tam > 0) {
int* vetor = new int[tam];
// use vetor[0] até vetor[tam - 1]
delete[] vetor;
vetor = nullptr;
}
new[] funciona com tamanho conhecido só em execução.
VLA ocuparia a pilha, que tem capacidade limitada.
Com new[], lembre do delete[].
int* ptr = new int;
*ptr = 13;
cout << *ptr << endl;
delete ptr;
ptr = nullptr;
newReserva um bloco e devolve um endereço.
*ptrAcessa o inteiro reservado.
deleteLibera o bloco reservado.
O elemento dinâmico não tem um nome próprio; usamos o ponteiro para encontrá-lo.
int n;
cin >> n;
int* vetor = new int[n];
for (int i = 0; i < n; i++)
cin >> vetor[i];
cout << vetor[0];
delete[] vetor;
vetor = nullptr;
new int[n]Reserva posições contíguas para n inteiros.
delete[] vetorLibera o bloco inteiro, não apenas a primeira posição.
Um vetor criado com new[] não muda de tamanho sozinho. Para crescer, criamos outro bloco e copiamos os elementos.
int* maior = new int[novoTamanho];
for (int i = 0; i < tamanhoAtual; i++)
maior[i] = vetor[i];
delete[] vetor;
vetor = maior;
new/delete e malloc/freenew/deleteint* v = new int[n];
delete[] v;delete ou delete[].malloc/freeint* v = static_cast<int*>(
malloc(n * sizeof(int)));
free(v);free.Não misture pares: memória de new não deve ser liberada com free; memória de malloc não deve ser liberada com delete.
malloc()?#include <cstdlib>
int n;
cin >> n;
int* vetor = static_cast<int*>(
malloc(n * sizeof(int))
);
if (vetor == nullptr) {
cout << "Falha na alocação";
} else {
vetor[0] = 42;
free(vetor);
vetor = nullptr;
}
sizeof(int) informa quantos bytes um inteiro ocupa.malloc retorna um endereço bruto, convertido para int*.free devolve o bloco ao sistema.Prefira new/delete quando o objetivo é estudar alocação dinâmica manual.
Você pode encontrar malloc/free; reconheça o par correto.
Estruturas como std::vector automatizam várias dessas responsabilidades.
Nesta aula o foco é compreender a memória. Em projetos maiores, abstrações que gerenciam recursos reduzem erros.
Dado um vetor V e uma chave K, queremos saber se K aparece e em qual índice.
V = {2, -3, 101, 104, 0, 1, 2, 3, -3, 0}
K = 104
// resposta: encontrado no índice 3
A busca linear funciona em qualquer vetor.
Podemos descartar regiões inteiras.
O formato dos dados orienta o algoritmo.
int buscaLinear(const int v[], int n, int chave) {
for (int i = 0; i < n; i++) {
if (v[i] == chave)
return i;
}
return -1;
}
Vetor: {4, 9, 2, 7, 5, 1} · chave: 7.
Use Próximo para testar cada comparação ou Reproduzir para assistir à sequência.
Se o vetor estiver ordenado, ainda podemos evitar comparações depois que o valor atual ultrapassa a chave.
int i = 0;
while (i < n and v[i] < chave)
i++;
if (i < n and v[i] == chave)
return i;
return -1;
Essa melhoria reduz trabalho em alguns casos, mas o pior caso ainda pode visitar quase todo o vetor.
Vetor ordenado: {1, 4, 5, 10, 16, 19, 20, 27, 30} · chave: 26.
Ao encontrar 27, já sabemos que 26 não aparece depois. A busca pode parar.
A busca binária exige um vetor ordenado e mantém apenas o intervalo onde a chave ainda pode estar.
chave com v[meio].Em cada passo, o tamanho do problema cai aproximadamente pela metade. O custo é proporcional a log₂(n).
Vetor: {1, 4, 5, 10, 16, 19, 20, 27, 30} · chave: 26.
As células apagadas estão fora do intervalo atual. A busca termina quando não sobra posição possível.
Mesmo vetor, agora procurando a chave 27.
Observe a diferença: encontrar o meio encerra a busca imediatamente.
int buscaBinaria(const int v[], int n, int chave) {
int inicio = 0;
int fim = n - 1;
while (inicio <= fim) {
int meio = (inicio + fim) / 2;
if (v[meio] == chave)
return meio;
if (chave < v[meio])
fim = meio - 1;
else
inicio = meio + 1;
}
return -1;
}
inicio, fim e meio descrevem o intervalo que ainda precisa ser investigado.
Recursão é quando uma função chama a si mesma para resolver uma versão menor do mesmo problema.
Uma situação simples que pode ser respondida sem nova chamada.
Reduz o problema e chama a função novamente.
problema grande
→ resolve um problema menor
→ resolve um problema ainda menor
→ caso base
→ retorna os resultados
Sem caso base ou sem redução, a função pode chamar a si mesma indefinidamente.
A matemática já descreve o fatorial de forma recursiva:
0! = 1
n! = n × (n - 1)!
4! = 4 × 3!
4! = 4 × 3 × 2 × 1 × 1 = 24
int fatorial(int n) {
if (n == 0) // caso base
return 1;
return n * fatorial(n - 1); // caso geral
}
fatorial(4) ainda não sabe o resultado de fatorial(3).
A multiplicação fica aguardando na pilha.
Primeiro acompanhamos a descida das chamadas; depois, os retornos resolvem a pilha.
O botão Próximo mostra uma nova chamada ou um retorno. A pilha cresce na descida e diminui na subida.
fatorial(4)return 4 * fatorial(3);A função ainda não retorna 24: ela precisa descobrir o resultado de fatorial(3).
fatorial(4)
fatorial(3)4 * (3 * fatorial(2))A chamada de número 3 é criada antes de qualquer produto ser finalizado.
fatorial(4)
fatorial(3)
fatorial(2)4 * (3 * (2 * fatorial(1)))fatorial(4)
fatorial(3)
fatorial(2)
fatorial(1)4 * (3 * (2 * (1 * fatorial(0))))A próxima chamada será fatorial(0), o caso base.
fatorial(4)
fatorial(3)
fatorial(2)
fatorial(1)
fatorial(0)4 * (3 * (2 * (1 * fatorial(0))))fatorial(0) é o caso base e devolve 1. A chamada será removida na próxima etapa.
fatorial(4)
fatorial(3)
fatorial(2)
fatorial(1)4 * (3 * (2 * (1 * 1)))
= 4 * (3 * (2 * 1))A chamada fatorial(0) saiu da pilha. Seu retorno 1 ocupa o lugar da chamada na expressão.
fatorial(4)
fatorial(3)
fatorial(2)4 * (3 * (2 * 1))
= 4 * (3 * 2)fatorial(1) resolve 1 * 1 = 1, devolve 1 e também é removido da pilha.
fatorial(4)
fatorial(3)4 * (3 * 2)
= 4 * 6fatorial(2) resolve 2 * 1 = 2, devolve 2 e deixa a expressão pronta para o próximo retorno.
fatorial(4)4 * 6
= 24fatorial(3) resolve 3 * 2 = 6, devolve 6 e é removido da pilha.
vazia4 * 6
= 24fatorial(4) resolve 4 * 6 = 24. A pilha ficou vazia e o resultado pode ser enviado ao cout.
fatorial(4)
└─ fatorial(3)
└─ fatorial(2)
└─ fatorial(1)
└─ fatorial(0)Na descida, cada chamada aguarda.
Na subida, cada chamada combina o resultado recebido com seu próprio valor.
Em problemas com mais de uma chamada recursiva, como Fibonacci, a estrutura deixa de ser uma corrente e vira uma árvore de chamadas.
int f(int n) {
return f(n - 1);
}Chamadas infinitas.
int f(int n) {
if (n == 0) return 1;
return f(n);
}O parâmetro nunca se aproxima do caso base.
Outros problemas: caso base incorreto, retorno errado ou profundidade tão grande que esgota a pilha de execução.
int fatorial(int n) {
if (n == 0) return 1;
return n * fatorial(n - 1);
}Expressa diretamente a relação matemática.
int resultado = 1;
for (int i = 2; i <= n; i++)
resultado *= i;Usa uma única função e uma repetição.
A versão recursiva pode ser mais didática para alguns problemas, mas normalmente usa mais memória para guardar chamadas pendentes.
int buscaBinaria(const int v[], int inicio, int fim, int chave) {
if (inicio > fim)
return -1; // intervalo vazio
int meio = (inicio + fim) / 2;
if (v[meio] == chave)
return meio;
if (chave < v[meio])
return buscaBinaria(v, inicio, meio - 1, chave);
return buscaBinaria(v, meio + 1, fim, chave);
}
Encontrou a chave ou o intervalo ficou vazio.
Chama a função para a metade possível.
Vetor: {1, 4, 5, 10, 16, 19, 20, 27, 30} · chave: 27.
busca(0, 8)
→ busca(5, 8)
→ busca(7, 8)
→ encontra em 7meio = 4 → 16
meio = 6 → 20
meio = 7 → 27Cada chamada reduz [inicio, fim]; o retorno devolve o mesmo índice até a chamada inicial.
Para conferir uma busca binária recursiva, registre uma linha por chamada.
| Chamada | inicio | fim | meio | v[meio] | decisão |
|---|---|---|---|---|---|
| 1 | 0 | 8 | 4 | 16 | direita |
| 2 | 5 | 8 | 6 | 20 | direita |
| 3 | 7 | 8 | 7 | 27 | encontrou |
O teste de mesa transforma o movimento do algoritmo em evidências: valores de entrada, decisão e próximo intervalo.
n inteiros.inicio, fim e meio.delete[].Teste três situações: chave no primeiro trecho, chave ausente e chave no último índice.
Use estas fontes para consultas pontuais depois da aula:
Os materiais completos, o Campus Virtual e o DREDD continuam sendo as fontes principais da disciplina.
& obtém endereço e * acessa o valor.new[]/delete[] formam um par.malloc/free formam outro par e não devem ser misturados com new/delete.PRÓXIMA AULA
Prof. Paulo Rettore
GAC124 — Introdução aos Algoritmos