Como observado no exemplo acima, ponteiros e arranjos estão intimamente relacionados em C. Na verdade, qualquer referência a um arranjo é convertida internamente para uma referência do tipo ponteiro. Por este motivo, quando eficiência de tempo de acesso é uma preocupação, muitos programadores trabalham diretamente com ponteiros.
O nome de um arranjo é uma expressão do tipo ponteiro que corresponde ao endereço do primeiro elemento do arranjo. Assim, a inicialização do ponteiro no exemplo acima poderia ser reescrita como
el = arr; /* arr equivale a &arr[0] */
Observe que, uma vez que arr equivale a um ponteiro, o elemento
arr[i]
poderia ser acessado da mesma forma como *(arr+i)
. Na
verdade, é isto que o compilador irá fazer internamente: qualquer expressão
da forma E1[E2]
será internamente traduzida para
*((E1)+(E2))
. Observe que isto implica que esta operação de
indexação é comutativa, embora tal fato raramente seja utilizado em
programação C.
Por outro lado, o inverso (usar o operador de indexação com uma variável ponteiro) também é possível. Assim, o laço de atribuição no exemplo acima poderia ter sido escrito como
for (i=0; i<10; ++i) el[i] = 0;
Apesar da forma usando o operador *
ser mais eficiente,
programadores iniciantes muitas vezes acham mais simples entender o acesso
usando o operador de indexação, e acabam preferindo esta forma.
Uma diferença fundamental entre um ponteiro e o nome de um arranjo é que o
ponteiro é uma variável, enquanto que o nome de um arranjo é uma
constante. Assim, expressões como arr++
ou &arr
não
fazem sentido.
Outra diferença que deve ser ressaltada é o fato de que a declaração de um arranjo efetivamente reserva o espaço para as variáveis, enquanto que a declaração de um ponteiro reserva apenas espaço para guardar um endereço. Considere o seguinte exemplo:
/* * Exemplo do uso indevido de ponteiro */ main() { int *el; /* el: ponteiro para inteiro */ int i; /* inicializa conteudo */ for (i=0; i<10; ++i) el[i] = 0; /* onde esta el[i]? */ }Uma vez que o ponteiro el não foi inicializado, a expressão
el[i]
pode estar apontando para qualquer posição da área de memória
-- possivelmente, para alguma posição inválida. Observe que o fato de ter
declarado o ponteiro não significa que esta variável possa ser utilizada
como um arranjo. Para tal, o ponteiro deve estar com o endereço de alguma
posição válida, seja através de uma atribuição envolvendo um arranjo, seja
através do uso de rotinas de alocação dinâmica.
Uma vez que ponteiros são variáveis, nada impede que arranjos de ponteiros sejam definidos. De fato, uma declaração tal como
int *aa[10];define uma variável aa que é um arranjo de dez ponteiros para variáveis inteiras. Cada elemento deste arranjo, desde
aa[0]
até
aa[9]
, é um ponteiro para inteiro(s) que tem a mesma propriedade que
os ponteiros vistos até o momento.
Observe que esta forma suporta uma opção para trabalhar com arranjos
multidimensionais, desde que respeitadas as diferenças entre ponteiros e
arranjos. Arranjos de ponteiros trazem uma flexibilidade adicional pelo
fato de que cada ``linha'' pode ter tamanho variável. No exemplo acima,
cada ponteiro aa[i]
pode estar apontando para um inteiro, para o
primeiro elemento de um arranjo com diversos inteiros, ou mesmo para nenhum
inteiro.