O operador unário & dá o "endereço de uma variável". Por sua vez, o operador unário * dá-nos o "conteúdo de um objecto apontado por um apontador". Para definir um apontador para uma variável de um determinado tipo, pode, por exemplo, usar-se a seguinte declaração:
int *pointer;Nota: É sempre necessário associar um tipo a um apontador. Assim, por exemplo, um apontador para um inteiro fica diferente de um apontador para um longo.
Considere-se o efeito do código seguinte:
int x = 1, y = 2;Vale a pena considerar o que passa ao nível da memória da máquina onde é executado. Vamos assumir que a variável x se encontra armazenada na posição de memória 100, y na posição 200 e ip na posição 1000. Note-se que um apontador é uma variável como as outras e os valores que lhe estão associados têm de ser armazenados algures na memória. É a natureza dos valores que se armazenam nos apontadores que é novo. Veja-se a figura seguinte:
int *ip;ip = &x;
y = *ip;
x = (int) ip;
*ip = 3;
As atribuições x=1
e y=2 obviamente colocam esses valores
nas posições de memória das variáveis x
e y. ip
é declarado como sendo um apontador para um inteiro e aí
é colocado o endereço da variável x
(&x). Assim ip
fica com o valor 100.
A seguir, a variável y
fica com o valor da posição de memória cujo endereço
se encontra em ip. No exemplo ip
aponta para a posição de memória 100 - que é
o endereço de x. Assim y
fica com o valor de x, que é
1.
Já vimos que o C permite conversões de tipos. Assim é
possível converter um apontador num inteiro (se tiverem o mesmo
tamanho - em geral 32 bits). O valor de ip
na 3ª instrução é transferido para x.
Finalmente transfere-se para o local apontado por ip
o valor 3. Como ip contém
o endereço de x é para
aí que vai parar o valor 3.
IMPORTANTE: Quando se declara um apontador ele não aponta para lugar nenhum. .
Assim ...
int *ip;... poderá gerar um erro. (Um crash do programa!).*ip = 100;
Um uso correcto, poderia ser, por exemplo:
int *ip, x;O conteúdo da posição de memória apontada pelo apontador pode ser usado normalmente em expressões aritméticas:ip = &x;
*ip = 100;
float *flp, *flq;Como já se disse um apontador é um endereço de memória, que é representado por um valor inteiro, mas um apontador NÃO é um inteiro (int). Ainda assim são possíveis certas operações aritméticas directamente com os apontadores. Uma das razões porque é necessário indicar o tipo da variável apontada por um apontador é permitir essas operações. Por exemplo, quando se incrementa um apontador, isso, em geral, não corresponde a somar 1 ao endereço que ele contém, mas sim o número de bytes que ocupa o tipo de valor para onde aponta. O incremento corresponde assim ao endereço na memória do próximo valor do mesmo tipo.*flp = *flq + 10;
++*flp; /* Incremento de (*flp) */
(*flq)++; /* Diferente de *flq++ */
flq = flp; /* Aqui transfere-se o conteúdo de um apontador (endereço de float) para o outro */
Apenas para um apontador para char corresponde a operação ++char_ptr a incrementar 1 ao valor que está em char_ptr.
Para apontadores para int's (de 32 bits) e float's, ++ptr corresponde a somar 4 ao valor contido em ptr.
Considere-se um float (fl) e um apontador para float (flp), como se mostra na próxima figura.
Se flp apontar para fl,
quando se incrementa flp (++flp)
ele passa a apontar para uma posição 4 bytes mais à
frente (4 bytes é o tamanho de um float).
Se se adicionasse 2 (flp + 2) ao
valor original de flp (&fl),
ele passaria a apontar para um endereço 8 bytes mais à frente
(ou seja 2 float's mais à
frente).
Quando se passam argumentos para funções em C, estes são passados por valor (excepto os arrays, como veremos); ou seja, é criada uma cópia do argumento no stack do processador e é essa cópia que chega à função; quando a função termina essa cópia desaparece. No entanto há situações em que a passagem de argumentos por valor não é muito conveniente; por exemplo, quando queremos que modifições feitas aos argumentos no interior das funções cheguem a quem as chamou, ou quando necessitamos de passar argumentos de tamanho muito elevado (estruturas com muitos membros). Certas linguagens permitem então a passagem de parâmetros de outra forma - passagem por referência - em que a informação que chega à função é apenas o endereço do local na memória onde se encontra o valor do argumento (p. exemplo, argumentos declarados como var no Pascal). No entanto o C não permite esse mecanismo sendo necessário o uso explícito de apontadores para o implementar. (Nota: o C++ contém já esse mecanismo).
Por exemplo, se quizessemos escrever uma função para trocar o conteúdo de duas variáveis, a forma habitual não funcionaria:
void swap(int a, int b)
{
int t;
t = a; a = b; b = t;
}
com a chamada swap(i, j);
Para funcionar teríamos de recorrer a apontadores e ao operador & de obtenção do endereço de uma variável:
void swap(int *a, int *b)
{
int t;
t = *a; *a = *b; *b = t;
}
com a chamada swap(&i, &j);
É possível também retornar apontadores como resultado de uma função, usado geralmente quando se pretende retornar informação que ocupe um grande espaço, como as estruturas:
typedef struct { float x, y, z; } coord;
coord *coor_fn(void);
void main(void)
{
coord p1;
...
p1 = *coord_fn();
...
}
coord *coor_fn(void)
{
coord p;
p = ... ;
return &p;
}
Aqui retorna-se um apontador cujo conteúdo é imediatamente
de-referenciado para uma variável do tipo correspondente. Deve fazer-se
esta transferência de imediato uma vez que a variável p
é local à função e desaparece quando a função
retorna, podendo a memória que lhe corresponde ser reutilizada.
int a[10], x;
int *pa;
pa = &a[0]; /* pa fica a apontar para a[0] - endereço inicial do array a[] */
x = *pa; /* x passa a ser igual a a[0] */
Observe-se agora a figura seguinte:
Para se acessar à posição i do array é agora possível escrever:
*(pa + i) que é equivalente a (<->) a[i]
Nota importante: O C não testa de maneira alguma a validade de acessos fora do bloco de memória previamente declarado ou alocado.
No entanto a identificação entre apontadores e arrays vai mais longe. Por exemplo, é válido escrever:
pa = a; em vez de pa = &a[0];
e como vimos a[i] <-> *(pa + i) , ou seja &(a[i]) <-> pa + i
Outra construção equivalente é pa[i] e *(pa + i)
No entanto existe uma diferença entre arrays e apontadores:
Assim a chamada à função int strlen(char s[]); pode ser feita de forma equivalente das seguintes maneiras:
strlen(s) ou strlen(&s[0]) sendo s um array de caracteres terminado com o carácter de código 0 (string).
Da mesma forma a declaração da função strlen também poderia ser feita como: int strlen(char *s);
Nota: strlen() é uma função da biblioteca standard do C que retorna o tamanho de uma string.
Esta função poderia ter sido escrita da forma seguinte:
int strlen(char *s)
{
char *p = s;
while (*p != '\0') /* ou
mais simplesmente while (*p++); */
p++;
/* uma vez que qualquer valor != de 0 é considerado true */
return p-s;
/* número de caracteres que cabem entre os endereços */
}
/* representados por p e s */
Outro exemplo: uma função que copia uma string para outra; strcpy() é também uma função da biblioteca standard do C.
void strcpy(char *s, char *t)
{
while (*t++ = *s++);
}
É de lembrar que o último carácter de uma string
em C deve ter o código 0 que faz com que o ciclo while termine.
Imaginemos que queríamos ordenar linhas de texto (strings) que se apresentam com comprimentos muito diferentes. Uma forma bastante eficiente de fazer isso em memória poder ser a que se esquematiza a seguir:
Este esquema elimina:
Considere-se um array a[5][35] que pretendemos passar como argumento a uma função. Na declaração desta devemos indicar o número de colunas, porque é necessário sempre saber quantos bytes ocupa cada elemento do array simples que tem como elementos arrays com esse número de colunas. Assim a função poderá ser declarada como: void f(int a[][35]) { ... } ou alternativamente como void f(int (*a)[35]) { ... }. Precisamos dos parêntesis em (*a) porque o operador [] tem maior prioridade do que o operador *.
Assim int (*a)[35] representa um apontador para um array de 35 int's, enquanto que int *a[35] representaria um array de 35 apontadores para int.
Vejamos agora algumas diferenças (subtis) entre apontadores e arrays. Tomemos as seguintes strings como exemplo:
char *name[10];
char aname[20][10];
Legalmente é possível escrever name[3][4] e aname[3][4] num programa em C.
Contudo:
char *name = {"no month", "jan", "feb", ...
};
char aname[][15] = {"no month", "jan", "feb",
... };
void some_function(void)
{
static char *months = { "jan", "feb",
"mar", "apr", "may", "jun",
"jul", "aug", "sep", "oct", "nov", "dec" };
...
}
struct coord {float x, y, z; } pt;
struct coord *ppt;
ppt = &pt;
Em C existe o operador -> que nos permite acessar diretamente a membros de uma estrutura apontada por um apontador, como nas 2 linha seguintes:
ppt->x = 1.0;
ppt->y = ppt->z - 3.1;
Outro exemplo: listas ligadas
typedef struct {
int value;
element *next;
} element;
element n1, n2;
n1.next = &n2;
O código de cima liga o elemento n1 a n2 através do seu membro next, como se mostra na figura:
*x = 100;
O código de cima está errado (x não inicializado - pode conter qualquer valor aleatório), no entanto não há qualquer mensagem do compilador. É necessário sempre inicializar os apontadores antes de os utilizar, como por exemplo:
int *x, y;
x = &y;
*x = 100;
Considere-se o seguinte código:
char *p;
*p = (char *) malloc(100);
*p = 'y';
Existe um erro na 1ª atribuição: não se deveria ter utilizado o operador * em p. A função malloc retorna um endereço (ou seja ou apontador), logo dever-se-ia ter escrito: p = (char *) malloc(100);
No entanto, mesmo com o código rectificado, pode subsistir um outro problema. Se malloc retornar o valor NULL a instrução seguinte acede memória inválida e em geral produz um erro.
O código correcto deveria então ser escrito da forma:
p = (char *) malloc(100);
if (p == NULL) {
printf("Error: out of memory\n");
exit(1);
/* termina o programa com código 1 */
}
*p = 'y';
2. Escreva um programa que conte o número de vezes que uma palavra
(um pequeno string) ocorre num texto (um string grande).
Leia uma palavra numa única linha. Leia de
seguida o texto até aparecer um carácter EOF. Não
esquecer os zeros terminais dos strings.
3. Escreva uma função (e o respectivo programa de teste)
que rode os seus 3 argumentos. Se estes se chamarem a, b e c, o valor de
a vai para b, o valor de b vai para c e o valor de c vai para a.