Programação de Computadores

Ponteiros e Alocação Dinâmica em C

Professor: Gabriel Soares Baptista

Ligação com as aulas anteriores

A aula 11 mostrou como modelar dados com struct.

Usamos símbolos como &, *, -> e NULL na prática.

Mas ainda faltava desmontar a lógica por trás deles.

Também notamos que o vetor Aluno turma[100] tem tamanho fixo.

Esta aula resolve as duas coisas: o que são ponteiros por dentro e como criar dados dinamicamente.

Objetivos

  • Entender que toda variavel ocupa um endereco de memoria.
  • Usar &, * e -> para localizar e modificar dados.
  • Diferenciar passagem por valor e passagem por endereco.
  • Entender por que vetores fixos sao limitados.
  • Usar malloc, calloc, realloc e free.
  • Compreender e evitar vazamentos de memoria.

Variaveis vivem na memoria

Quando declaramos uma variavel, o programa reserva um espaco na memoria.

Esse espaco tem uma posicao concreta com um endereco.

int idade = 20;

printf("Valor: %d\n", idade);           // 20
printf("Endereco: %p\n", (void*)&idade); // 0x...
  • idade representa o valor da variavel.
  • &idade representa o endereco da variavel.
  • Sao coisas relacionadas, mas nao sao a mesma coisa.

Um ponteiro guarda endereco

int idade = 20;
int *p = &idade;

Leia assim:

  • idade guarda o valor 20
  • &idade produz o endereco de idade
  • p guarda esse endereco
  • Portanto, p aponta para idade

p nao guarda 20. Quem guarda 20 e idade. p guarda o endereco de idade.

O tipo do ponteiro importa

int *p;        // ponteiro para int
float *q;      // ponteiro para float
Aluno *a;      // ponteiro para Aluno
char *texto;   // ponteiro para char

O tipo informa o que esperamos encontrar no endereco e quantos bytes ler ou escrever.

* tambem acessa o conteudo apontado

O * aparece em dois contextos:

  • na declaracao: faz parte do tipo (int *p)
  • na expressao: acessa o conteudo no endereco (*p)
printf("Endereco: %p\n", (void*)p);   // o endereco
printf("Conteudo: %d\n", *p);          // o valor la encontrado

p e endereco. *p e o conteudo encontrado nesse endereco.

*p = 35;   // altera a variavel apontada
printf("%d\n", idade);   // agora imprime 35

p e *p nao sao a mesma coisa. p e endereco. *p e o conteudo.

printf() e scanf() na memoria

int idade = 20;
printf("Valor: %d\n", idade);       // le o conteudo
printf("Endereco: %p\n", &idade);   // mostra a localizacao
int idade;
scanf("%d", &idade);   // precisa do endereco para escrever

scanf() nao foi chamado para observar a variavel. Ele precisa escrever nela. Para escrever, precisa saber onde ela esta.

Certo e errado no scanf

scanf("%d", &idade);   // CORRETO: entrega o endereco
scanf("%d", idade);    // ERRADO: idade e um valor, nao um endereco

O fluxo:

  1. usuario digita um valor
  2. scanf interpreta esse valor
  3. o programa precisa saber onde grava-lo
  4. &idade entrega o endereco correto
  5. a escrita ocorre na posicao da variavel

Passagem por valor

void tentar_alterar(int x){
    x = 100;
}

int main(void){
    int n = 10;
    tentar_alterar(n);
    printf("%d\n", n);   // imprime 10
    return 0;
}

A funcao recebeu uma copia do valor de n.

Alterar x nao altera n, porque sao variaveis em enderecos diferentes.

Passagem por endereco

void alterar(int *p){
    *p = 100;
}

int main(void){
    int n = 10;
    alterar(&n);
    printf("%d\n", n);   // imprime 100
    return 0;
}

&n entrega o endereco de n. A funcao usa *p para acessar e modificar a variavel original.

Foi exatamente isso que aplicar_bonus(Aluno *a, float bonus) fez na aula 11.

Visualizando a diferenca

Passagem por valor:

main: n = 10    ---copia--->    funcao: x = 10

Passagem por endereco:

funcao: p = 0x100   ---mesmo endereco--->   main: n em 0x100

Na passagem por valor, sao duas variaveis separadas.

Na passagem por endereco, a funcao trabalha com a mesma posicao de memoria.

Ponteiros para struct e ->

Aluno aluno;
aluno.matricula = 1001;   // acesso direto: .

Aluno *a = &aluno;
a->matricula = 1001;      // acesso via ponteiro: ->
  • . quando temos a estrutura diretamente.
  • -> quando temos um ponteiro para a estrutura.

a->faltas equivale a (*a).faltas. Na pratica, usamos -> porque a leitura fica mais natural.

NULL = ausencia de endereco valido

int *p = NULL;

p existe como variavel, mas nao aponta para nenhum objeto valido.

Aluno *a = buscar_aluno_por_matricula(...);

if(a != NULL){
    aplicar_bonus(a, bonus);
}

Se a matricula existe: a recebe o endereco do aluno.

Se nao existe: a recebe NULL. O teste protege contra acesso invalido.


Se a == NULL, nao faz sentido tentar a->faltas. Nao ha estrutura valida naquele endereco.

Arrays e enderecos

int v[3] = {10, 20, 30};

printf("%d\n", v[0]);   // 10
printf("%d\n", *v);     // 10 tambem

O nome do vetor se comporta como o endereco do primeiro elemento.

O vetor ocupa posicoes consecutivas de memoria: 0x300, 0x304, 0x308...

Isso explica &turma[i] em ler_aluno(&turma[i]): turma[i] e o aluno na posicao i, e &turma[i] e o endereco desse aluno.

O problema do tamanho fixo

Aluno turma[100];
  • Sempre 100 posicoes, mesmo que n = 3.
  • Se n > 100, o programa quebra ou corrompe memoria.
  • Nao da pra crescer depois de criado.
  • O tamanho real so aparece em tempo de execucao.
int n;
scanf("%d", &n);
Aluno turma[n];   // VLA: funciona em alguns compiladores, mas nao e portatil
Reflita

Far sentido o programa ocupar memoria para 100 alunos se a maioria das execucoes usa apenas 3 ou 4?

Duas regioes de memoria

      Memoria do programa
      /                \
Pilha (stack)        Heap (monte)
Automatica           Manual
int x, float v[10]   malloc, free
Tamanho fixo         Tamanho em execucao
Liberada ao sair     Liberada com free

Ate agora usamos so a pilha. A alocacao dinamica usa o heap.

malloc(): pedindo memoria

#include <stdlib.h>

void *malloc(size_t tamanho);

Recebe a quantidade de bytes e devolve o endereco do bloco (ou NULL se falhar).

int *p = malloc(sizeof(int));
*p = 42;
  • sizeof(int) calcula os bytes de um int (normalmente 4).
  • malloc pede esses bytes ao heap.
  • p guarda o endereco devolvido.
  • Usamos *p normalmente.

Alocando um vetor

int n;
scanf("%d", &n);

int *v = malloc(n * sizeof(int));

Agora n veio da entrada. O heap aloca exatamente o necessario.

Apos alocar, usamos v[i] como um vetor comum:

for(i = 0; i < n; i++)
    v[i] = i * 10;

malloc(n) aloca n bytes. Para n inteiros, precisa ser malloc(n * sizeof(int)).

Verificando falhas

int *v = malloc(n * sizeof(int));

if(v == NULL){
    printf("Erro: memoria insuficiente.\n");
    return 1;
}

malloc pode falhar se o heap nao tiver memoria suficiente.

Ignorar a verificacao significa arriscar acessar *v quando v == NULL, o que causa falha de segmentacao.

A turma dinamica

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]);
Antes (fixo) Depois (dinamico)
Aluno turma[100] Aluno *turma = malloc(n * sizeof(Aluno))
Sempre 100 posicoes Exatamente n posicoes
n maximo 100 Limitado pela RAM

As funcoes da biblioteca nao mudam -- recebem Aluno * e tanto faz se veio da pilha ou do heap.

calloc(): alocando e zerando

malloc entrega lixo de memoria. calloc entrega tudo zerado.

int *v1 = malloc(5 * sizeof(int));   // ?, ?, ?, ?, ?
int *v2 = calloc(5, sizeof(int));    // 0, 0, 0, 0, 0
Funcao Argumentos Inicializacao
malloc(n * sizeof(int)) Total de bytes Nao limpa
calloc(n, sizeof(int)) Quantidade, tamanho Zera tudo

calloc e mais seguro para vetores usados como acumuladores.

realloc(): redimensionando

int *v = malloc(3 * sizeof(int));
v[0] = 10; v[1] = 20; v[2] = 30;

int *temp = realloc(v, 5 * sizeof(int));
if(temp != NULL){
    v = temp;
    v[3] = 40;
    v[4] = 50;
}
  • Preserva os dados originais.
  • Se nao couber no mesmo local, aloca outro, copia os dados e libera o antigo.
  • Use ponteiro auxiliar (temp) para nao perder o bloco original se falhar.

v = realloc(v, ...) e perigoso! Se realloc falhar, retorna NULL e voce perde o endereco original.

Acrescentando um aluno

void turma_adicionar(Aluno **turma, int *n, Aluno aluno){
    Aluno *temp = realloc(*turma, (*n + 1) * sizeof(Aluno));
    if(temp == NULL) return;

    *turma = temp;
    (*turma)[*n] = aluno;
    (*n)++;
}

Aluno **turma: para modificar o ponteiro da main, passamos o endereco dele.

Mesma logica de scanf("%d", &idade): para alterar um Aluno *, passamos Aluno **.

free(): devolvendo memoria

int *v = malloc(100 * sizeof(int));
// usa o vetor...
free(v);
v = NULL;   // pratica defensiva

Depois de free(v), o bloco volta ao sistema.

Acessar v[i] depois disso e comportamento indefinido.

free(NULL) e seguro, entao v = NULL apos free protege contra uso acidental.

Vazamento de memoria

for(i = 0; i < 1000; i++){
    int *v = malloc(1000000 * sizeof(int));
    // esqueceu de chamar free(v)
}

A cada iteracao, o endereco anterior e perdido. Milhoes de bytes nunca serao liberados.

Em programas que rodam horas (servidores, editores), vazamentos se acumulam e degradam o sistema.

Regra de ouro

Cada malloc, calloc ou realloc bem-sucedido exige um free correspondente.

O sistema completo com alocacao dinamica

int main(void){
    int n;
    scanf("%d", &n);

    Aluno *turma = malloc(n * sizeof(Aluno));
    if(turma == NULL) return 1;

    for(i = 0; i < n; i++)
        ler_aluno(&turma[i]);

    // aplica bonus, imprime relatorio...

    free(turma);
    return 0;
}

Mudou muito pouco em relacao a versao com vetor fixo.

A diferenca: o tamanho e exato, pode ser qualquer valor e liberamos no final.

O ciclo completo

Aula Problema central Ferramenta
11. TADs Dados soltos, sem modelo struct, typedef, enum
12. Ponteiros e Aloc. Dinamica Localizar, criar, redimensionar &, *, ->, malloc, free

struct modela as entidades.

Ponteiros localizam e modificam essas entidades na memoria.

Alocacao dinamica cria e redimensiona esses dados durante a execucao.

Problema 1: troca com ponteiros

Escreva uma funcao void trocar(int *a, int *b) que troca os valores entre duas variavies.

No main, declare x = 10 e y = 20, chame trocar(&x, &y) e imprima os valores depois da troca.

Saida esperada:

x = 20, y = 10

Problema 2: vetor dinamico de notas

Leia um inteiro n, aloque um vetor de float com malloc, preencha com notas lidas do teclado, calcule a media e imprima.

Nao esqueca de verificar se malloc retornou NULL e de liberar a memoria no final.

Exemplo de execucao:

3
8.5 7.0 9.5
Media: 8.33

Problema 3: expandindo um vetor

Comece com um vetor de 3 inteiros alocado com malloc e preenchido com [10, 20, 30].

Use realloc para expandir para 5 posicoes e preencher as novas com 40 e 50.

Imprima todos os elementos no final.

Saida esperada:

10 20 30 40 50

Proximos Passos

Agora sabemos localizar dados com ponteiros e criar e redimensionar dados com alocacao dinamica.

Com struct, ponteiros e alocacao dinamica, temos as ferramentas para construir programas que se adaptam ao tamanho real dos dados.

Na proxima aula, vamos estudar recursao e continuar com exercicios praticos para consolidar a leitura e a escrita de codigo.