Considere o programa abaixo que pede ao usuário dois inteiros, armazena-os em duas variáveis, troca seus valores, e os imprime.
#include <stdio.h> main(void) { int a, b, temp; printf("Entre dois numeros: "); scanf("%d %d", &a, &b); printf("Voce entrou %d e %d\n", a, b); /* Troca a com b */ temp = a; a = b; b = temp; printf("Trocados, eles sao %d e %d\n", a, b); }
Aqui está um exemplo de execução do programa:
Entre dois numeros: 3 5 Voce entrou 3 e 5 Trocados, eles sao 5 e 3
O seguinte trecho do programa executa a troca de valores das variáveis a e b:
temp = a; a = b; b = temp;
É possível escrever uma função que executa esta operação de troca? Considere a tentativa abaixo de escrever esta função:
#include <stdio.h> void troca(int, int); void troca(int x, int y) { int temp; temp = x; x = y; y = temp; } main(void) { int a, b; printf("Entre dois numeros: "); scanf("%d %d", &a, &b); printf("Voce entrou com %d e %d\n", a, b); /* Troca a e b */ troca(a, b); printf("Trocados, eles sao %d e %d\n", a, b); }
Se você executar este programa, verá que ele não funciona:
Entre dois numeros: 3 5 Voce entrou 3 e 5 Trocados, eles sao 3 e 5
Como você já viu nas notas anteriores, em C os argumentos são passados por valor. Uma vez que somente os valores das variáveis são passados, não é possível para a função troca() alterar os valores de a e b porque troca() não sabe onde na memória estas variáveis estão armazenadas. Além disso, troca() não poderia ser escrito usando a sentença return porque só podemos retornar um valor (não dois) através da sentença return.
A solução para o problema acima é ao invés de passar os valores de a e b, passar o endereço das variáveis a e b. Desta forma, troca() saberia em que endereço de memória escrever, portanto poderia alterar os valores de a e b.
Lembre-se que em C a cada variável está associado: (i) um nome; (ii) um tipo; (iii) um valor; e (iv) um endereço. Assuma que existam as seguintes definições de variáveis.
int i = 5; char c = 'G';
Na memória, eles podem estar armazenados da forma abaixo:
A variável inteira i está armazenada no endereço 1342. Ela usa dois bytes de memória (quando um objeto usa mais de um byte, seu endereço é onde ele começa - neste caso, 1342 e não 1343). A variável do tipo char c está armazenada no endereço 1346 e usa um byte de memória. O compilador é que controla do local de armazenamento destas variáveis em memória.
Nós podemos usar o operador de endereço para determinar o endereço de uma objeto na memória. Este operador só pode ser usado com ``lvalues'' (objetos que podem estar no lado esquerdo de uma atribuição, como no caso de variáveis) porque ``lvalues'' tem um endereço alocado na memória.
Por exemplo, no exemplo acima, poderíamos usar o operador de endreço como nas expressões abaixo:
&i
tem valor 1342
&c
tem valor 1346
Em C , uma variável que contém um endereço de memória é uma variável do tipo ponteiro. Um valor, que é um endereço (como &a) é um valor de ponteiro. Quando um ponteiro (a variável) contém um determinado endreço, dizemos que ele aponta para o endereço de memória (ou se este endereço de memória for associado a uma variável, dizemos que ele aponta para esta variável).
Há um tipo distinto de ponteiro para cada tipo básico C (como int, char e float). É verdade que todos os endereços tem
o mesmo tamanho (em Dev-C são 5 bytes), mas nós também precisamos
saber algo sobre o que é armazenado no endereço de memória apontado
(quantos bytes ocupa e como os bytes devem ser interpretados). Por
exemplo, um tipo ponteiro usado para ``apontar'' para inteiros é
chamado ponteiro para int e isso é denotado por um *.
Variáveis do tipo ponteiro para int são usados para armazenar
endereços de memória que contem valores do tipo int.
Por exemplo, dadas as definições de i e c acima, nós podemos definir duas novas variáveis pi e pc, ambos do tipo ponteiro.
int *pi; char *pc;Nesta definição não inicializamos as variáveis com nenhum valor. Podemos inicializá-las com:
pi = &i; pc = &c;Depois destas atribuições, o valor de pi seria 1342, e o valor de pc seria 1346.
Note que nesta definição da variável int *pi, pi é o nome da variável e int * é o tipo de pi (ponteiro para int).
Quando um ponteiro aponta para um endereço de memória, a operação para acessar o conteúdo do endereço apontado é chamado de dereferência. O operador unário * é usado para fazer a dereferência. Note que este uso do símbolo * não tem nada a ver com o símbolo de multiplicação. Usando os exemplos anteriores, *pi é o objeto apontado por pi.
*pi
tem valor 5
*pc
tem valor 'G'
Como um pointer dereferenciado (tais como *pi ou *pc) refere-se a um objeto na memória, ele pode ser usado não só como valor, mas também como um ``lvalue''. Isto significa que um pointer dereferenciado pode ser usado no lado esquerdo de uma atribuição. Veja alguns exemplos:
printf("Valor= %d, Char = %c\n", *pi, *pc); *pi = *pi + 5; *pc = 'H';*pi no lado esquerdo do = refere-se ao endereço de memória para o qual pi aponta. *pi no lado direito do = refere-se ao valor armazenado no endereço apontado por pi. A sentença *pi = *pi + 5; faz com que o valor armazenado no endereço apontado por pi seja incrementado de 5. Note que o valor de *pi muda, não o valor de pi.
Neste exemplo, os valores das variáveis i e c poderiam ter sido alterados sem a utilização de ponteiros da seguinte forma:
printf("Valor = %d, Char = %c\n", i, c); i = i + 5; c = 'H';
Os exemplos acima ilustram como uma variável pode ser acessada diretamente (através do seu nome) ou indiretamente (através de um ponteiro apontando para o endereço da variável).
Nos exemplos acima, pode parecer que ponteiros não são úteis, já que tudo que fizemos pode ser feito sem usar ponteiros. Agora, considere novamente o exemplo da função troca(). Quando a e b são passados como argumentos para troca(), na verdade, somente seus valores são passados. A função não podia alterar os valores de a e b porque ela não conhece os endereços de a e b. Mas se ponteiros para a e b forem passados como argumentos ao invés de a e b, a função troca() seria capaz de alterar seus valores; ela saberia então em que endereço de memória escrever. Na verdade, a função não sabe que os endereços de memória são associados com a e b, mas ela pode modificar o conteúdo destes endereços. Portanto, passando um ponteiro para uma variável (ao invés da variável), habilitamos a função a alterar o conteúdo destas variáveis da função chamadora.
Uma vez que endereços de variáveis são do tipo ponteiro, a lista de parâmetros formais da função deve refletir isso. A definição da função troca() deveria ser alterada, e a lista de parâmetros formais deve ter argumentos não do tipo int, mas ponteiros para int, ou seja, int *. Quando chamamos a função troca(), nós não passamos como parâmetros reais a e b, que são do tipo int, mas &a e &b, que são do tipo int *. Dentro da função troca() deverá haver mudanças também. Uma vez que agora os parâmetros formais são ponteiros, o operador de dereferência, *, deve ser usado para acessar os objetos. Assim, a função troca() é capaz de alterar os valores de a e b ``remotamente''.
O programa abaixo é a versão correta do problema enunciado para a função troca():
#include <stdio.h> void troca(int *, int *); /* function troca(px, py) * acao: troca os valores inteiros apontados por px e py * entrada: apontadores px e py * saida: valor de *px e *py trocados * suposicoes: px e py sao apontadores validos * algoritmo: primeiro guarda o primeiro valor em um temporario e troca */ void troca(int *px, int *py) { int temp; temp = *px; *px = *py; *py = temp; } main(void) { int a, b; printf("Entre dois numeros: "); scanf("%d %d", &a, &b); printf("Voce entrou com %d e %d\n", a, b); /* Troca a e b -- passa enderecos */ troca(&a, &b); printf("Trocados, eles sao %d e %d\n", a, b); }
A saída deste programa é:
Entre dois numeros: 3 5 Voce entrou com 3 e 5 Trocados, eles sao 5 e 3
Basicamente, se a função precisa alterar o valor de uma variávels da função chamadora, então passamos o endereço da variável como parâmetro real, e escrevemos a função de acordo, ou seja, com um ponteiro como parâmetro formal.
A precedência dos operadores * e & é alta, a mesma que outros operadores unários. A tabela 4 apresenta a precedência de todos os operadores vistos até agora.
Armando Luiz Nicolini Delgado