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