Ponteiros

O objetivo dessa unidade é apresentar o conteúdo relacionado a ponteiros. Será feita uma abordagem focando na linguagem C.

Introdução

Ponteiros são um dos recursos mais poderosos da linguagem C. Qualquer programa de utilidade prática escrito em C dificilmente dispensará o seu uso. A tentativa de evitá-los implicará quase sempre em códigos maiores e de execução mais lenta.

Ponteiros ou apontadores são variáveis que armazenam o endereço de memória de outras variáveis. Dizemos que um ponteiro “aponta” para uma variável quando contém o endereço da mesma.

Os ponteiros podem apontar para qualquer tipo de variável. Portanto, temos ponteiros para int, float, double etc.

Por que usar ponteiros?

Ponteiros são muito úteis quando uma variável tem que ser acessada em diferentes partes de um programa. Neste caso, o código pode ter vários ponteiros espalhados por diversas partes do programa, “apontando” para a variável que contém o dado desejado. Caso este dado seja alterado, não há problema algum, pois todas as partes do programa têm um ponteiro que aponta para o endereço onde reside o dado atualizado.

Declaração de um ponteiro

Para declarar um ponteiro em C basta colocarmos um asterisco - * - antes do nome desse ponteiro.

Exemplos:

Como visto acima ponteiros são variáveis que armazenam endereço, e endereço são apenas números, por que então declarar ponteiros com os tipos (int, float, char etc)?

Cada variável ocupa um determinado tamanho na memória, além disso as posições ocupadas por ela são vizinhas e contíguas (em sequência), com exceção do tipo char (ocupa só 1 byte, ou seja, só um bloco).

Um ponteiro irá sempre armazenar o endereço do primeiro bloco (primeiro byte) e também saberá quantos bytes cada variável ocupa, ou seja, quais são blocos vizinhos de memória que pertencem a determinada variável.

Exemplos

No exemplo abaixo declaramos uma variável int nome e um ponteiro int ptr_nome. A variável inteira ocupa 4 bytes de memória, ou seja, 4 blocos de memória, cada bloco com um endereço.

Suponha que o endereço do primeiro bloco de nome seja 4052. Caso apontarmos o ponteiro ptr_nome para nome, em ptr_nome ficará armazenado 4052.

Porém, por ser também um int, ptr_nome saberá que os próximos 3 blocos de memória pertencem à variável nome, ou seja, os endereços 4053, 4054 e 4055.

Inicializando um ponteiro

Para atribuir um valor a um ponteiro recém-criado poderíamos igualá-lo a um valor de memória. Mas, como saber a posição na memória de uma variável do nosso programa? Seria muito difícil saber o endereço de cada variável que usamos, mesmo porque estes endereços são determinados pelo compilador na hora da compilação e realocados na execução. Podemos então deixar que o compilador faça este trabalho por nós. Para saber o endereço de uma variável basta usar o operador &. Veja o exemplo:

Criamos um inteiro count com o valor 10 e um apontador para um inteiro pt. A expressão &count nos dá o endereço de count, o qual armazenamos em pt. Simples, não é? Repare que não alteramos o valor de count, que continua valendo 10.

Como nós colocamos um endereço em pt, ele está agora "liberado" para ser usado. Podemos, por exemplo, alterar o valor de count usando pt. Para tanto vamos usar o operador "inverso" do operador &. É o operador *.

O operador * possui 2 utilizações distintas no uso de ponteiros.

Se utilizado na declaração de uma variável, indica que a mesma é um ponteiro. Durante a implementação do programa seu uso é referente à manipulação do conteúdo da variável.

O operador & é utilizado para a manipulação do endereço de memória

No exemplo acima, uma vez que fizemos pt=&count a expressão *pt é equivalente ao próprio count (conteúdo da variável). Isto significa que, se quisermos mudar o valor de count para 12, basta fazer *pt=12.

Operações com Ponteiros

IGUALAR 2 PONTEIROS.

Se temos dois ponteiros p1 e p2 podemos igualá-los fazendo p1=p2. Repare que estamos fazendo com que p1 aponte para o mesmo lugar que p2. Se quisermos que a variável apontada por p1 tenha o mesmo conteúdo da variável apontada por p2 devemos fazer:

INCREMENTO e DECREMENTO.

Quando incrementamos um ponteiro ele passa a apontar para o próximo valor do mesmo tipo para o qual o ponteiro aponta. Isto é, se temos um ponteiro para um inteiro e o incrementamos ele passa a apontar para o próximo inteiro. Esta é mais uma razão pela qual o compilador precisa saber o tipo de um ponteiro: se você incrementa um ponteiro char* ele anda 1 byte na memória e se você incrementa um ponteiro double* ele anda 8 bytes na memória. O decremento funciona de modo semelhante. Supondo que p é um ponteiro, as operações são escritas como:

Lembrando que estamos falando de operações com ponteiros e não de operações com o conteúdo das variáveis para as quais eles apontam. Para incrementar ou decrementar o conteúdo da variável apontada pelo ponteiro p, faz-se:

SOMA e SUBTRAÇÃO

Vamos supor que você queira incrementar um ponteiro de 15. Basta fazer uma das opções abaixo:

E se você quiser usar o conteúdo do ponteiro 15 posições adiante:

A subtração funciona da mesma maneira.

COMPARAÇÃO

Bem, em primeiro lugar, podemos saber se dois ponteiros são iguais ou diferentes (== e !=), ou seja, se apontam para a mesma posição na memória ou posições diferentes. No caso de operações do tipo >, <, >= e <= estamos comparando qual ponteiro aponta para uma posição mais alta na memória. Então uma comparação entre ponteiros pode nos dizer qual dos dois está "mais adiante" na memória. A comparação entre dois ponteiros se escreve como a comparação entre outras duas variáveis quaisquer:

Há, entretanto, operações que você não pode efetuar num ponteiro. Você não pode dividir ou multiplicar ponteiros, adicionar dois ponteiros, adicionar ou subtrair floats ou doubles de ponteiros.

Impressão de Ponteiros

Em C, pode-se imprimir o valor armazenado no ponteiro (um endereço), usando-se a função printf com o formatador %p na string de formato. Por exemplo:

Já para acessar o conteúdo de uma posição de memória, cujo endereço está armazenado em um ponteiro, usa-se o operador de derreferência (*). Por exemplo:

Video Explicativo - Ponteiros 1

Ponteiros para vetores

Ponteiros oferecem um eficiente e prático meio de acesso e manipulação dos elementos de um vetor. Sejam, por exemplo, um vetor e um ponteiro:

A linha acima inicializa o ponteiro, associando-o ao primeiro elemento do vetor.

E a figura abaixo dá uma ideia gráfica da operação.

Obs: o valor 4052 é meramente ilustrativo. Depende da localização de memória onde o programa é carregado.

Uma vez apontado para o primeiro elemento do vetor, o valor deste último pode ser acessado ou modificado via ponteiro de forma idêntica à já vista para variáveis simples.

É fácil concluir que os demais elementos do vetor podem ser acessados via incremento ou decremento do ponteiro, uma vez que ele contém endereço.

Considera-se, agora, que ptr é incrementado conforme abaixo

Se fosse uma variável comum, o seu conteúdo seria simplesmente aumentado de uma unidade com o operador ++. No caso de ponteiro, o conteúdo é incrementado do número de bytes correspondente a uma variável do tipo para o qual ele aponta.

Neste exemplo, desde que ptr é do tipo int (que usa 4 bytes), o seu endereço real passa de 4052 a 4056, ou seja, aponta para o segundo elemento do vetor. Assim, todos os seus elementos podem ser apontados via incremento ou decremento do ponteiro (ptr++ ou ptr--, por exemplo).

Se, em vez de int, os dados e ponteiro fossem, por exemplo, tipo double (8 bytes), cada incremento ou decremento unitário do ponteiro fariam seu valor mudar de 8 bytes, permitindo o correto acesso a cada elemento. Isso significa que o programador não precisa se preocupar com o real conteúdo do ponteiro. Basta considerar as correspondências unitárias, isto é, se o ponteiro é incrementado ou decrementado de um, ele aponta para o próximo elemento ou para o elemento anterior.

Cabe, entretanto, ao programador manter o ponteiro dentro dos limites do vetor. Se ultrapassar e o valor for modificado, resultados imprevisíveis podem ocorrer. Em geral, os compiladores não verificam isso.

A inicialização do ponteiro foi proporcionada pela linha já vista:

Mas também poderia ter sido feita da seguinte forma tendo o mesmo efeito:

Ponteiros para matriz

Da mesma forma que os vetores, também podemos fazer uma varredura sequencial em uma matriz utilizando um ponteiro. Vejamos:

Considere o seguinte programa para zerar uma matriz:

Podemos reescrevê-lo usando ponteiros:

No primeiro programa, cada vez que se faz matriz[i][j], com base nos valores de i e j, o programa calcula o deslocamento para dar ao ponteiro. Ou seja, o programa tem que calcular 2500 deslocamentos. No segundo programa o único cálculo que deve ser feito é o de um incremento de ponteiro. Fazer 2500 incrementos em um ponteiro é muito mais rápido que calcular 2500 deslocamentos completos.

Ponteiros para estruturas

Ponteiros para estruturas são escritos de forma similar aos anteriores. A principal diferença é o uso do operador -> para acesso aos membros. Considere o exemplo a seguir:

No programa acima, após a declaração da estrutura telefone, é definido um ponteiro do tipo telefone (ptr) de forma similar aos ponteiros de outros tipos.

A linha ptr = &pedro; inicializa o ponteiro ptr. Na sequência na linha abaixo podemos observar o uso do operador de ponteiro para estrutura como argumento de printf.

Essa declaração tem o mesmo efeito de pedro.nome quando não se utiliza ponteiro.

Ponteiros para ponteiros

Note que um ponteiro é uma variável como outra qualquer, e por isso também ocupa espaço em memória. Para obtermos o endereço que um ponteiro ocupa em memória, usamos o operador &, assim como fazemos nas variáveis comuns. Mas e se estivéssemos interessados em guardar o endereço de um ponteiro, que tipo de váriavel deveria recebê-lo? A resposta é: um ponteiro, isto é, um ponteiro para outro ponteiro.

Como já sabemos, para declarar um ponteiro, deve-se verificar o tipo da variável que ele irá apontar e colocar um asterisco entre o tipo da variável e o nome do ponteiro. Agora, para se guardar o endereço de um ponteiro, os mesmos passos devem ser seguidos. Primeiramente, verificamos o tipo da variável que será apontada (int *) e colocamos um asterisco entre o tipo e nome do ponteiro. Vejamos:

Primeiramente, declaramos uma variável int x e a inicializamos, depois declaramos um ponteiro(p_x) e o fizemos apontar para x . Por fim, declaramos um ponteiro que irá apontar para p_x, ou seja, um ponteiro para ponteiro. Note que C não impõe limites para o número de asteriscos em uma variável.

No exemplo a seguir, todos os printf escreverão o mesmo conteúdo na tela.

Ponteiros como parâmetros de funções

Vamos começar esse tópico por uma situação-problema: eu tenho 2 variáveis e quero trocar o valor delas. Vamos começar com um algoritmo simples, dentro da função main():

Esse exemplo funcionará exatamente como esperado: primeiramente ele imprimirá "5 10" e depois ele imprimirá "10 5". Mas e se quisermos trocar várias vezes o valor de duas variáveis? É muito mais conveniente criar uma função que faça isso. Vamos fazer uma tentativa de implementação da função swap (troca, em inglês):

No entanto, o que queremos não irá acontecer. Você verá que o programa imprime duas vezes "5 10". Por que isso acontece? Lembre-se do escopo das variáveis: as variáveis a e b são locais à função main( ), e quando as passamos como argumentos para swap(), seus valores são copiados e passam a ser chamados de i e j; a troca ocorre entre i e j, de modo que quando voltamos à função main() nada mudou.

Então como poderíamos fazer isso? Como são retornados dois valores, não podemos usar o valor de retorno de uma função. Mas existe uma alternativa: os ponteiros!

Neste exemplo, definimos a função swap( ) como uma função que toma como argumentos dois ponteiros para inteiros; a função faz a troca entre os valores apontados pelos ponteiros. Já na função main( ), passamos os endereços das variáveis para a função swap(), de modo que a função swap( ) possa modificar variáveis locais de outra função. O único possível inconveniente é que, quando usarmos a função, teremos de lembrar de colocar um & na frente das variáveis que estivermos passando para a função.

Se você pensar bem, já vimos uma função em que passamos os argumentos precedidos de &: é a função scanf( )! Por que fazemos isso? É simples: chamamos a função scanf( ) para que ela atribua às nossas variáveis valores digitados pelo usuário. Ora, essas variáveis são locais, e portanto só podem ser alteradas por outras funções através de ponteiros!

Quando uma função recebe como parâmetros os endereços e não os valores das variáveis, dizemos que estamos fazendo uma chamada por referência; é o caso desse último exemplo. Quando passamos diretamente os valores das variáveis para uma função, dizemos que é uma chamada por valor; foi o caso do segundo exemplo.

Video Explicativo - Ponteiros 2

Exercícios