Subsecções

17 Mais sobre funções: Quando return não é suficiente

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.

17.1 Usando ponteiros

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:

\includegraphics[scale=0.5]{ptr1}

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.

17.2 O operador de endereço (&)

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

17.3 Tipo ponteiro

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).

17.4 O operador de dereferência: *

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).

17.5 Ponteiros como argumentos de funções

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.

17.6 Precedência de operadores

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.


Tabela 4: Precedência e associatividade de operadores
Operador Associatividade
   
() esquerda para direita
! - ++ -- * & (cast) (unários) direita para esquerda
* / % esquerda para direita
+ - esquerda para direita
< <= > >= esquerda para direita
== != esquerda para direita
&& esquerda para direita
|| esquerda para direita
= += -= *= /= %= direita para esquerda


Armando Luiz Nicolini Delgado
2013-10-21