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
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.
| 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.
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.
| 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.
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
xcontém42e está no endereço0x100;ycontém137e está no endereço0x200;pcontém0x100(o endereço dex);ppcontém0x300(o endereço dep).
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,
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.
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 idadePara 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çãosequenceDiagram
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.00Mudamos o valor de meta sem acessá-la diretamente. Isso é o poder dos ponteiros!
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.
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
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.
Nesta unidade, você aprendeu
✅ a declarar e desreferenciar ponteiros;
✅ a modificar valores via ponteiros;
✅ a passar variáveis por referência;
✅ a modificar ponteiros.
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.