Ponteiros (Revisão)

Por muito tempo, construir estruturas de dados exigia que os desenvolvedores as reescrevessem para cada tipo de dado. Era uma batalha constante: uma lista para números inteiros, outra para textos, outra para dados de usuários, e assim por diante. Essa repetição não apenas consumia tempo, mas também abria portas para erros e inconsistências.

A ideia central de estruturas genéricas é poder construir estruturas universais que podem ser aplicadas em qualquer projeto. Embora a linguagem C não tenha suporte nativo a “genéricos”, como C++ ou Java, a solução tradicional e elegante é usar ponteiros void* e funções de callback.

Antes de explorarmos essas estruturas, é fundamental revisar e aprofundar dois conceitos cruciais: ponteiros e ponteiros de função.

Como você deve ter visto em Algoritmos e Estruturas de Dados 1, um ponteiro é uma variável que armazena o endereço de memória de outra variável. Essa capacidade é a base para criar estruturas de dados com tamanho dinâmico (que crescem ou diminuem em tempo de execução).

Declaração

Quando declaramos uma variável e atribuímos um valor, esse valor é armazenado em um endereço de memória. Quando escrevemos int x = 10;, por exemplo, ocorrem dois passos conceituais:

  • Declaração: o programa reserva um espaço na memória para armazenar um inteiro.
  • Atribuição: o valor 10 é gravado nesse espaço de memória, que possui um endereço.

A variável x é apenas um rótulo usado pelo programador.

Ilustração da memória para uma variável do tipo int
Variável Endereço Conteúdo
x 0x75 10

Para se obter o endereço da variável x escrevemos &x. O resultado é modificado toda vez que você roda o programa. Como um ponteiro armazena um endereço de memória, um ponteiro para x seria declarado como int* x_ptr = &x.

Se liga! A memória em si é ilustrada pelo endereço e conteúdo armazenado. A variável é apenas um rótulo que representa este par.

O princípio básico de um ponteiro é a compatibilidade de tipo, ou seja, um ponteiro deve ser do mesmo tipo do dado que ele aponta.

double meta = 7.5;
double *meta_ptr = &meta; // O operador & retorna o endereço na memória

Na linha 2, meta_ptr guarda o endereço da variável meta.

O quadro abaixo ilustra a memória depois das declarações do trecho de código anterior.

Ilustração da memória para um ponteiro
Variável Endereço Conteúdo
meta 0x55 7.5
meta_ptr 0x58 0x55

Estamos usando endereços fictícios. Note que meta_ptr guarda o endereço de meta. Neste caso, dizemos que meta_ptr aponta para meta.

Vamos explorar outro exemplo usando um diagrama que ilustra uma memória.

flowchart RL
    %% Nós de variáveis e ponteiros
    X["📦 <b>x</b><br/>Endereço: <code>0x100</code><br/>Conteúdo: <b>42</b>"]
    Y["📦 <b>y</b><br/>Endereço: <code>0x200</code><br/>Conteúdo: <b>137</b>"]
    P["🎯 <b>p</b><br/>Endereço: <code>0x300</code><br/>Conteúdo: <code>0x100</code>"]
    PP["🎯 <b>pp</b><br/>Endereço: <code>0x400</code><br/>Conteúdo: <code>0x300</code>"]

    %% Conexões
    PP -->|"aponta para"| P
    P -->|"aponta para"| X
    P ~~~ Y
    Y ~~~ X

    %% Estilos de formatação
    classDef var fill:#A8E6CF,stroke:#2E7D32,color:#000,stroke-width:1px;
    classDef ptr fill:#FFD3B6,stroke:#E65100,color:#000,stroke-width:1px;
    class X,Y var
    class P,PP ptr

Se liga! Cada quadro é um bloco de memória que possui um endereço e um valor. Os ponteiros estão em laranja.

As variáveis x e y carregam dados que estão armazenados nos endereços 0x100 e 0x200, respectivamente. A variável p é um ponteiro cujo conteúdo é 0x100, portanto aponta para x. A variável pp é outro ponteiro, mas com conteúdo 0x300, ou seja, aponta para o local com endereço 0x300, que seria p. Isso mesmo que você pensou, um ponteiro de ponteiro.

int x = 42;
int y = 137;
int *p = &x;
int **pp = &p;

Resumindo

  • x contém 42 e está no endereço 0x100;
  • y contém 137 e está no endereço 0x200;
  • p contém 0x100 (o endereço de x);
  • pp contém 0x300 (o endereço de p).

Vale salientar que há três declarações de ponteiros ligeiramente diferentes:

double *meta_ptr = &meta;
double* meta_ptr = &meta;
double * meta_ptr = &meta;

Se liga! Essas declarações dizem respeito ao local do símbolo *.

No entanto, a primeira é mais recomendada: tipo *ponteiro = &variavel.

Desreferenciação

Uma operação importante com ponteiros é a desreferenciação (ou dereferencing). Ela ocorre quando acessamos o conteúdo armazenado no endereço que o ponteiro aponta. Por exemplo,

double meta = 7.5;
double *meta_ptr = &meta; // O operador & retorna o endereço na memória
printf("*meta_ptr: %.2f\n", *meta_ptr); // *meta_ptr: 7.50

Na segunda linha, criamos um ponteiro que aponta para meta. Na linha 3, o operador * serve para desreferenciar, ou seja, ao usar *meta_ptr na impressão, o C acessa o valor armazenado no endereço que meta_ptr aponta, isto é, 7.5.

Dica

O * tem dupla função:

  • Declarar um ponteiro (double *p)
  • Desreferenciar um ponteiro (*p)

Se liga! Os operadores * e & se anulam, ou seja, *&p ou &*p é o mesmo que p.

É válido mencionar que, em uma variável que guarda um dado, podemos acessar o dado e o endereço. Já em um ponteiro, podemos acessar seu endereço, o conteúdo (outro endereço) e o dado armazenado pela variável que ele aponta:

int idade = 12;
int *idade_ptr = &idade;

printf("idade: %d\n", idade);
printf("&idade: %x\n", &idade);

printf("&idade_ptr: %x\n", &idade_ptr); // Endereço do ponteiro
printf("idade_ptr: %x\n", idade_ptr);   // Endereço de idade
printf("*idade_ptr: %d\n", *idade_ptr); // Conteúdo de idade

Para fixação, observe o diagrama de sequência que ilustra os processos de declação e desreferenciação.

int x = 10;
int *ptr = &x;
int y = *ptr;  // desreferenciação

sequenceDiagram
    participant Stack as Memória (Stack)
    participant X as Variável x
    participant Ptr as Ponteiro ptr
    participant CPU as CPU/Programa

    Note over CPU,Stack: Declaração e atribuição inicial
    CPU->>Stack: Aloca espaço para x = 10
    Stack->>X: Cria x (valor=10, endereço=0x1000)

    Note over CPU,Ptr: Ponteiro recebe o endereço de x
    CPU->>Ptr: ptr = &x
    Ptr->>Stack: Guarda endereço 0x1000

    Note over CPU,Stack: Desreferenciação
    CPU->>Ptr: lê conteúdo de ptr
    Ptr-->>Stack: endereço 0x1000
    Stack-->>CPU: retorna valor armazenado (10)
    CPU->>Stack: y = 10

    Note right of CPU: *ptr lê o valor armazenado em x

Além de ler o conteúdo de uma variável, também podemos modificá-lo indiretamente usando ponteiros:

double meta = 7.5;
double *meta_ptr = &meta; 

*meta_ptr = 10;
printf("meta: %.2f\n", meta); // meta: 10.00

Mudamos o valor de meta sem acessá-la diretamente. Isso é o poder dos ponteiros!

Nota🎯 Desafio de Código
  • Crie um ponteiro p que aponte para uma variável int x = 5.
  • Use o ponteiro para alterar x para 42.
  • Depois, exiba o valor e o endereço de x no console.
#include <stdio.h> 

int main(){
    int x = 5;   
    int *p = &x; // Ponteiro apontando para x (ambos do mesmo tipo)
    *p = 42;     // Modificação indireta

    printf("x: %d\n", x);
    printf("&x: %p\n", &x);

    return 0;
}
Nota🎯 Desafio de Código
  • Passo 1: Declare um ponteiro chave_ptr que aponte para a chave_secreta.
  • Passo 2: Mude o valor da chave_secreta para 99 usando o ponteiro.
  • Passo 3: Imprima o valor da chave secreta.
#include <stdio.h>

int main() {
    int chave_secreta = 10;
    // SEU CÓDIGO

    return 0;
}
#include <stdio.h> 

int main() {
    int chave_secreta = 10;
    int *chave_ptr = &chave_secreta;
    *chave_ptr = 99;

    printf("chave_secreta: %d\n", chave_secreta);
    return 0;
}

Ponteiros como parâmetros

Em C, os parâmetros são passados por valor. Portanto, a função recebe uma cópia do argumento.

void dobrar_meta (double meta){ 
    meta = 2*meta; 
}

Na main:

double meta = 7.5;
printf("meta: %.2f\n", meta);
dobrar_meta(meta);
printf("meta: %.2f\n", meta);

Saída:

meta: 7.50
meta: 7.50

Esperavámos que no exemplo acima, o valor de meta tivesse sido dobrado, mas nada mudou. A razão é que a função alterou apenas a cópia local. A variável meta na função main é global, já na função dobrar_meta é local. Significa que quando esta função termina, as variáveis são descartadas.1

1 Se você declarar uma variável local com o mesmo nome de uma variável global, a variável local prevalece dentro do seu escopo, ocultando temporariamente a global.

Imprimindo os endereços, vamos constatar que trata-se de variáveis diferentes.

void dobrar_meta (double meta){ 
    printf("local &meta: %x\n", &meta);
    meta = 2*meta; 
}

Na main:

double meta = 7.5;
printf("main &meta: %x\n", &meta);
dobrar_meta(meta);

Saída:

main &meta: 0xe8
local &meta: 0xc0

Os endereços são diferentes. Logo, não são as mesmas variáveis que estamos manipulando

A variável interna em dobrar_meta é criada em outra região de memória e recebe uma cópia do valor passado como argumento. Esse valor é dobrado em seguida, mas não reflete na variável da função main, pois ela está alocada em outra posição.

flowchart LR
 subgraph s1[" "]
        X["📦 <b>meta</b><br>Endereço: <code>0xe8</code><br>Conteúdo: <b>7.5</b>"]
        Y["📦 <b>meta</b><br>Endereço: <code>0xc0</code><br>Conteúdo: <b>7.5</b>"]
  end

 subgraph s2[" "]
        A["📦 <b>meta</b><br>Endereço: <code>0xe8</code><br>Conteúdo: <b>7.5</b>"]
        B["📦 <b>meta</b><br>Endereço: <code>0xc0</code><br>Conteúdo: <b>15</b>"]
  end

    s1 --dobrar_meta--> s2
    
     X:::var
     Y:::var
     A:::var
     B:::var
    classDef var fill:#A8E6CF,stroke:#2E7D32,color:#000,stroke-width:1px
    style s1 fill:transparent
    style s2 fill:transparent

Para solucionar isso, ao invés de passarmos o conteúdo armazenado, podemos passar o endereço. Isso é conhecido como passagem por referência. Na verdade, ainda será feita uma cópia do valor passado, mas como esse valor copiado é um endereço (o ponteiro), esse artifício será suficiente para manipularmos os dados originais.

#include <stdio.h>

void dobrar_meta (double *meta){ 
    *meta = 2*(*meta); // Desreferenciação 
}

int main (){
    double meta = 7.5;
    
    printf("meta: %.2f\n", meta);
    dobrar_meta(&meta); // Passando o endereço
    printf("meta: %.2f\n", meta);

    return 0;
}

Saída:

meta: 7.50
meta: 15.00

Dessa vez, a função modificou a variável original, pois passamos a referência. Vamos ilustrar como isso aconteceu.

Variável Endereço Conteúdo
main meta 0xe8 7.5
local meta 0xc0 0xe8

Dentro da função, quando fazemos *meta, o C faz a desreferenciação, ou seja, retorna o conteúdo do local cujo o endereço é 0xe8. Neste local, a operação de dobrar é efetuada.

Variável Endereço Conteúdo
main meta 0xe8 15
local meta 0xc0 0xe8

Ao finalizar, a variável meta da função main sofreu a alteração.

flowchart LR
 subgraph s1[" "]
        X["📦 <b>meta</b><br>Endereço: <code>0xe8</code><br>Conteúdo: <b>7.5</b>"]
        Y["🎯 <b>meta</b><br>Endereço: <code>0xc0</code><br>Conteúdo: <b>0xe8</b>"]
  end
 subgraph s2[" "]
        A["📦 <b>meta</b><br>Endereço: <code>0xe8</code><br>Conteúdo: <b>15</b>"]
        B["🎯 <b>meta</b><br>Endereço: <code>0xc0</code><br>Conteúdo: <b>0xe8</b>"]
  end
    s1 -- dobrar_meta --> s2

     X:::var
     Y:::ptr
     A:::var
     B:::ptr
    classDef var fill:#A8E6CF,stroke:#2E7D32,color:#000,stroke-width:1px
    classDef ptr fill:#FFD3B6,stroke:#E65100,color:#000,stroke-width:1px;
    style s1 fill:transparent
    style s2 fill:transparent

Importante

Quando passamos o endereço (&meta) para a função dobrar_meta, a variável meta interna vai armazenar em seu conteúdo um endereço de memória. Ou seja, ela é um ponteiro e, portanto, podemos desreferenciar. Por exemplo, considere as seguintes assinaturas de funções:

void foo(int a);
void bar(int* a);
void baz(int** a);

Quando fazemos foo(x), internamente é feita uma cópia do valor de x, ou seja, é criado int a = x. Do mesmo modo, quando fazemos bar(&x), então é criado int *a = &x. Já para a função baz, devemos passar um endereço de um ponteiro, por exemplo, baz(&p) (onde p é um int*). Se usarmos os mesmos nomes, o C consegue diferenciar a variável da função da que vem como argumento. Resumindo:

Considere

int x = 10;
int *p = &x;
Se a função é… Ela espera receber… Como você chama O que pode fazer
foo(int a) Um valor inteiro foo(x) Pode fazer cálculos com o 10, mas não toca no x original
bar(int* a) Um endereço de um inteiro bar(&x) Pode fazer cálculos com o 10 e alterar x
baz(int** a) Um endereço de um ponteiro baz(&p) Pode fazer cálculos com o 10 e alterar x e o ponteiro p

Se você quer alterar o valor, passe um ponteiro. Se você quer alterar para onde o ponteiro aponta, passe o endereço do ponteiro.

Nota🎯 Desafio de Código

Implemente uma função swap que troque os valores de duas variáveis. Teste na main com:

int x = 10, y = 20;
swap(&x, &y);
printf("x = %d, y = %d\n", x, y);

Saída esperada: x = 20, y = 10

#include <stdio.h> 

void swap(int *a, int *b){
    int aux = *a;
    a* = *b;
    b* = aux;
}

int main(){
    int x = 10, y = 20;
    swap(&x, &y);
    printf("x = %d, y = %d\n", x, y);

    return 0;
}
NotaExercício de Fixação

Considere o código base abaixo:

#include <stdio.h>

int main() {
    int x = 5;
    int y = 10;
    int *p = &x;

    printf("x = %d\n", x);
    printf("y = %d\n", y);
    printf("*p = %d\n", *p);

    return 0;
}

Considere as funções a seguir independentemente.

void f1(int a) { a = a + 10; }
  1. Qual chamada é válida?
  • f1(x)
  • f1(&x)
  • f1(p)
  1. Após a chamada correta, qual será o valor de x?
void f2(int *a) { *a = *a + 10; }
  1. Qual chamada é válida?
  • f2(x)
  • f2(&x)
  • f2(p)
  1. Após a chamada correta, qual será o valor de x?
void f3(int **a) { **a = 99; }
  1. Qual chamada é válida?
  • f3(&x)
  • f3(p)
  • f3(&p)
  1. Após a chamada correta, qual será o valor de x?
void f4(int **a, int *b) { *a = b; }
  1. Qual chamada faz p passar a apontar para y?
  • f4(p, &y)
  • f4(&p, y)
  • f4(&p, &y)
  1. Após a chamada correta, qual será o valor impresso por *p?
void f5(int *a) {
    int z = 50;
    a = &z;
}
  1. Após a chamada f5(p), p passa a apontar para z?

Nesta unidade, você aprendeu

✅ a declarar e desreferenciar ponteiros;

✅ a modificar valores via ponteiros;

✅ a passar variáveis por referência;

✅ a modificar ponteiros.

Dica

Se você quiser aprofundar mais seu conhecimento sobre ponteiros, revise materiais sobre ponteiros para structs, aritmética de ponteiros, ponteiros para strings e a relação entre arrays e ponteiros.

# O que melhor descreve um ponteiro em C? 1. [ ] Uma variável que armazena o valor de outra variável. 1. [x] Uma variável que armazena o endereço de memória de outra variável. 1. [ ] Uma função que manipula endereços de memória. 1. [ ] Um tipo especial usado apenas para arrays. # Por que um ponteiro deve ser do mesmo tipo da variável para a qual aponta? 1. [x] Para garantir a interpretação correta dos dados armazenados na memória. 1. [ ] Para permitir conversão implícita de tipos. 1. [ ] Porque o C faz o casting automático de ponteiros. 1. [ ] Não é necessário; qualquer ponteiro pode apontar para qualquer tipo de dado com segurança. # O que acontece ao usar o operador `*` sobre um ponteiro? 1. [ ] Ele recupera o endereço de memória armazenado no ponteiro. 1. [x] Ele acessa o valor armazenado no endereço de memória apontado pelo ponteiro. 1. [ ] Ele declara uma nova variável ponteiro. 1. [ ] Ele libera a memória associada ao ponteiro. # Quais das declarações abaixo são **equivalentes e válidas** em C? - [x] `double *meta_ptr = &meta;` - [x] `double* meta_ptr = &meta;` - [ ] `double meta_ptr* = &meta;` - [x] `double * meta_ptr = &meta;` # Por que a função `dobrar_meta(double meta)` não altera o valor da variável `meta` na função `main`? 1. [x] Porque o C passa argumentos por valor, criando uma cópia da variável. 1. [ ] Porque variáveis do tipo `double` são imutáveis. 1. [ ] Porque a função não tem retorno (`void`). 1. [ ] Porque a variável `meta` é constante. # O que faz a expressão `*meta = 2 * (*meta);`? 1. [ ] Declara um novo ponteiro. 1. [x] Dobra o valor armazenado na variável original, desreferenciando o endereço recebido. 1. [ ] Altera o endereço armazenado no ponteiro. 1. [ ] Cria uma cópia do ponteiro. # Marque as afirmativas verdadeiras sobre o exemplo `swap(&x, &y)`. - [x] São passados os endereços das variáveis `x` e `y`. - [x] Os valores de `x` e `y` são modificados diretamente. - [ ] A função troca apenas cópias das variáveis. - [ ] A função utiliza passagem por valor.
De volta ao topo