Em sistemas computacionais, toda informação processada pelo programa reside na memória RAM. Cada byte dessa memória recebe um identificador numérico único — o endereço de memória. Esse conceito é fundamental: assim como um CEP localiza uma residência, um endereço de memória permite ao processador acessar exatamente onde um dado está armazenado.
Por exemplo, uma variável do tipo int ocupa tipicamente 4 bytes, enquanto um char ocupa apenas 1 byte. Para manipular esses dados com precisão, o compilador atribui a cada byte um número sequencial — geralmente representado em hexadecimal (ex.: 0x7FFFAA10). Em ambientes de 32 bits, o espaço de endereçamento vai de 0x00000000 até 0xFFFFFFFF, totalizando até 4 GB de memória endereçável.
Abaixo, um exemplo que exibe os endereços de duas entidades distintas:
#include <stdio.h>
int main() {
int valor = 42;
char texto[] = "linguagem-c";
printf("Endereço de 'valor': %p\n", (void*)&valor);
printf("Endereço de 'texto': %p\n", (void*)texto);
return 0;
}
O especificador %p é o padrão portátil para impressão de endereços. Embora alguns ambientes exibam prefixos como 0x, outros não — por isso, o casting para void* garante compatibilidade entre plataformas. Note que texto, sendo um array, já se comporta como um ponteiro para seu primeiro elemento; logo, não requer o operador &.
Tudo na Memória é Endereço
No nível da máquina, não há distinção formal entre "código" e "dados": ambos são sequências binárias armazenadas em posições de memória. O sistema operacional impõe restrições de permissão — regiões com direito de execução contêm instruções, enquanto áreas com permissão de leitura/escrita abrigam variáveis. O processador opera exclusivamente por meio de endereços: ele busca instruções em determinados endereços e lê ou grava dados em outros.
Nomes de variáveis e funções são artefatos de conveniência para o programador. Durante a compilação e ligação, eles são substituídos por endereços reais. Por exemplo, a instrução resultado = x + y; é traduzida internamente para algo equivalente a:
0x7FFFAA28 = *(0x7FFFAA10) + *(0x7FFFAA14)
Onde * denota a operação de desreferenciamento: extrair o valor armazenado no endereço indicado.
Declaração e Tipagem de Ponteiros
Um ponteiro é uma variável cujo valor é um endereço de memória. Sua declaração inclui um asterisco (*) antes do nome, seguido do tipo dos dados apontados:
int numero = 123;
int *ptr_numero = № // ptr_numero guarda o endereço de 'numero'
O tipo int* não é sinônimo de int: é um tipo distinto, com tamanho e comportamento próprios. Isso é crucial para aritmética de ponteiros — como veremos adiante.
Vários ponteiros podem ser declarados simultaneamente, mas o * deve preceder cada identificador:
double *p1, *p2, *p3; // três ponteiros para double
double *q1, r2, s3; // apenas q1 é ponteiro; r2 e s3 são doubles normais
Desreferenciamento: Acessando o Valor Apontado
O operador unário * também serve para acessar o conteúdo do endereço armazenado em um ponteiro:
#include <stdio.h>
int main() {
long valor = 987654321L;
long *endereco = &valor;
printf("Valor direto: %ld\n", valor); // acesso direto
printf("Valor via ponteiro: %ld\n", *endereco); // acesso indireto
return 0;
}
Aqui, *endereco recupera o valor armazenado no local apontado — equivalente a valor. Entretanto, essa operação envolve dois passos: primeiro, ler o endereço contido em endereco; depois, acessar a memória nesse endereço. Já o acesso direto a valor requer apenas um único salto.
O desreferenciamento também permite modificação:
int a = 5, b = 10;
int *p = &a;
*p = b; // agora 'a' vale 10
Aritmética com Ponteiros
Ponteiros suportam operações aritméticas limitadas — principalmente adição, subtração e comparação. O incremento (++) ou decremento (--) não avança em bytes unitários, mas sim em blocos do tamanho do tipo apontado:
#include <stdio.h>
int main() {
short s = 1;
int i = 2;
double d = 3.14;
short *ps = &s;
int *pi = &i;
double *pd = &d;
printf("Antes: ps=%p, pi=%p, pd=%p\n", (void*)ps, (void*)pi, (void*)pd);
ps++; pi++; pd++;
printf("Após ++: ps=%p, pi=%p, pd=%p\n", (void*)ps, (void*)pi, (void*)pd);
return 0;
}
Supondo tamanhos típicos (short: 2 bytes, int: 4 bytes, double: 8 bytes), a saída refletirá incrementos de 2, 4 e 8 respectivamente. Isso torna a aritmética de ponteiros especialmente útil com arrays — onde arr + 1 naturalmente aponta para o próximo elemento.
Atenção: aplicar aritmética a ponteiros que não apontam para elementos de um mesmo bloco contíguo (como arrays ou structs) resulta em comportamento indefinido. O exemplo abaixo ilustra o risco:
int x = 100, y = 200;
int *px = &x;
printf("%d\n", *(px + 1)); // ❌ Não garante que 'y' esteja logo após 'x'
Compiladores não impedem esse uso, mas o resultado depende de fatores como alinhamento de memória, otimizações e ABI — jamais confie nele em código produtivo.
Operadores & e * em Combinação
As expressões &*p e *&x parecem redundantes, mas revelam propriedades importantes:
*&xequivale ax: obter o endereço dexe então desreferenciá-lo retorna o próprio valor.&*pequivale ap: desreferenciarpobtém o valor apontado; aplicar&recupera novamente o endereço — o mesmo dep.
Essas equivalências reforçam que & e * são operações inversas — desde que aplicadas de forma válida e sobre memória acessível.
Resumo das Funções do Asterisco (*)
O símbolo * tem três significados contextuais em C:
- Multiplicação: operador binário aritmético (
a * b). - Declaração de ponteiro: parte da sintaxe de tipo (
float *p;). - Desreferenciamento: operador unário que acessa o valor em um endereço (
*p = 42;).
Essa sobrecarga de significado exige atenção redobrada ao ler e escrever código — mas também oferece concisão e poder quando dominada.