Ponteiros são um dos recursos mais poderosos e fundamentais da linguagem C. Eles permitem manipular diretamente endereços de memória, possibilitando alocação dinâmica, passagem por referência e construção de estruturas de dados complexas. Nesta aula, vamos entender o que é um ponteiro, como usar os operadores & e *, como declará-los e a importância do valor NULL.

Dominar ponteiros é essencial para se tornar um programador C competente. Embora possam parecer confusos no início, com prática e exemplos claros você verá que são ferramentas naturais e extremamente úteis.

O que é um ponteiro

Um ponteiro é uma variável que armazena o endereço de memória de outra variável. Em vez de guardar um valor direto (como um inteiro ou caractere), um ponteiro guarda a localização onde esse valor está armazenado. Isso permite acessar e modificar indiretamente os dados.

Imagine a memória do computador como um grande armário com gavetas numeradas. Cada gaveta tem um endereço único. Uma variável comum guarda um objeto dentro da gaveta; um ponteiro guarda o número da gaveta onde o objeto está. Assim, você pode encontrar o objeto mesmo sem saber seu conteúdo atual.

#include <stdio.h>

int main() {
    int x = 10;       // variável comum
    int *ptr;          // declaração de ponteiro para inteiro
    ptr = &x;         // ptr recebe o endereço de x
    printf("Valor de x: %d\n", x);
    printf("Endereço de x: %p\n", (void*)&x);
    printf("Valor de ptr: %p\n", (void*)ptr);
    return 0;
}

No exemplo, ptr armazena o endereço de x. A saída mostrará que ptr e &x têm o mesmo valor (o endereço de x).

Operadores & e *

O operador & (e-comercial) é chamado de operador de endereço. Quando aplicado a uma variável, retorna o endereço de memória onde essa variável está armazenada. Por exemplo, &x retorna o endereço de x.

O operador * (asterisco) é chamado de operador de desreferência (ou indireção). Quando aplicado a um ponteiro, ele acessa o valor armazenado no endereço apontado. Por exemplo, se ptr aponta para x, então *ptr equivale a x. Você pode ler ou modificar o valor através do ponteiro.

#include <stdio.h>

int main() {
    int x = 10;
    int *ptr = &x;
    printf("x = %d\n", x);       // 10
    printf("*ptr = %d\n", *ptr);  // 10
    *ptr = 20;                     // altera x indiretamente
    printf("x = %d\n", x);       // 20
    return 0;
}

Note que *ptr = 20 modifica o valor de x, pois ptr aponta para x. Isso demonstra como ponteiros permitem modificar variáveis de outros escopos (útil em funções).

Declaração

Para declarar um ponteiro, usamos o tipo de dado que ele aponta, seguido de um asterisco (*) e o nome da variável. A sintaxe geral é: tipo *nome_do_ponteiro;. O asterisco indica que a variável é um ponteiro, não um valor comum.

Exemplos de declaração:

int *p;       // ponteiro para inteiro
float *q;     // ponteiro para float
char *r;      // ponteiro para char
void *s;      // ponteiro genérico (sem tipo específico)

É importante inicializar o ponteiro antes de usá-lo. Um ponteiro não inicializado contém um valor lixo (endereço inválido), e desreferenciá-lo pode causar falhas de segmentação. A inicialização pode ser com o endereço de uma variável existente ou com NULL.

int a = 5;
int *ptr = &a;   // inicializado com endereço de a
int *ptr2 = NULL; // inicializado com NULL

NULL

NULL é uma constante simbólica definida em várias bibliotecas (como <stddef.h> ou <stdio.h>) que representa um ponteiro que não aponta para lugar algum. Na prática, seu valor é 0 (zero) quando convertido para um ponteiro. Usar NULL para inicializar ponteiros é uma boa prática, pois permite verificar se o ponteiro é válido antes de desreferenciá-lo.

Desreferenciar um ponteiro NULL (ou zero) é um comportamento indefinido e geralmente causa uma falha de segmentação (crash). Por isso, sempre verifique se um ponteiro não é NULL antes de usá-lo, especialmente em funções que recebem ponteiros como argumentos.

#include <stdio.h>

int main() {
    int *ptr = NULL;
    if (ptr != NULL) {
        printf("%d\n", *ptr);
    } else {
        printf("Ponteiro é NULL, não é seguro desreferenciar.\n");
    }
    return 0;
}

Além disso, a macro NULL é frequentemente usada para indicar que um ponteiro não foi atribuído ou que uma função não retornou um endereço válido (por exemplo, malloc retorna NULL em caso de falha).

Boas práticas

Sempre inicialize ponteiros. Se não tiver um endereço válido, atribua NULL. Verifique se o ponteiro não é NULL antes de desreferenciá-lo. Evite ponteiros selvagens (não inicializados). Lembre-se de que o operador & só pode ser aplicado a variáveis (l-values), não a expressões ou constantes.

Referências

Exercícios

  1. Declare um ponteiro para inteiro chamado p e atribua a ele o endereço de uma variável a inicializada com 42. Em seguida, imprima o valor de a usando o ponteiro.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        int a = 42;
        int *p = &a;
        printf("Valor de a: %d\n", *p);
        return 0;
    }
  2. Escreva um programa que declare duas variáveis inteiras x e y, e um ponteiro ptr. Faça ptr apontar para x, atribua 10 a x através do ponteiro, depois faça ptr apontar para y e atribua 20 a y através do ponteiro. Imprima os valores de x e y.

    ✓ Resposta:
    #include <stdio.h>
    
    int main() {
        int x, y;
        int *ptr;
        ptr = &x;
        *ptr = 10;
        ptr = &y;
        *ptr = 20;
        printf("x = %d, y = %d\n", x, y);
        return 0;
    }
  3. O que acontece se você tentar desreferenciar um ponteiro NULL? Escreva um pequeno programa que demonstre (cuidado: pode crashar). Em vez de executar, apenas explique o comportamento.

    ✓ Resposta:

    Desreferenciar um ponteiro NULL é um comportamento indefinido. Na maioria dos sistemas, isso resulta em uma falha de segmentação (segmentation fault) e o programa é encerrado abruptamente. O código abaixo ilustra, mas não execute em ambiente de produção:

    int *ptr = NULL;
    printf("%d\n", *ptr); // crash!
  4. Explique a diferença entre int *p e int* p (espaço antes ou depois do asterisco). Isso faz diferença para o compilador?

    ✓ Resposta:

    Não faz diferença para o compilador. Ambos declaram p como um ponteiro para inteiro. O espaçamento é questão de estilo. No entanto, ao declarar múltiplas variáveis em uma linha, o asterisco se associa ao nome da variável, não ao tipo. Exemplo: int* p, q; declara p como ponteiro e q como inteiro. Por isso, muitos preferem int *p; para deixar claro que o asterisco faz parte do nome da variável.

  5. Escreva uma função troca que receba dois ponteiros para inteiros e troque os valores apontados. Use-a no main para trocar duas variáveis.

    ✓ Resposta:
    #include <stdio.h>
    
    void troca(int *a, int *b) {
        int temp = *a;
        *a = *b;
        *b = temp;
    }
    
    int main() {
        int x = 5, y = 10;
        printf("Antes: x=%d, y=%d\n", x, y);
        troca(&x, &y);
        printf("Depois: x=%d, y=%d\n", x, y);
        return 0;
    }