Ponteiros são representações de endereços na memória do computador. Eles constituem um recurso de programação de baixo nível, que espelha a representação de estruturas de dados em memórias de computadores. Seu uso é devido em grande parte a eficiência (ou seja, tem o objetivo de fazer com que programas sejam executadas rapidamente ou usando poucos recursos de memória) ou, algumas vezes, a necessidade de acesso ao hardware. O uso de ponteiros poderia ser em grande parte das vezes ser substituído por uso de um estilo de programação baseado em abstrações mais próximas do domínio do problema e não da implementação de uma solução do problema em um computador, abstrações essas tanto de dados quanto de definição de funções sobre esses dados.
O uso de ponteiros deve ser feito com cuidado, para evitar erros em tempo de execução que podem ser difíceis de entender e de corrigir.
Uma variável é um lugar da memória ao qual foi dado um nome (o nome da variável). Toda variável tem um endereço, que é o endereço do lugar da memória que foi alocado para a variável. Um ponteiro é um endereço (da memória), ou um tipo (de valores que são ponteiros para variáveis de um determinado tipo; por exemplo, o tipo int * é um tipo ponteiro, de valores que são ponteiros para variáveis de tipo int; dizemos apenas: tipo “ponteiro para int”). Além disso, quando o contexto deixar claro, usamos também ponteiro para denotar variável que contém um endereço.
Um tipo ponteiro é indicado pelo caractere * como sufixo de um tipo, indicando o tipo ponteiro para áreas de memória deste tipo. Por exemplo:
|
declara uma variável de nome p e tipo int *, ou seja,
ponteiro para int.
O caractere * é considerado em um comando de declaração de variáveis em C como qualificador da variável que o segue, de modo que, por exemplo:
|
declara um ponteiro p e uma variável q de tipo int,
e não dois ponteiros.
O uso de ponteiros é baseado principalmente no uso dos operadores
& e *, e da função malloc.
O operador &, aplicado a uma variável, retorna o endereço dessa variável. Por exemplo, a sequência de comandos:
|
declara p como uma variável de tipo int *, q como
uma variável de tipo int e armazena o endereço de q em
p.
O operador * é um operador chamado de “derreferenciação”:
aplicado a um ponteiro p, o resultado é a variável apontada por
p (se usado como um valor, o resultado é o valor contido na
variável apontada por p).
O uso de * em uma declaração significa declaração de um ponteiro; o uso de * precedendo um ponteiro em uma expressão significa “derreferenciação”.
Considere, por exemplo, o problema de trocar o conteúdo dos valores
contidos em duas variáveis a e b de tipo int. Para
fazer isso precisamos passar para uma função troca o endereço
das variáveis, i.e. devemos chamar troca(&a,&b), onde a função
inatroca é definida como a seguir:
|
Note que uma chamada troca(a,b) em vez de troca(&a,&b)
fará com que a execução do programa use endereços de memória que são
valores inteiros contidos em a e b, e isso fará com que o
programa provavelmente termine com um erro devido a tentativa de
acesso a endereço inválido de memória.
Note também que a função scanf modifica o valor de uma variável,
e é por isso que o endereço da variável é que deve ser passado como
argumento de scanf.
Ocorre um erro durante a execução de um programa quando um ponteiro é “derreferenciado” e o ponteiro representa um endereço que não está no conjunto de endereços válidos que o programa pode usar. Esse erro é comumente chamado em inglês de segmentation fault, ou seja, erro de segmentação. Isso significa que foi usado um endereço que está fora do segmento (trecho da memória) associado ao processo que está em execução.
O endereço 0 é também denotado por NULL (definido em
stdlib) e é usado para indicar um “ponteiro nulo”, que não é
endereço de nenhuma variável. É em geral também usado em
inicializações de variáveis de tipo ponteiro para as quais não se sabe
o valor no instante da declaração.
Em C, é possível incrementar (somar um a) um ponteiro. Sendo p
um ponteiro para valores de um tipo t qualquer, p+1
representa o endereço que está n posições de memória seguintes à
posicão do endereço denotado por p” e, analogamente, p-1
representa o “endereço que está n posições de memória anteriores ao
endereço denotado por p”, onde n é o tamanho do tipo t
(i.e. o número de unidades de memória ocupado por variáveis do tipo
t).
Assim, é possível realizar operações aritméticas quaisquer com um ponteiro e um inteiro, levando em conta, é claro, o tamanho do tipo do valor denotado por um ponteiro: somar o inteiro 1 a um ponteiro significa somar um valor ao ponteiro igual ao tamanho do tipo desse ponteiro (i.e. o número de unidades de memória ocupado por variáveis desse tipo).
Por exemplo, um valor inteiro ocupa, em muitas implementações, 4 bytes
(32 bits). Nessas implementações, se p é do tipo *int
(isto é, é um ponteiro para variáveis de tipo int), p+1
representa um endereço 4 bytes maior do que o endereço denotado por
p (se uma unidade de memória é igual a 4 bytes, então nesse caso
p+1 representa de fato somar 1 a p).
Em C, o nome de uma variável de tipo arranjo pode ser usado como um
ponteiro para a primeiro posição do arranjo, com a diferença que o
valor dessa variável não pode ser modificado (a variável de tipo
arranjo corresponde uma variável de tipo ponteiro declarada com o
atributo const).
Por exemplo, quando se declara o arranjo:
|
um arranjo de 10 posições é alocado e o nome a representa um
ponteiro para a primeira posição desse arranjo. Ou seja, o nome
a representa o mesmo que &a[0].
Sendo i uma expressão qualquer de tipo int, a expressão
a[i] denota o mesmo que (a+i) — ou seja, a i-ésima
posição do arranjo a, i posições depois da 0-ésima
posição. Se usada em um contexto que requer um valor, essa expressão é
“derreferenciada”, fornecendo o valor *(a+i). Portanto, em
C a operação de indexação de arranjos é expressa em termos de uma
soma de um inteiro a um ponteiro.
Assim, quando um arranjo a é passado como argumento de uma
função f, apenas o endereço &a[0] é passado.