Ponteiros

Um ponteiro é um tipo que se refere a outro objeto. Utilizar o símbolo * em uma declaração faz com que ela seja um ponteiro, por exemplo:

char  *a;  // a é um ponteiro para um char
int   *b;  // b é um ponteiro para um int
float *c;  // c é um ponteiro para um float
float **d; // d é um ponteiro para um ponteiro para um float

Para fazer com que um ponteiro se refira a uma variável, precisamos adquirir o endereço da variável na memória com o operador & e atribuí-lo ao ponteiro:

char A;
int B;
float C;
float *D;

a = &A; // a agora se refere à variável A
b = &B; // b agora se refere à variável B
c = &C; // c agora se refere à variável C
d = &D; // d agora se refere à variável D

Operador de Indireção

O símbolo * é comumente o operador "produto" e realiza multiplicação, mas quando precede um ponteiro esse mesmo símbolo é o operador "indireção". A indireção serve para acessar o objeto alvo do ponteiro, por exemplo:

int i;

int *p = &i; // p se refere a i

*p = 5; // A indireção acessa i
printf("%d\n", i);  // Exibe 5
printf("%d\n", *p); // Exibe 5

i = 12;
printf("%d\n", i);  // Exibe 12
printf("%d\n", *p); // Exibe 12

Como pode ser observado acima, a indireção de um ponteiro é análoga ao objeto a que ele se refere. I.e., *p = 2 é efetivamente i = 2.

Ponteiros como Parâmetros

Até agora vimos o que ponteiros fazem, mas não um motivo para usá-los. Um caso em que ponteiros são úteis é uma função que deve modificar o valor de seus argumentos, tome como exemplo uma função que incrementa um inteiro:

void incremento(int valor)
{
    valor = valor + 1;
}

Como o escopo de valor é o corpo da função, a atribuição não possui efeito no código externo. Para que o resultado seja utilizado, nesse caso, é preciso retorná-lo:

int incremento(int valor)
{
    return valor + 1;
}

Porém essa função é contraintuitiva, incremento(num) retorna num + 1 mas o valor de num é inalterado a não ser que o retorno seja utilizado: num = incremento(num). Utilizando ponteiros, esse incômodo pode ser evitado:

void incremento(int *endereço)
{
    *endereço = *endereço + 1;
}

A função acima recebe um ponteiro e incrementa o valor do inteiro a que ele se refere. Embora o escopo de endereço seja o corpo da função, o objeto alvo pode estar em algum lugar externo. Essa versão é mais intuitiva que a anterior, pois basta utilizar incremento(&num) para num ser incrementado, sem necessidade de receber um valor de retorno.

Inicialização

Como vimos, ponteiros se referem a outros objetos. Quando um ponteiro não é inicializado, seu valor é indefinido e utilizar o operador de indireção causará comportamento perigoso e indesejado.

Uma maneira segura de especificar que um ponteiro não referencia algum objeto válido é utilizar a constante NULL definida em <stddef.h>, e.g. int *p = NULL;. NULL em uma condição é equivalente a false, assim, antes de realizar uma indireção no ponteiro, é possível verificar se ele é nulo:

void incremento(int *endereço)
{
    if (endereço)
        *endereço = *endereço + 1;
}

Se o ponteiro recebido for nulo, a condição endereço será false e a instrução *endereço = *endereço + 1; não será executada.

Em outras palavras, utilizar NULL é uma convenção para que ponteiros inválidos possuam um valor que os identifiquem. Funções de várias bibliotecas (incluindo a biblioteca padrão) verificam se um ponteiro é nulo antes de tentar utilizá-lo.

Referências

  • ISO/IEC JTC1/SC22/WG14 N2310:
    • 6.5.3.2 Address and indirection operators
    • 6.7.6 Declarators
Conteúdo escrito pela organização codinStruct, disponível pela licença CC BY-SA 4.0. Veja o repositório original do conteúdo.