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

Para quem está começando, pode parecer (e algumas vezes é) um tanto difícil. Mas não há outro caminho senão enfrentar a realidade.

Tópicos: O que os ponteiros permitem fazer? | O que é ponteiro? | Declarando ponteiros | Inicializando um ponteiro | Ponteiros para arrays | Ponteiros e strings | Ponteiros de estruturas | Ponteiros como argumentos de funções | Ponteiros para funções |
O que os ponteiros permitem fazer?  (início da página)
É difícil enumerar tudo. Abaixo os usos mais comuns.

» Acessar endereços de memória que o programa aloca em tempo de execução.
» Acessar variáveis que não são visíveis a uma função.
» Manipulação de arrays usando ponteiros para seus elementos, evitando a forma direta.
» Manipulação de strings.
» Passar o endereço de uma função para outra.
» Retornar mais de um valor de uma função.
O que é ponteiro?  (início da página)
A definição é simples: Ponteiro é uma variável que contém o endereço de um objeto de dados, normalmente uma outra variável. Daí o nome, pois ele aponta para outra variável.
Declarando ponteiros  (início da página)
Conforme qualquer variável em C, o ponteiro deve ser declarado antes de ser usado. Basta colocar o operador indireto (*) após o tipo conforme se segue:
int *ptr;
E teremos a variável ptr definida como um ponteiro para uma variável do tipo int.

Lembrar que a declaração de ponteiro não tem o mesmo significado da de uma variável. Ela apenas indicará o tipo de objeto de dados que será apontado e, desde que contém endereço de memória, o tamanho em bytes que ocupa não tem relação com o do objeto apontado mas é fixo e depende apenas do modelo de memória do sistema (2 bytes ou 4 bytes, normalmente).

Se for declarar mais de uma variável por linha, usar o (*) antes de cada. Exemplo:
char *ch1, *ch2; e ch1 e ch2 serão ponteiros para o tipo char.
Se for omitido conforme char *ch1, ch2; a variável ch2 não será ponteiro mas sim, uma variável comum tipo char e certamente provocará erro se usada como ponteiro.

O ponteiro pode ser declarado para qualquer tipo legal de variável em C (char, int, float, double, etc), além de void, que seria um genérico, podendo apontar para qualquer tipo de dado.
Inicializando um ponteiro  (início da página)
Uma vez declarado, o ponteiro deve ser associado ao endereço da variável desejada para que possa ser usado. Veja exemplo abaixo (é suposto que as linhas sejam executadas dentro de uma função).
int var;
int *ptr;
var = 10;
ptr = &var;


A última linha associa o ponteiro ptr ao endereço da variável var. Para isto é usado o operador de endereçamento (&).

A Fig 1 dá uma noção gráfica desta relação.
Está considerado que o endereçamento do sistema é dado por 2 bytes.
Assim ptr ocupará 2 bytes e var também 2 por ser uma variável int.

O número 4052 da posição de memória do primeiro byte de var é apenas ilustrativo e, na prática, dependerá do local da memória onde o programa foi carregado. Mas supomos que seja este. Portanto, após a execução de ptr = &var; o conteúdo de ptr será 4052, ou seja, o primeiro byte da variável apontada.

Com ptr apontando para var, podemos ler ou modificar o valor desta última de forma indireta através de ptr. Exemplo: se continuarmos as linhas anteriores com esta
int newVar = *ptr; o valor de newVar será 10, isto é, o valor de var. E, se continuarmos
*ptr = 20; o valor de var passará a ser 20, ou seja, modificado através de ptr.

É importante lembrar que um ponteiro declarado mas não inicializado poderá ter conteúdo nulo ou aleatório, a depender de onde foi alocado pelo sistema. Nesta condição, se o conteúdo apontado for modificado conforme linha anterior, poderá afetar variáveis de sistema ou de outros programas com conseqüências imprevisíveis.
Ponteiros para arrays  (início da página)
Ponteiros oferecem um eficiente e prático meio de acesso e manipulação dos elementos de uma array. Seja uma globalmente declarada conforme abaixo.
int ar[] = {10, 50, 20, 30};
E o ponteiro
int *ptr;

E se, dentro de uma função fizermos
ptr = &ar[0];

Quando esta for executada, ptr estará associado ao primeiro byte da array conforme Fig 2 ao lado.
Similar ao item anterior, o valor de 4052 é meramente ilustrativo.
Aritmética do ponteiro

Desde que que ptr foi apontado para o início da array (elemento 0), o endereçamento indireto *ptr poderá obter ou modificar o valor deste elemento, similar item anterior.

E para os demais elementos? Considerando que ptr está apontado para o elemento 0, basta incrementá-lo de uma unidade para movê-lo para o elemento 1, ou seja, ptr++; ou ptr += 1; Esta operação fará o conteúdo de ptr 4054, que é o endereço do elemento 1.

Assim, na aritmética dos ponteiros, os compiladores ajustam os endereços de acordo com o tamanho dos elementos da array e você não precisará se preocupar com isto (Exemplo: se em vez de int os dados fossem do tipo float, o endereço seria incrementado de 4 bytes).

Estando ptr apontado para o elemento 1, se você desejar retornar ao 0, basta dar um decremento, ptr--; ou ptr -= 1;

Em resumo podemos dizer que a simples soma ou subtração ao ponteiro permite o acesso a qualquer elemento. Entretanto, cabe a quem programa cuidar para que o ponteiro não ultrapasse os limites da array. Caso contrário, o programa poderá ficar corrompido, com resultados imprevisíveis. Os compiladores, normalmente, não verificam isso.

Uma outra sintaxe para iniciar

No início deste item foi usada a seguinte linha para inicializar o ponteiro:
ptr = &ar[0]; Mas poderia ser perfeitamente desta forma:
ptr = ar;
Isto significa que o nome de uma array é, na realidade, um ponteiro e pode ser usado da mesma forma.
Ponteiros e strings  (início da página)
Desde que uma string é uma array de elementos tipo char, as operações ocorrem conforme item anterior mas aqui estão algumas notações mais elegantes para simplificar a sintaxe. Seja, por exemplo o seguinte programa:
#include <stdio.h>
main(){
int i;
char rua[] = "nova";
char *ptr = rua;
for (i=0; i<4; i++)
     printf( "%d> %c\n", i, *ptr++ );
}
A saída seria:

0> n
1> o
2> v
3> a
 
Strings têm um caractere nulo após o último elemento. Assim uma versão mais elegante do programa seria:
#include <stdio.h>
main(){
int i = 0;
char rua[] = "nova";
char *ptr = rua;
while( *ptr )
     printf( "%d> %c\n", i++, *ptr++ );
}
A saída seria a mesma do anterior.

Entretanto o loop while permite o seguinte artifício: 

*ptr irá parar o loop quando chegar no caractere nulo no final da string.
Agora uma outra versão do programa:
#include <stdio.h>
main(){
int i;
char rua[] = "nova";
char *ptr = rua;
for (i=0; i<4; i++)
     printf( "%d> %c\n", i, *(rua+i) );
}
É quase idêntico ao primeiro mas, no último argumento de printf foi usado *(rua+i) em lugar de *ptr++.

Isso porque, conforme item anterior, o nome de uma array é também um ponteiro para a mesma e pode ser usado como tal.
E também poderia usar o clássico rua[i].

As notações *ptr++ (isto é incrementado para cada i), *(rua+i) e rua[i] se equivalem e podem ser usadas sem distinção. Entretanto, muitos programadores preferem a notação como ponteiro por ser mais rápida em certas condições. Mas não é só isso. Depois de você se habituar com os ponteiros e de fazer uso intensivo deles, você irá certamente preferir a notação como ponteiro para dar uma aparência mais homogênea aos seus códigos.
Ponteiros de estruturas  (início da página)
Ponteiros para estruturas são escritos de forma similar aos já mencionados. A principal diferença é o uso do operador -> para acesso aos membros das mesmas. Veja exemplo a seguir.
#include <stdio.h>

struct telelista{
    char nome[15];
    char ddd[2];
    char num[10];
};
struct telelista *ptr;
main(){

struct telelista pedro = {
    "Pedro da Silva", "99", "123-456"};

ptr = &pedro;
printf( "%s", ptr->nome );
}
No programa ao lado, após a declaração da estrutura, é declarado o ponteiro ptr para a mesma, de forma similar ao de outras variáveis.

Dentro da função main(), depois de definidos membros para a variável pedro, o ponteiro ptr é associado à mesma.

Veja então o segundo argumento da função printf, ptr->nome que aponta para o membro nome da estrutura (tem o mesmo efeito da notação usual pedro.nome)

Os demais membros seriam acessados de forma similar (ptr->ddd e ptr->mum).
Ponteiros como argumentos de funções  (início da página)
Quando uma função recebe uma variável como argumento, ela tem apenas uma cópia do valor desta variável. Assim, se esta variável é externa à função, esta não pode modificar seu valor. Mas se o endereço  (isto é, um ponteiro) da variável for passado à função, não haverá dificuldade na modificação.

A idéia também permite que uma função possa retornar mais de um valor. Veja exemplo no programa abaixo.
#include <stdio.h>
void dobrar (int*, int*);

main(){

int a = 2, b = 4;
dobrar(&a, &b);
printf("a=%d b=%d", a, b);

}

void dobrar(int *ptr0, int *ptr1){
    *ptr0 *= 2;
    *ptr1 *= 2;
}
A saída deste programa seria:

a=4 b=8

Ou seja, as variáveis a e b, externas à função dobrar, foram modificadas pela mesma e a função retornou mais de um valor.

Um ganho que se obtém ao se passar ponteiros para uma função é de desempenho quando os argumentos são objetos grandes como arrays e estruturas. Lembrar que a função recebe uma cópia do argumento. Assim, ao se passar um endereço, é eliminada a operação de copiar todo o objeto.
Ponteiros para funções  (início da página)
Pelo que foi até então exposto, é lícito supor que ponteiros podem se referir a qualquer objeto localizado na memória. Tais objetos incluem também as funções e, portanto, é possível criar um ponteiro para uma função.

A sintaxe é semelhante mas tem algumas regras próprias conforme programa exemplo abaixo.
#include <stdio.h>

main(){

int (*ptrf) ();
ptrf = printf;

(*ptrf) ("Teste de ponteiro");

}
A saída do programa seria:
Teste de ponteiro
Ou seja, a função printf foi chamada através do ponteiro ptrf.

Deve se observada a sintaxe indicada. Inclusive o tipo de dado na declaração do ponteiro int (*ptrf) (); deve ser o mesmo retornado pela função (int porque printf retorna int).
Ponteiros de funções permitem procedimentos avançados como, por exemplo, a criação de uma tabela de funções e a execução de cada uma de acordo com um determinado critério