Ponteiros

Ponteiros são um recurso poderoso na programação em C que permite trabalhar diretamente com endereços de memória.

Em C, um ponteiro é uma variável especial que armazena o endereço de memória de outra variável ou mesmo de uma função. Em outras palavras, um ponteiro “aponta” para o endereço de memória de outra variável. Isso permite aceder e manipular diretamente na memória o valor dessa variável.

Na imagem acima, a variável ptr guarda o endereço de memória da variável nr. A variável ptr é um ponteiro para a variável nr. Através do ponteiro ptr podemos aceder e alterar o conteúdo da variável nr.

O uso de ponteiros permite o seguinte:

  • Acesso direto à memória: Ponteiros fornecem acesso direto a locais de memória, permitindo uma manipulação de dados mais rápida em comparação ao acesso a dados por meio de nomes de variáveis.
  • Manipulação eficiente de dados: Estruturas de dados complexas podem ser manipuladas de forma mais eficiente usando ponteiros, levando a um desempenho aprimorado em operações como percorrer listas, por exemplo.

As duas características acima mencionadas levam a um tempo de execução de um programa em C mais rápido.

O operador endereço de (address of)

O operador endereço de (&) é fundamental na manipulação de ponteiros uma vez que este operador nos o endereço de memória de uma variável:

int nr = 7;
int endereco = &nr; 

Declaração e inicialização de ponteiros

Os ponteiros são declarados como outra variável qualquer mas usando o operador de indireção(*).

A sintaxe para a declaração de um ponteiro é a seguinte:

tipo_dado *nome_ponteiro;

Após a declaração de um ponteiro inicializamo-lo com o endereço de uma outra variável:

//declaração de uma variável do tipo int
int nr=7;
//declaração de um ponteiro do tipo int   
int *ptr; 
//inicialização do ponteiro com o endereço da variável nr
ptr = &nr;  

NOTA: o ponteiro ptr guarda o endereço da variável nr, logo podemos dizer que o ponteiro *ptr aponta para a variável nr.

NOTA: o tipo de dados de um ponteiro deve ser sempre o mesmo da variável para que ele aponta.

Quando declaramos um ponteiro podemos inicializa-lo de imediato :

int nr = 7;
int *ptr = &nr;

NOTAS:

  • o nome do ponteiro sem asterisco refere-se ao endereço de memória nele guardado.
  • o nome do ponteiro com asterisco refere-se ao valor que está guardado nesse endereço de memória

Exemplo:

    int nr = 7;

    ptr = &nr;

    printf("%d - %d", ptr, *ptr);

Aceder ao valor apontado por um ponteiro

Para aceder ao valor apontado por um ponteiro, usamos o operador de indireção *:

int numero = 37;
int *ponteiroNumero = №

//acessa o valor apontado pelo ponteiro:
int valor = *ponteiroNumero; // valor conterá 37

Modificar o valor apontado por um ponteiro

Podemos  modificar o valor apontado por um ponteiro simplesmente atribuindo um novo valor a ele, usando o operador de indireção:

*ponteiroNumero = 94; // Agora, 'numero' contém 94

Aritmética de ponteiros

A aritmética de ponteiros permite que se manipulem os endereços de memória de forma eficaz para aceder e percorrer arrays, estruturas de dados e outros tipos de dados.

A aritmética de ponteiros consiste basicamente na possibilidade que o C oferece de podermos incrementar ou decrementar um endereço de memória guardado num ponteiro, de forma a o ponteiro apontar para endereço de memória depois ou antes do atual, ou, então, podermos adicionar ou diminuir um valor a esse endereço.

A mecânica por trás da aritmética de ponteiros tem a ver com o tipo de dados do ponteiro em causa.

Vamos considerar o ponteiro seguinte:

int nr = 7;
int *ptr = &nr;

Vamos supor que o endereço guardado no endereço *ptr é 6422036.

Incrementemos o ponteiro de uma unidade e imprimamos o valor no ecran:

printf(“%d”, ptr + 1);

O novo endereço mostrado no ecran é agora 6422040.

Ou seja, ao adicionarmos 1 ao endereço, de facto o C adicionou 4.

A razão de tal é porque sendo o ponteiro *ptr do tipo int (que ocupa 4 bytes de memória) ao adicionarmos 1 ao endereço no ponteiro são adicionados 4 pois só assim o ponteiro passa para o endereço seguinte.

Resumidamente, ao adicionarmos 1 a um ponteiro dos tipos de dados abaixo, na realidade são adicionados os valores seguintes:

TipoTamanhoIncremento de 1Decrementa de 1
char1 byteIncrementa 1Decrementa 1
int4 bytesIncrementa 4Decrementa 4
float4 bytesIncrementa 4Decrementa 4
double8 bytesIncrementa 8Decrementa 8
Aritmética de ponteiros

A aritmética de ponteiros é muito usada para percorrermos arrays :

    int nrs[] = {4, 8, 6, 5};

    //inicialização do ponteiro com o endereço do array:
    int *ptr = nrs;

    //percorre todos os elementos do array incrementando o ponteiro em cada iteração:
    int i;
    for(i = 0; i < 4; i++, ptr++){
        printf("%d\n", *ptr);
    }

Ponteiros para estruturas

Podemos usar ponteiros em variáveis do tipo de uma estrutura qualquer.

Consideremos a estrutura seguinte:

   struct Pessoa {
        char nome[20];
        int idade;
        float altura;
    };

Declaremos uma variável do tipo de estrutura Pessoa e um ponteiro para essa variável:

    struct Pessoa pessoa;
    struct Pessoa *ptr = &pessoa;

Usando o ponteiro *ptr podemos aceder aos membros da estrutura:

    strcpy(ptr->nome, "Maria");

    ptr->idade = 21;

    ptr->altura = 1.76;

NOTA: quando usamos ponteiros para estruturas devemos usar o operador ‘->’ em lugar do operador ‘.’ para aceder aos membros da estrutura.

Ponteiros para funções

Ponteiros para funções em C são variáveis que armazenam o endereço de uma função, permitindo chamá-la indiretamente. São úteis para implementar callbacks, tabelas de funções e programação orientada a eventos.

Como declarar um ponteiro para função

A sintaxe básica é:

tipo_retorno (*nome_ponteiro)(tipo_param1, tipo_param2, ...);

Exemplo:
Para uma função que recebe dois inteiros e retorna um inteiro:

int (*operacao)(int, int);

Como atribuir uma função ao ponteiro

Basta usar o nome da função (sem parênteses):

int soma(int a, int b) {
return a + b;
}

operacao = soma;

Como chamar a função via ponteiro

Você pode usar tanto (*operacao)(x, y) quanto operacao(x, y):

int resultado = operacao(3, 4);  // resultado = 7

Exemplo completo

#include <stdio.h>

int soma(int a, int b) {
return a + b;
}

int subtrai(int a, int b) {
return a - b;
}

int main() {
int (*operacao)(int, int);

operacao = soma;
printf("Soma: %d\n", operacao(5, 3)); // Imprime 8

operacao = subtrai;
printf("Subtração: %d\n", operacao(5, 3)); // Imprime 2

return 0;
}

Aplicações comuns

  • Callbacks: Passar funções como argumento para outras funções.
  • Tabelas de funções: Implementar menus ou comandos dinâmicos.
  • Abstração: Permitir que funções genéricas operem sobre diferentes operações.

Dicas

  • O tipo do ponteiro deve ser compatível com o da função.
  • Use ponteiros para funções para tornar seu código mais flexível e modular.

Passagem de ponteiros para funções

Podemos ter ponteiros como parâmeros de funções. Os argumentos passados são endereços de variáveis. Trata-se de uma passagem de argumentos por referência ao invés de ser por cópia. Na função podemos modificar diretamente os valores originais dessas variáveis:

void alteraNrs(int *p1, int *p2){
    *p1 = 10;
    *p2 = 20;
 }

int main()
{
    int nr1 = 6;
    int nr2 = 14;

    alteraNrs(&nr1, &nr2);

    printf("%d - %d", nr1, nr2); //output : 10 - 20

}

Ao chamarmos a função alteraNrs() passamos os endereços das variáveis nr1 e nr2. A função altera os valores destas variáveis.