Ponteiros
Um ponteiro é uma variável que armazena o endereço de outra variável. Ponteiros devem ser declarados com o símbolo *:
int *x; /* ponteiro para int */
char *s; /* ponteiro para char */
int **y; /* ponteiro para ponteiro para int */
O operador & obtém o endereço de uma variável, enquanto o operador * acessa (dereferencia) o que está no endereço de memória cujo valor é aquela expressão. Por exemplo:
int x = 5;
int *y;
y = &x; /* o valor de y passa a ser o endereço de x */
*y = 3; /* escreve 3 no endereço cujo valor é y */
printf("%d\n", x);
Imprime 3. Mesmo sem alterar x diretamente, alteramos o que está na memória em que x está armazenado, o que tem o mesmo efeito.
Note que o * tem significados diferentes quando usado na declaração (int *y) e quando usado como operador (*y = 3;): no primeiro caso, o asterisco serve apenas para denotar que a variável y é um ponteiro. Note também que os operadores & e * são como funções inversas, de modo que &*x equivale a *&x que equivale a x.
Como outras variáveis, é possível atribuir um valor a um ponteiro durante sua declaração; por exemplo, as duas linhas do exemplo acima
int *y;
y = &x;
Podem ser escritas como uma só:
int *y = &x;
Erros ao manipular ponteiros são comuns, e são a causa mais comum do famoso Segmentation fault.
Nota: em uma declaração como
int *x, y;
x é um ponteiro para int, mas y é apenas um int, não um ponteiro. Isso talvez não seja surpreendente quando a declaração é escrita assim, mas essa declaração poderia também ser escrita como
int* x, y;
Ainda assim, y continua não sendo um ponteiro.
NULL
NULL é um valor especial que indica que o ponteiro não aponta para nenhum endereço válido. É uma boa prática atribuir um ponteiro a NULL durante sua declaração.
Dereferenciar um ponteiro que aponta para NULL gera um Segmentation fault:
int *p = NULL;
int x = *p + 1; /* Segmentation fault! */
O valor NULL é quase sempre definido como 0, de modo que a condição
if(p == NULL) {
/* ... */
}
É frequentemente escrita como
if(!p) {
/* ... */
}
E da mesma forma, if(p != NULL) pode ser escrito simplesmente como if(p).
Nota: NULL é garantidamente 0 em sistemas que seguem a especificação POSIX, entre os quais Linux.
Relação com arrays
Arrays e ponteiros estão fortemente relacionados. Uma array, na verdade, guarda o endereço de seu primeiro elemento:
int arr[20];
int *p;
p = arr; /* válido: p agora aponta para o primeiro elemento de arr */
Além disso, é possível somar um valor inteiro a um ponteiro para obter o n-ésimo valor depois daquele endereço:
int arr[20];
int *p;
p = arr + 4; /* o endereço 4 ints depois do começo de arr; ou seja, o endereço do quinto elemento de arr */
Além disso, por definição, arr[i] é equivalente a *(arr + i). Desse modo, se declaramos uma array como
double arr[SIZE];
Podemos percorrer seus elementos como
int i;
for(i = 0; i < SIZE; i++) {
/* fazer algo com arr[i] */
}
Ou como
double arr[SIZE];
/* ... */
double *p;
for(p = arr; p < arr + SIZE; p++) {
/* fazer algo com *p */
}
Isso explica porque os índices das arrays em C começam em zero: arr[0] equivale a *(arr + 0), que equivale a *arr. Isso explica também porque índices negativos não funcionam em C da mesma forma que em Python: arr[-4] é o endereço do quarto elemento antes do começo da array. Por último, isso explica porque &arr[2] é equivalente a arr + 2, pois &arr[2] -> &*(arr + 2) -> arr + 2.
Diferenças com arrays
Arrays e ponterios são semelhantes, mas não idênticos. Primeiramente, não é possível mudar o endereço que a array guarda:
int arr[20];
int x;
int *p;
p = &x; /* OK */
arr = &x; /* ERRO! */
p++; /* OK */
arr++; /* ERRO! */
Mas a diferença mais importante aparece ao usar o operador sizeof:
int arr[20];
int *p = arr;
printf("%d %d\n", sizeof(arr), sizeof(p));
Imprime valores diferentes: sizeof(arr) é igual a 20 * sizeof(int),
enquanto sizeof(p) corresponde a sizeof(int*).
Alocação dinâmica
Arrays possuem uma limitação séria: seu tamanho deve ser estático e conhecido em tempo de compilação. Por exemplo, o código a seguir não é válido:
size_t size;
/* leia size de algum lugar */
int p[size]; /* ERRO! */
Para contornar essa limitação, usa-se a alocação dinâmica de memória, por meio das funções malloc (ou calloc) e free, declaradas em stdlib.h.malloc recebe um tamanho em bytes, aloca uma região de memória com esse tamanho, e retorna um ponteiro para o começo dessa região, ou NULL em caso de erro. Essa é uma forma válida de fazer o que tentamos no exemplo anterior:
size_t size;
/* leia size de algum lugar */
int *p = malloc(size * sizeof(int)); /* espaço para size ints */
if(p == NULL) {
/* tratamento de erro */
}
Há também a função calloc, que recebe dois parâmetros: o número de elementos e o tamanho de cada elemento, e também retorna uma região de memória com o tamanho necessário - mas os bytes dessa região são garantidamente inicializados para 0, o que livra o programador da possibilidade de problemas relacionados a memória não-inicializada. O exemplo anterior, usando calloc:
size_t size;
/* leia size de algum lugar */
int *p = calloc(size, sizeof(int));
if(p == NULL) {
/* tratamento de erro */
}
Após uma chamada a qualquer uma das duas funções, a memória alocada deve ser liberada pela função free:
int *p = calloc(size, sizeof(int));
/* ... */
free(p);
p = NULL; /* boas práticas; veja abaixo */
Alguns erros podem surgir pelo uso indevido de free:
- caso
freenunca seja chamada para uma região de memória alocada, temos um memory leak; a memória só será recuperada pelo Sistema Operacional quando o processo terminar. - caso a região alocada seja acessada depois de uma chamada a
free, temos um caso de use-after-free, que pode ser explorado como falha de segurança. Para evitar isso, é prudente atribuir a NULL todo ponteiro apontando para aquela região depois de chamarfree. - caso
freeseja chamada duas vezes para a mesma região de memória, temos um double-free, que é um caso de corrupção de memória e normalmente faz com que o programa termine.
Nota: o tamanho de uma região de memória alocada dinamicamente não pode ser obtido com sizeof:
int *p = malloc(num * sizeof(int));
printf("%d", sizeof(p));
free(p);
Imprime o valor de sizeof(int*), indepedentemente de quantos bytes foram alocados por malloc. Novamente, isso ocorre porque p é um ponteiro, não uma array.
Nota: é comum ver casts depois de uma chamada a malloc ou calloc:
int *p = (int*) malloc(num * sizeof(int));
Porém, esse cast não é necessário, pois malloc tem tipo de retorno void*, que pode ser implicitamente convertido para qualquer outro tipo de ponteiro (vide seção void*).
(Nomenclatura: há quem chame a região de memória alocada por malloc e similares de "array alocada dinamicamente", mas evitamos chamar isso de array, porque o que temos de fato é um ponteiro.)