Ponteiros e Alocação Dinâmica12.1
Nas aulas anteriores, aprendemos a usar variáveis para armazenar dados, funções para organizar a lógica e struct para modelar entidades como Aluno. Vimos que algumas funções conseguiam modificar os dados originais enquanto outras não, e usamos símbolos como &, * e -> sem desmontar completamente a lógica por trás deles. Também notamos que o vetor da turma tinha tamanho fixo, Aluno turma[100], mesmo que o número real de alunos só aparecesse durante a execução.
Esta aula resolve essas duas questões de uma vez. Primeiro, vamos entender o que é um endereço de memória, o que é um ponteiro e por que &, * e -> aparecem no código. Depois, vamos usar esses mesmos ponteiros para criar e redimensionar dados dinamicamente com malloc, calloc, realloc e free.
Ponteiros localizam dados na memória. Alocação dinâmica cria esses dados durante a execução. As duas ideias andam juntas: sem ponteiros não há alocação dinâmica, e sem alocação dinâmica os ponteiros só apontam para variáveis que já existem.
O ponto de partida: variáveis vivem na memória12.2
Quando declaramos uma variável em C, o programa reserva um espaço na memória para armazenar o seu conteúdo. Esse espaço não é apenas uma ideia abstrata. Ele ocupa uma posição concreta, e essa posição tem um endereço.
Veja este exemplo:
int idade = 20;
printf("Valor: %d\n", idade);
printf("Endereco: %p\n", (void*)&idade);
O primeiro printf exibe o valor guardado na variável. O segundo exibe o endereço onde esse valor está armazenado.
No codigo: idade
|
v
Na memoria: endereco 0x100, conteudo 20
Leia esse diagrama assim:
- no código, usamos o nome
idade - na memória, essa variável ocupa um lugar
- o valor guardado nesse lugar é
20 - o endereço desse lugar pode ser obtido com
&idade
O ponto central aqui é este:
idaderepresenta o valor da variável&idaderepresenta o endereço da variável
Essas duas coisas estão relacionadas, mas não são a mesma coisa.
Valores como 0x100 aparecem aqui apenas como exemplos ilustrativos. Em um programa real, os endereços exatos dependem da execução e do ambiente.
Um ponteiro é uma variável que guarda endereço12.3
Agora podemos introduzir a ideia principal. Um ponteiro é uma variável cujo conteúdo não é um número comum do problema, mas um endereço de memória.
Exemplo:
int idade = 20;
int *p = &idade;
Leia essa declaração assim:
idadeguarda o valor20&idadeproduz o endereço deidadepguarda esse endereço- portanto,
paponta paraidade
Esse diagrama ajuda a evitar uma confusão muito comum: p não guarda 20. Quem guarda 20 é idade. O que p guarda é o endereço de idade.
O tipo do ponteiro importa. Se escrevemos:
int *p;
isso significa que p aponta para um int. Da mesma forma:
float *q;
Aluno *a;
char *texto;
significam, respectivamente, ponteiro para float, ponteiro para Aluno e ponteiro para char. O tipo informa o que esperamos encontrar no endereço apontado e quantos bytes devem ser lidos ou escritos a partir dali.
O operador * também serve para acessar o conteúdo apontado12.4
O símbolo * aparece em dois contextos diferentes nesta aula:
- na declaração, ele faz parte do tipo do ponteiro
- em uma expressão, ele significa acesso ao conteúdo apontado
Veja:
int idade = 20;
int *p = &idade;
printf("Endereco guardado em p: %p\n", (void*)p);
printf("Conteudo apontado por p: %d\n", *p);
Leia assim:
pé o endereço guardado no ponteiro*pé o valor encontrado nesse endereço
Por isso, se fizermos:
*p = 35;
o valor de idade passa a ser 35, porque p aponta justamente para a posição onde idade está armazenada.
int idade = 20;
int *p = &idade;
*p = 35;
printf("%d\n", idade);
Saída:
35
p e *p não são a mesma coisa. p é endereço. *p é o conteúdo encontrado nesse endereço.
printf() e scanf() ajudam a enxergar a memória12.5
Desde o início da disciplina, você já usa printf() e scanf(). Agora podemos usá-los para entender melhor o que acontece com os dados na memória.
printf() pode mostrar valor e endereço12.5.1
int idade = 20;
printf("Valor: %d\n", idade);
printf("Endereco: %p\n", (void*)&idade);
O primeiro printf() lê o conteúdo da variável para exibir. O segundo mostra a localização da variável na memória.
scanf() precisa receber um endereço12.5.2
Agora observe:
int idade;
scanf("%d", &idade);
scanf() não foi chamado para apenas observar a variável. Ele precisa escrever nela. Para fazer isso, precisa saber onde essa variável está.
Esse diagrama revela o motivo real do & em scanf():
- o usuário digita um valor
scanf()interpreta esse valor- o programa precisa saber onde gravá-lo
&idadeentrega o endereço correto- a escrita ocorre na posição correspondente à variável
Portanto:
scanf("%d", &idade); // correto: entrega o endereco
scanf("%d", idade); // errado: idade e um valor int, nao um endereco
Passagem por valor e passagem por endereço12.6
Na aula de funções, vimos que parâmetros são variáveis locais. Isso significa que, quando passamos um valor comum para uma função, o que a função recebe é uma cópia.
Veja:
void tentar_alterar(int x){
x = 100;
}
int main(void){
int n = 10;
tentar_alterar(n);
printf("%d\n", n);
return 0;
}
A saída continua sendo 10, porque a função alterou apenas a sua cópia local.
Agora compare com uma função que recebe endereço:
void alterar(int *p){
*p = 100;
}
int main(void){
int n = 10;
alterar(&n);
printf("%d\n", n);
return 0;
}
Nesse caso, a saída passa a ser 100.
O que mudou foi isto:
- antes, a função recebia apenas uma cópia do valor
- agora, ela recebe o endereço da variável original
- usando
*p, ela acessa exatamente o conteúdo dessa variável
Foi exatamente isso que aconteceu na aula 11 quando usamos funções como:
void aplicar_bonus(Aluno *a, float bonus);
Essa função não recebia uma cópia inteira do aluno. Ela recebia o endereço do aluno real, e por isso conseguia modificar a estrutura armazenada no vetor da turma.
Ponteiros para struct e o operador ->12.7
Na aula anterior, você viu dois tipos de acesso a campos de estrutura:
aluno.matricula
e
a->matricula
Agora a diferença pode ser explicada com precisão.
Quando temos a estrutura diretamente, usamos .:
Aluno aluno;
aluno.matricula = 1001;
Quando temos um ponteiro para a estrutura, usamos ->:
Aluno aluno;
Aluno *a = &aluno;
a->matricula = 1001;
Leia esse diagrama assim:
aguarda o endereço do aluno- o programa usa esse endereço para localizar a estrutura
- depois disso, acessa o campo desejado
Portanto:
aluno.faltasacessa o campo quando temos a estrutura diretamentea->faltasacessa o campo quando temos o endereço dessa estrutura
Se quisermos, podemos escrever também:
(*a).faltas
que significa a mesma coisa que:
a->faltas
Na prática, usamos -> porque a leitura fica mais natural.
NULL significa ausência de endereço válido12.8
Nem todo ponteiro precisa apontar para uma variável válida o tempo todo. Às vezes, o programa precisa representar justamente o contrário: a ideia de que nenhum endereço útil foi encontrado.
É para isso que usamos NULL.
int *p = NULL;
Esse comando significa que p existe como variável, mas no momento não aponta para nenhum objeto válido do programa.
Isso aparece naturalmente em funções de busca. Lembre da aula 11:
Aluno *a = buscar_aluno_por_matricula(turma, n, matricula);
if(a != NULL){
aplicar_bonus(a, bonus);
}
Esse diagrama mostra dois cenários possíveis:
- a busca encontrou um aluno e devolveu um endereço válido
- a busca não encontrou ninguém e devolveu
NULL
É por isso que o teste vem antes do uso. Se a == NULL, não faz sentido tentar acessar a->faltas, porque não existe uma estrutura válida naquele caminho.
Arrays e endereços: uma ponte com o que já vimos12.9
Ponteiros também aparecem o tempo todo quando trabalhamos com vetores. Considere:
int v[3] = {10, 20, 30};
O vetor ocupa posições consecutivas de memória. O nome v, em muitos contextos, se comporta como o endereço do primeiro elemento.
printf("%d\n", v[0]);
printf("%d\n", *v);
Nessa situação, v[0] e *v acessam o mesmo primeiro elemento.
Nesse diagrama, o nome v está ligado ao primeiro elemento do vetor. Por isso, *v acessa o conteúdo armazenado em v[0]. Ao mesmo tempo, o desenho deixa claro que o vetor não é um valor único: ele é uma sequência de posições vizinhas de memória.
Essa ideia ajuda a reler vários trechos já usados na disciplina. Por exemplo, na aula 11:
Aluno turma[100];
ler_aluno(&turma[i]);
Aqui, turma[i] é o aluno da posição i, enquanto &turma[i] é o endereço desse aluno. Como ler_aluno() recebe Aluno *, o que precisamos entregar para a função é justamente o endereço do elemento correto.
Quando o programa percorre a turma com um for, ele está apenas repetindo contas ou está caminhando por várias posições de memória que guardam alunos distintos?
O problema do tamanho fixo12.10
Agora que entendemos ponteiros, temos a ferramenta para localizar e modificar dados na memória. Mas ainda existe uma limitação importante: todos os vetores que usamos até agora tinham tamanho fixo, decidido antes da execução.
Na aula 11, nosso sistema de boletim armazenava os alunos assim:
Aluno turma[100];
Esse vetor sempre ocupa espaço para 100 alunos, mesmo que a turma tenha apenas 3. Se a turma tiver 150 alunos, o programa simplesmente não funciona, ou pior, funciona mas corrompe memória porque o vetor não tem espaço suficiente.
O incômodo real não é o número 100. É que o número exato de alunos só aparece na primeira linha da entrada, depois que o programa já está rodando:
3
Ana 1001 8.0 7.0 9.0 2
Bruno 1002 5.0 6.0 4.0 5
Caio 1003 3.0 4.0 5.0 1
O programa lê n = 3, mas o vetor já foi declarado com 100 posições antes mesmo de main começar. Em outras palavras, o tamanho do vetor foi decidido em tempo de compilação, enquanto o tamanho real do problema só aparece em tempo de execução.
int n;
scanf("%d", &n);
Aluno turma[n]; // isso nem sempre funciona em C
Em C, declarar um vetor com tamanho vindo de uma variável como Aluno turma[n] funciona em alguns compiladores (os que suportam VLA, Variable Length Array), mas não é portátil. Além disso, ainda não resolve o segundo problema: e se o programa, depois de ler a turma, precisar acrescentar mais um aluno? Um vetor declarado em tempo de compilação não cresce.
Se o tamanho da turma fosse n = 10000, faria sentido o programa ocupar memória para 10000 alunos desde o início, mesmo que a maior parte das execuções use apenas 3 ou 4?
Duas regiões de memória12.11
Até aqui, todas as variáveis que criamos foram alocadas automaticamente. Quando a execução entra em um bloco de código, as variáveis daquele bloco ganham espaço. Quando a execução sai do bloco, esse espaço é automaticamente devolvido.
Essa região da memória onde as variáveis locais e os parâmetros vivem é chamada de pilha (stack). Toda variável declarada da forma que usamos até agora, como int x;, float v[10]; e Aluno a;, fica na pilha.
A pilha é rápida e automática, mas tem uma limitação importante: o tamanho de cada variável precisa ser conhecido em tempo de compilação. É por isso que Aluno turma[100] funciona, mas Aluno turma[n] não funciona em todos os compiladores.
Existe outra região da memória chamada heap (monte). Diferentemente da pilha, o heap não é gerenciado automaticamente pela entrada e saída de funções. É o programador quem decide quando pedir memória ao heap e quando devolvê-la. Essa é a base da alocação dinâmica.
| Pilha (stack) | Heap (monte) |
|---|---|
| Gerenciada automaticamente | Gerenciada pelo programador |
| Tamanho decidido em compilação | Tamanho decidido em execução |
| Variáveis locais e parâmetros | Blocos criados com malloc |
| Liberada ao sair do escopo | Liberada com free |
| Rápida e limitada | Flexível, mas exige cuidado |
Dinâmica significa que a quantidade de memória pode ser decidida durante a execução, reagindo ao que o programa encontra nos dados de entrada, em vez de ser fixada antes da compilação.
malloc(): pedindo memória durante a execução12.12
A função malloc() (memory allocate) pede um bloco de memória ao heap e devolve o endereço desse bloco. Ela está declarada em <stdlib.h>.
O protótipo é este:
void *malloc(size_t tamanho);
A função recebe a quantidade de bytes desejada e retorna um ponteiro do tipo void *. Em C, void * é um ponteiro genérico que pode ser atribuído a qualquer tipo de ponteiro sem conversão explícita.
Alocando um elemento12.12.1
O uso mais simples de malloc é pedir espaço para uma única variável:
int *p = malloc(sizeof(int));
Vamos ler esse comando parte por parte:
sizeof(int)calcula quantos bytes umintocupa (normalmente 4)malloc(sizeof(int))pede exatamente esse número de bytes ao heapmallocdevolve o endereço do bloco alocadopguarda esse endereço
Depois de alocado, podemos usar *p normalmente:
int *p = malloc(sizeof(int));
*p = 42;
printf("%d\n", *p); // imprime 42
O que mudou em relação ao que fazíamos antes? Antes, faríamos:
int x = 42;
int *p = &x;
Agora, a variável x nem precisa existir. O espaço foi obtido diretamente do heap, e p aponta para ele. Se p sair de escopo sem que a memória tenha sido liberada, o bloco continua existindo no heap, e isso é tanto uma vantagem (a memória sobrevive ao fim da função) quanto um perigo (se ninguém mais tiver o endereço, o bloco fica permanentemente ocupado).
Alocando um vetor12.12.2
Pedir memória para vários elementos é igualmente simples. Basta multiplicar o tamanho de um elemento pela quantidade desejada:
int n;
scanf("%d", &n);
int *v = malloc(n * sizeof(int));
Agora o programa lê n do teclado e aloca exatamente o espaço necessário para n inteiros. Se n = 3, o heap recebe um pedido de 3 * sizeof(int) bytes. Se n = 10000, recebe um pedido de 10000 * sizeof(int) bytes.
Observe que, depois de alocar, podemos usar v exatamente como um vetor comum. A notação com colchetes continua funcionando:
for(i = 0; i < n; i++){
v[i] = i * 10;
}
for(i = 0; i < n; i++){
printf("%d ", v[i]);
}
A diferença é que agora n veio da entrada. Se o usuário digitar 5, o heap terá 5 posições. Se digitar 50000, terá 50000 posições. Em nenhum momento o programa precisou decidir esse número antes de compilar.
sizeof não é uma função executadasizeof é resolvido em tempo de compilação. Ele não mede o tamanho de um bloco alocado dinamicamente, e sim o tamanho do tipo da expressão. sizeof(v) onde v é int * devolve o tamanho do ponteiro, não do bloco apontado.
Tratando falhas de alocação12.12.3
malloc pode falhar. Se o heap não tiver memória suficiente para atender ao pedido, a função retorna NULL. Um programa correto deve verificar isso:
int *v = malloc(n * sizeof(int));
if(v == NULL){
printf("Erro: memoria insuficiente.\n");
return 1;
}
Ignorar essa verificação significa arriscar acessar *v ou v[i] quando v vale NULL, o que provoca comportamento indefinido e tipicamente encerra o programa de forma abrupta.
A turma dinâmica12.12.4
Agora podemos voltar ao sistema de alunos da aula 11 e substituir o vetor fixo por um vetor dinâmico:
int n;
scanf("%d", &n);
Aluno *turma = malloc(n * sizeof(Aluno));
if(turma == NULL){
printf("Erro ao alocar turma.\n");
return 1;
}
for(i = 0; i < n; i++){
ler_aluno(&turma[i]);
}
Compare com o que tínhamos antes:
| Antes (fixo) | Depois (dinâmico) |
|---|---|
Aluno turma[100]; |
Aluno *turma = malloc(n * sizeof(Aluno)); |
| Sempre 100 posições | Exatamente n posições |
n não pode passar de 100 |
n limitado apenas pela memória disponível |
Não precisa de free |
Precisa de free quando terminar |
O programa continua funcionando da mesma forma. turma[i] acessa o aluno da posição i, &turma[i] entrega o endereço desse aluno, e todas as funções da biblioteca da aula 11 continuam compatíveis.
Se turma agora é um ponteiro, por que turma[i] continua funcionando como se fosse um vetor comum?
calloc(): alocando e zerando12.13
malloc entrega um bloco de memória sem alterar o conteúdo que já existia nele. Isso significa que, logo após a alocação, os valores dentro do bloco são imprevisíveis, ou seja, são "lixo" de memória.
Se quisermos que todas as posições comecem zeradas, usamos calloc (clear allocation):
int *v = calloc(n, sizeof(int));
A diferença em relação a malloc está nos argumentos e no comportamento:
| Função | Argumentos | Inicialização |
|---|---|---|
malloc(n * sizeof(int)) |
Total de bytes | Não limpa (lixo) |
calloc(n, sizeof(int)) |
Quantidade separada do tamanho de cada | Zera tudo |
int *v1 = malloc(5 * sizeof(int)); // conteudo: ?, ?, ?, ?, ?
int *v2 = calloc(5, sizeof(int)); // conteudo: 0, 0, 0, 0, 0
Para a maioria dos programas que usam alocação dinâmica, começar com valores zerados é mais seguro e conveniente, especialmente em vetores que serão preenchidos parcialmente ou usados como acumuladores. A contrapartida é que calloc pode ser ligeiramente mais lento que malloc, porque precisa percorrer o bloco inteiro escrevendo zeros.
mallocEm alguns compiladores e situações específicas de depuração, malloc pode acabar entregando memória zerada por coincidência. Mas isso não é garantido pelo padrão da linguagem. Contar com esse comportamento é um erro.
realloc(): redimensionando um bloco que já existe12.14
Até aqui, resolvemos o problema de saber o tamanho exato durante a execução. Mas ainda existe outro problema: e se o tamanho mudar depois que o bloco já foi alocado?
Imagine que o sistema de boletim, depois de ler a turma, precise acrescentar mais um aluno porque uma nova matrícula chegou. A função que resolve isso é realloc (reallocate):
void *realloc(void *bloco, size_t novo_tamanho);
realloc recebe um bloco previamente alocado e o novo tamanho desejado em bytes. Ela tenta redimensionar o bloco no próprio local. Se não for possível (porque a memória vizinha já está ocupada), realloc aloca um novo bloco maior, copia os dados antigos para ele, libera o bloco antigo e devolve o endereço do novo.
int *v = malloc(3 * sizeof(int));
v[0] = 10;
v[1] = 20;
v[2] = 30;
// Agora preciso de 5 posicoes
int *temp = realloc(v, 5 * sizeof(int));
if(temp != NULL){
v = temp;
v[3] = 40;
v[4] = 50;
}
Alguns cuidados importantes com realloc:
- O conteúdo original até o mínimo entre o tamanho antigo e o novo é preservado
- Se o novo tamanho for maior, os bytes adicionais não são inicializados (comportamento igual ao
malloc) - Se
reallocfalhar, retornaNULL, mas o bloco original continua válido. Por isso, nunca façap = realloc(p, ...)diretamente sem um ponteiro auxiliar
int *temp = realloc(v, novo_n * sizeof(int));
if(temp == NULL){
printf("Erro ao redimensionar.\n");
// v ainda e valido; podemos continuar com o tamanho antigo
}
else {
v = temp;
}
Se o primeiro argumento de realloc for NULL, a função se comporta exatamente como malloc. Se o novo tamanho for 0, o comportamento é dependente de implementação.
Acrescentando um aluno à turma12.14.1
No nosso sistema de boletim, podemos usar realloc para adicionar um novo aluno:
void turma_adicionar(Aluno **turma, int *n, Aluno aluno){
Aluno *temp = realloc(*turma, (*n + 1) * sizeof(Aluno));
if(temp == NULL){
printf("Erro ao expandir turma.\n");
return;
}
*turma = temp;
(*turma)[*n] = aluno;
(*n)++;
}
Repare no duplo ponteiro Aluno **turma. A função precisa modificar o ponteiro que a main está usando, e para isso ela recebe o endereço desse ponteiro, exatamente a mesma lógica de passagem por endereço que estudamos na primeira metade desta aula. Para alterar um Aluno *, passamos Aluno **. É a mesma lógica de scanf("%d", &idade): para modificar idade, passamos &idade.
free(): devolvendo memória ao sistema12.15
Toda memória alocada com malloc, calloc ou realloc precisa ser explicitamente devolvida com free. Enquanto a pilha é gerenciada automaticamente, o heap depende do programador.
int *v = malloc(100 * sizeof(int));
// usa o vetor...
free(v);
Depois de free(v), o bloco volta a pertencer ao sistema. Qualquer acesso a v[i] depois disso é comportamento indefinido. O ponteiro v continua guardando o endereço antigo, mas esse endereço não é mais válido.
Depois de free(v), o ponteiro v ainda contém o endereço do bloco liberado, mas esse endereço não deve mais ser usado. Acessar *v nesse ponto é tão errado quanto acessar *p quando p == NULL. Uma prática defensiva comum é fazer v = NULL; logo após free(v);, porque free(NULL) é garantidamente seguro.
Vazamento de memória12.15.1
Memória alocada e não liberada continua ocupada até o fim do programa. Se o programa aloca repetidamente sem liberar, o consumo de memória cresce sem parar. Esse problema é chamado de vazamento de memória (memory leak).
// ERRO: vazamento de memoria
for(i = 0; i < 1000; i++){
int *v = malloc(1000000 * sizeof(int));
// usa v...
// esqueceu de chamar free(v)
}
Nesse exemplo, a cada iteração malloc reserva um bloco novo e o endereço antigo (guardado em v) é perdido. Como ninguém mais sabe onde o bloco antigo está, ele nunca será liberado. Em menos de um segundo, o programa pode consumir gigabytes de memória desnecessariamente.
Em programas pequenos que terminam rápido, o sistema operacional recupera toda a memória quando o processo encerra. Mas em programas que rodam por horas ou dias (servidores, editores, navegadores), vazamentos de memória se acumulam e degradam o sistema.
Toda chamada de malloc, calloc ou realloc bem-sucedida deve ter uma chamada de free correspondente. Se o programa perde o endereço de um bloco antes de liberá-lo, essa memória fica permanentemente inacessível.
O sistema de alunos completo com alocação dinâmica12.16
Agora podemos reescrever o sistema de boletim da aula 11 usando alocação dinâmica em vez de vetor fixo. A mudança afeta principalmente a main, enquanto as funções da biblioteca aluno.h / aluno.c continuam praticamente as mesmas.
Entrada12.16.1
A entrada continua igual:
3
Ana 1001 8.0 7.0 9.0 2
Bruno 1002 5.0 6.0 4.0 5
Caio 1003 3.0 4.0 5.0 1
2
1001 1.0
9999 2.0
A primeira linha traz n. Depois vêm n alunos. Depois um inteiro m com a quantidade de operações de bônus e os pares de matrícula com valor.
Programa principal12.16.2
#include <stdio.h>
#include <stdlib.h>
#include "aluno.h"
int main(void){
int n, i;
scanf("%d", &n);
Aluno *turma = malloc(n * sizeof(Aluno));
if(turma == NULL){
printf("Erro ao alocar turma.\n");
return 1;
}
for(i = 0; i < n; i++){
ler_aluno(&turma[i]);
}
int m;
scanf("%d", &m);
for(i = 0; i < m; i++){
int matricula;
float bonus;
scanf("%d %f", &matricula, &bonus);
Aluno *a = buscar_aluno_por_matricula(turma, n, matricula);
if(a != NULL){
aplicar_bonus(a, bonus);
}
}
imprimir_relatorio(turma, n);
free(turma);
return 0;
}
Compare com a versão anterior:
Aluno turma[100]virouAluno *turma = malloc(n * sizeof(Aluno))- O tamanho
nagora pode ser qualquer valor inteiro positivo - O programa verifica se
mallocfuncionou - No final,
free(turma)devolve a memória
O restante do código, as funções em aluno.h e aluno.c, não mudou absolutamente nada. buscar_aluno_por_matricula, aplicar_bonus, aluno_media, imprimir_relatorio continuam recebendo um ponteiro Aluno *turma e um inteiro n, exatamente como antes. Isso mostra uma qualidade importante dos ponteiros: funções que recebem Aluno * não se importam se o vetor veio da pilha ou do heap.
E se a turma crescer durante a execução12.16.3
Agora suponha que, além das operações de bônus, a entrada também possa conter novos alunos para acrescentar à turma. Podemos tratar isso com realloc:
Aluno *turma = malloc(n * sizeof(Aluno));
// ... preenche a turma ...
// Mais tarde, precisa acrescentar um aluno
Aluno novo;
ler_aluno(&novo);
Aluno *temp = realloc(turma, (n + 1) * sizeof(Aluno));
if(temp == NULL){
printf("Erro ao expandir.\n");
}
else {
turma = temp;
turma[n] = novo;
n++;
}
Antes, com Aluno turma[100], isso simplesmente não era possível. O vetor fixo não cresce, e a única saída era definir um limite arbitrário alto o suficiente para "nunca estourar", o que é uma aposta, não uma solução.
O ciclo completo12.17
As últimas aulas formam uma progressão que vale a pena enxergar como um todo:
| Aula | Problema central | Ferramenta |
|---|---|---|
| 11. Tipos Definidos pelo Programador | Dados soltos, sem modelo | struct, typedef, enum |
| 12. Ponteiros e Alocação Dinâmica | Localizar, modificar e criar dados | &, *, ->, malloc, free |
A aula 11 nos deu estruturas para modelar entidades como Aluno. Esta aula nos deu ponteiros para localizar e modificar essas estruturas na memória e alocação dinâmica para criar e redimensionar esses dados durante a execução.
Juntas, essas ideias formam a base para programas em C que não apenas funcionam, mas que se adaptam aos dados que encontram.
Questões12.18
1. No trecho int idade = 20;, explique a diferença entre idade e &idade.
2. O que significa dizer que um ponteiro é uma variável que armazena endereços? Dê um exemplo.
3. Em int *p = &idade;, o que está guardado em p? E em *p? Explique por que não são a mesma coisa.
4. Se int *p = &idade; e depois executamos *p = 50;, qual será o novo valor de idade? Justifique.
5. Por que scanf("%d", &idade) está correto, mas scanf("%d", idade) está errado?
6. Explique a diferença entre tentar_alterar(int x) e alterar(int *p) em relação à modificação da variável original.
7. Qual é a diferença entre aluno.faltas e a->faltas? Em que situação cada um é usado?
8. O que NULL comunica quando uma função de busca o retorna? Por que devemos testar if(a != NULL) antes de usar a->faltas?
9. Explique a diferença entre declarar int v[10] e usar int *v = malloc(10 * sizeof(int)).
10. Por que precisamos multiplicar por sizeof(Aluno) ao chamar malloc para alocar a turma? O que acontece se escrevermos apenas malloc(n)?
11. Qual é a diferença entre malloc e calloc? Quando calloc é mais conveniente?
12. O que realloc faz quando não consegue expandir o bloco no mesmo local? Por que devemos usar um ponteiro auxiliar em vez de fazer v = realloc(v, ...)?
13. O que é um vazamento de memória? Escreva um exemplo pequeno que provoca esse problema.
14. No sistema de boletim, por que free(turma) aparece apenas uma vez no final da main, e não dentro de funções como buscar_aluno_por_matricula ou aplicar_bonus?
15. No código da função turma_adicionar, explique por que o parâmetro turma é declarado como Aluno **turma (ponteiro duplo) em vez de Aluno *turma.
1. idade representa o valor guardado na variável (o número 20). &idade representa o endereço onde esse valor está armazenado na memória (algo como 0x100). Um é o conteúdo, o outro é a localização.
2. Significa que o conteúdo do ponteiro é uma localização de memória, não um valor comum como idade ou matrícula. Exemplo: int *p = &idade; faz p guardar o endereço de idade, não o valor 20.
3. Em p fica guardado o endereço de idade. *p é o valor encontrado nesse endereço, ou seja, 20. p e *p não são a mesma coisa: um é o endereço, o outro é o conteúdo localizado por esse endereço.
4. O novo valor de idade será 50, porque p aponta para a posição de idade na memória. *p = 50 escreve 50 diretamente nessa posição, alterando a variável original.
5. scanf() precisa de um endereço para saber onde gravar o valor lido do teclado. &idade fornece esse endereço. idade sozinho representa apenas um valor inteiro, não uma localização.
6. tentar_alterar(int x) recebe uma cópia do valor de n; alterar x não afeta n. alterar(int *p) recebe o endereço de n; usar *p modifica diretamente a variável original.
7. aluno.faltas acessa o campo faltas quando temos a estrutura diretamente (variável do tipo Aluno). a->faltas acessa o mesmo campo quando temos um ponteiro para a estrutura (variável do tipo Aluno *).
8. NULL comunica que nenhum endereço válido foi encontrado. O teste if(a != NULL) é necessário porque, se a == NULL, tentar acessar a->faltas tentaria acessar memória em um endereço inválido, causando falha de segmentação.
9. int v[10] aloca o vetor na pilha com tamanho fixo decidido em tempo de compilação, e a memória é liberada automaticamente ao sair do escopo. int *v = malloc(10 * sizeof(int)) aloca no heap, o tamanho pode vir de uma variável, e a memória só é liberada com free.
10. malloc recebe a quantidade de bytes, não de elementos. sizeof(Aluno) informa quantos bytes cada Aluno ocupa. Multiplicando por n, obtemos o total de bytes para n alunos. malloc(n), sem sizeof, alocaria apenas n bytes, insuficientes para armazenar os alunos.
11. malloc entrega o bloco sem limpar o conteúdo (valores imprevisíveis). calloc entrega o bloco com todos os bytes zerados. calloc é mais conveniente quando o vetor será preenchido parcialmente, usado como acumulador, ou quando é importante que todas as posições comecem com zero.
12. realloc aloca um novo bloco maior em outro endereço, copia os dados antigos para ele, libera o bloco antigo e devolve o endereço do novo. Usamos ponteiro auxiliar porque, se realloc falhar, retorna NULL mas o bloco original continua válido. Se fizéssemos v = realloc(v, ...), perderíamos o endereço original em caso de falha.
13. Vazamento de memória ocorre quando um bloco alocado no heap perde todas as referências antes de ser liberado, tornando-se permanentemente inacessível. Exemplo:
for(i = 0; i < 100; i++){
int *v = malloc(1000 * sizeof(int));
}
A cada iteração, o endereço anterior é perdido e nenhum free é chamado.
14. A main é quem alocou a turma (malloc), então a responsabilidade de liberá-la (free) é dela. As funções buscar_aluno_por_matricula e aplicar_bonus apenas consultam e modificam elementos que já existem no bloco alocado. Elas não são donas da memória, portanto não devem liberá-la.
15. turma_adicionar precisa modificar o ponteiro que a main está usando, pois após realloc o endereço do bloco pode ter mudado. Para alterar uma variável de fora da função, precisamos do endereço dessa variável. Se a variável na main é Aluno * (um ponteiro), seu endereço é Aluno **. É a mesma lógica de scanf("%d", &idade): para modificar idade, passamos &idade; para modificar turma, passamos &turma.
Próximos passos12.19
Agora sabemos localizar dados com ponteiros e criar e redimensionar dados com alocação dinâmica. Com struct, ponteiros e alocação dinâmica, temos as ferramentas centrais para construir programas em C que se adaptam ao tamanho real dos dados.
Na próxima aula, vamos estudar recursão, uma técnica em que uma função chama a si mesma para resolver problemas que podem ser decompostos em versões menores de si próprios, e que se apoia diretamente na pilha de chamadas que começamos a entender nesta aula. Continuaremos com exercícios práticos para consolidar a leitura e a escrita de código, porque é programando que o entendimento realmente se firma.