sábado, julho 09, 2011

Aula 7 –Funções


A Função

Funções são as estruturas que permitem ao usuário separar seus programas em blocos. Se não as tivéssemos, os programas teriam que ser curtos e de pequena complexidade. Para fazermos programas grandes e complexos temos de construí-los bloco a bloco.
 Uma função no C tem a seguinte forma geral:
 tipo_de_retorno nome_da_função (declaração_de_parâmetros)
{
corpo_da_função
}
O tipo-de-retorno é o tipo de variável que a função vai retornar. O default é o tipo int, ou seja, uma função para qual não declaramos o tipo de retorno é considerada como retornando um inteiro. A declaração de parâmetros é uma lista com a seguinte forma geral:
tipo nome1, tipo nome2, ... , tipo nomeN
Repare que o tipo deve ser especificado para cada uma das N variáveis de entrada. É na declaração de parâmetros que informamos ao compilador quais serão as entradas da função (assim como informamos a saída no tipo-de-retorno).
O corpo da função é a sua alma. É nele que as entradas são processadas, saídas são geradas ou outras coisas são feitas.

O Comando return

O comando return tem a seguinte forma geral:
return valor_de_retorno; ou return;
Digamos que uma função está sendo executada. Quando se chega a uma declaração return a função é encerrada imediatamente e, se o valor de retorno é informado, a função retorna este valor. É importante lembrar que o valor de retorno fornecido tem que ser compatível com o tipo de retorno declarado para a função.
Uma função pode ter mais de uma declaração return. Isto se torna claro quando pensamos que a função é terminada quando o programa chega à primeira declaração return. Abaixo estão dois exemplos de uso do return:  
 
#include 
int Square (int a)
{
      return (a*a);
}
int main ()
{
      int num;
      printf ("Entre com um numero: ");
      scanf ("%d",&num);
      num=Square(num);
      printf ("\n\nO seu quadrado vale: %d\n",num);
      return 0;
}
 
#include 
int EPar (int a)
{           
      if (a%2)           /* Verifica se a e divisivel por dois */
          return 0;        /* Retorna 0 se nao for divisivel */
      else
          return 1;        /* Retorna 1 se for divisivel */
}
int main ()
{
      int num;
      printf ("Entre com numero: ");
      scanf ("%d",&num);
      if (EPar(num))
            printf ("\n\nO numero e par.\n");
      else
            printf ("\n\nO numero e impar.\n");
      return 0;
}
É importante notar que, como as funções retornam valores, podemos aproveitá-los para fazer atribuições, ou mesmo para que estes valores participem de expressões. Mas não podemos fazer:
func(a,b)=x;    /* Errado! */
No segundo exemplo vemos o uso de mais de um return em uma função.
Fato importante: se uma função retorna um valor você não precisa aproveitar este valor. Se você não fizer nada com o valor de retorno de uma função ele será descartado. Por exemplo, a função printf() retorna um inteiro que nós nunca usamos para nada. Ele é descartado.
 


AUTO AVALIAÇÃO
Veja como você está. Escreva a função 'EDivisivel(int a, int b)' (tome como base EPar(int a)). A função deverá retornar 1 se o resto da divisão de a por b for zero. Caso contrário, a função deverá retornar zero.

Protótipos de Funções

Até agora, nos exemplos apresentados, escrevemos as funções antes de escrevermos a função main(). Isto é, as funções estão fisicamente antes da função main(). Isto foi feito por uma razão. Imagine-se na pele do compilador. Se você fosse compilar a função main(), onde são chamadas as funções, você teria que saber com antecedência quais são os tipos de retorno e quais são os parâmetros das funções para que você pudesse gerar o código corretamente. Foi por isto as funções foram colocadas antes da função main(): quando o compilador chegasse à função main() ele já teria compilado as funções e já saberia seus formatos.
Mas, muitas vezes, não poderemos nos dar ao luxo de escrever nesta ordem. Muitas vezes teremos o nosso programa espalhado por vários arquivos. Ou seja, estaremos chamando funções em um arquivo que serão compiladas em outro arquivo. Como manter a coerência?
A solução são os protótipos de funções. Protótipos são nada mais, nada menos, que declarações de funções. Isto é, você declara uma função que irá usar. O compilador toma então conhecimento do formato daquela função antes de compilá-la. O código correto será então gerado. Um protótipo tem o seguinte formato:
tipo_de_retorno nome_da_função (declaração_de_parâmetros);
onde o tipo-de-retorno, o nome-da-função e a declaração-de-parâmetros são os mesmos que você pretende usar quando realmente escrever a função. Repare que os protótipos têm uma nítida semelhança com as declarações de variáveis. Vamos implementar agora um dos exemplos da seção anterior com algumas alterações e com protótipos:
#include 
float Square (float a);
int main ()
{
      float num;
      printf ("Entre com um numero: ");
      scanf ("%f",&num);
      num=Square(num);
      printf ("\n\nO seu quadrado vale: %f\n",num);
      return 0;
}
float Square (float a)
{
      return (a*a);
}
Observe que a função Square() está colocada depois de main(), mas o seu protótipo está antes. Sem isto este programa não funcionaria corretamente.
Usando protótipos você pode construir funções que retornam quaisquer tipos de variáveis. É bom ressaltar que funções podem também retornar ponteiros sem qualquer problema. Os protótipos não só ajudam o compilador. Eles ajudam a você também: usando protótipos, o compilador evita erros, não deixando que o programador use funções com os parâmetros errados e com o tipo de retorno errado, o que é uma grande ajuda!

O Tipo void

Agora vamos ver o único tipo da linguagem C que não detalhamos ainda: o void. Em inglês, void quer dizer vazio e é isto mesmo que o void é. Ele nos permite fazer funções que não retornam nada e funções que não têm parâmetros! Podemos agora escrever o protótipo de uma função que não retorna nada:
void nome_da_função (declaração_de_parâmetros);
Numa função, como a acima, não temos valor de retorno na declaração return. Aliás, neste caso, o comando return não é necessário na função.
 Podemos, também, fazer funções que não têm parâmetros:
tipo_de_retorno nome_da_função (void);
 ou, ainda, que não tem parâmetros e não retornam nada:
void nome_da_função (void);
 Um exemplo de funções que usam o tipo void:
#include 
void Mensagem (void);
int main ()
{
      Mensagem();
      printf ("\tDiga de novo:\n");
      Mensagem();
      return 0;
}
void Mensagem (void)
{
      printf ("Ola! Eu estou vivo.\n");
}
Se quisermos que a função retorne algo, devemos usar a declaração return. Se não quisermos, basta declarar a função como tendo tipo-de-retorno void. Devemos lembrar agora que a função main() é uma função e como tal devemos tratá-la. O compilador acha que a função main() deve retornar um inteiro. Isto pode ser interessante se quisermos que o sistema operacional receba um valor de retorno da função main(). Se assim o quisermos, devemos nos lembrar da seguinte convenção: se o programa retornar zero, significa que ele terminou normalmente, e, se o programa retornar um valor diferente de zero, significa que o programa teve um término anormal. Se não estivermos interessados neste tipo de coisa, basta declarar a função main como retornando void.
 As duas funções main() abaixo são válidas:
main (void)
{
      ....
      return 0;
}
 
void main (void)
{
      ....
}
A primeira forma é válida porque, como já vimos,  as funções em C têm, por padrão, retorno inteiro.. Alguns compiladores reclamarão da segunda forma de main, dizendo que main sempre deve retornar um inteiro. Se isto acontecer com o compilador que você está utilizando, basta fazer main retornar um inteiro.

Arquivos-Cabeçalhos

Arquivos-cabeçalhos são aqueles que temos mandado o compilador incluir no início de nossos exemplos e que sempre terminam em .h. A extensão .h vem de header (cabeçalho em inglês). Já vimos exemplos como stdio.h, conio.h, string.h. Estes arquivos, na verdade, não possuem os códigos completos das funções. Eles só contêm protótipos de funções. É o que basta. O compilador lê estes protótipos e, baseado nas informações lá contidas, gera o código correto. O corpo das funções cujos protótipos estão no arquivo-cabeçalho, no caso das funções do próprio C, já estão compiladas e normalmente são incluídas no programa no instante da "linkagem". Este é o instante em que todas as referências a funções cujos códigos não estão nos nossos arquivos fontes são resolvidas, buscando este código nos arquivos de bibliotecas.
Se você criar algumas funções que queira aproveitar em vários programas futuros, ou módulos de programas, você pode escrever arquivos-cabeçalhos e incluí-los também.
Suponha que a função 'int EPar(int a)', seja importante em vários programas, e desejemos declará-la num módulo separado. No arquivo de cabeçalho chamado por exemplo de 'funcao.h' teremos a seguinte declaração:
int EPar(int a);
O código da função será escrito num arquivo a parte. Vamos chamá-lo de 'funcao.c'. Neste arquivo teremos a definição da função:



int EPar (int a)
{
if (a%2)             /* Verifica se a e divisivel por dois */
        return 0;
else
        return 1;
}
Por fim, no arquivo do programa principal teremos o programa principal. Vamos chamar este arquivo aqui de 'princip.c'.
#include 
#include "funcao.h"   
void main ()
{
      int num;
      printf ("Entre com numero: ");
      scanf ("%d",&num);
      if (EPar(num))
            printf ("\n\nO numero e par.\n");
      else
            printf ("\n\nO numero e impar.\n");
}
Este programa poderia ser compilado usando a seguinte linha de comando para o gcc:
gcc princip.c funcao.c -o saida
onde 'saida' seria o arquivo executável gerado.
Para gerar o executável deste programa no Rhide você deve criar um projeto, com a opção Project -> Open. Digitar um nome para o seu projeto (por exemplo saida). Ao apertar OK, o Rhide criará uma janela de projeto, onde você deverá adicionar os arquivos que serão usados para compor o seu executável. Para isto, você deve apertar a tecla e em seguida escolher os arquivos princip.c e funcao.c . Daí, é só mandar compilar o projeto, com a opção Compile -> Make. Se não der erro, pode executar!

AUTO AVALIAÇÃO
Veja como você está:
  Escreva um programa que faça uso da função EDivisivel(int a, int b). Organize o seu programa em três arquivos: o arquivo prog.c , conterá o programa principal; o arquivo func.c conterá a função; o arquivo func.h conterá o protótipo da função. Compile os arquivos e gere o executável a partir deles.




Escopo de Variáveis

Já foi dada uma introdução ao escopo de variáveis. O escopo é o conjunto de regras que determinam o uso e a validade de variáveis nas diversas partes do programa.
Variáveis locais
O primeiro tipo de variáveis que veremos são as variáveis locais. Estas são aquelas que só têm validade dentro do bloco no qual são declaradas. Sim. Podemos declarar variáveis dentro de qualquer bloco. Só para lembrar: um bloco começa quando abrimos uma chave e termina quando fechamos a chave. Até agora só tínhamos visto variáveis locais para funções completas. Mas um comando for pode ter variáveis locais e que não serão conhecidas fora dali. A declaração de variáveis locais é a primeira coisa que devemos colocar num bloco. A característica que torna as variáveis locais tão importantes é justamente a de serem exclusivas do bloco. Podemos ter quantos blocos quisermos com uma variável local chamada x, por exemplo, e elas não apresentarão conflito entre elas.
A palavra reservada do C auto serve para dizer que uma variável é local. Mas não precisaremos usá-la pois as variáveis declaradas dentro de um bloco já são consideradas locais. Abaixo vemos um exemplo de variáveis locais:
func1 (...)
{
      int abc,x;
      ...
}
func (...)
{
      int abc;
      ...
}
void main ()
{
      int a,x,y;
      for (...)
        {
            float a,b,c;
        ...
        }
      ...
}
No programa acima temos três funções. As variáveis locais de cada uma delas não irão interferir com as variáveis locais de outras funções. Assim, a variável abc de func1() não tem nada a ver (e pode ser tratada independentemente) com a variável abc de func2(). A variável x de func1() é também completamente independente da variável x de main(). As variáveis a, b e c são locais ao bloco for. Isto quer dizer que só são conhecidas dentro deste bloco for e são desconhecidas no resto da função main(). Quando usarmos a variável a dentro do bloco for estaremos usando a variável a local ao for e não a variável a da função main().
- Parâmetros formais
O segundo tipo de variável que veremos são os parâmetros formais. Estes são declarados como sendo as entradas de uma função. Não há motivo para se preocupar com o escopo deles. É fácil: o parâmetro formal é  uma variável local da função. Você pode também alterar o valor de um parâmetro formal, pois esta alteração não terá efeito na variável que foi passada à função. Isto tem sentido, pois quando o C passa parâmetros para uma função, são passadas apenas cópias das variáveis. Isto é, os parâmetros formais existem independentemente das variáveis que foram passadas para a função. Eles tomam apenas uma cópia dos valores passados para a função.

- Variáveis globais

Variáveis globais são declaradas, como já sabemos, fora de todas as funções do programa. Elas são conhecidas e podem ser alteradas por todas as funções do programa. Quando uma função tem uma variável local com o mesmo nome de uma variável global a função dará preferência à variável local. Vamos ver um exemplo:  
int z,k;
func1 (...)
{
      int x,y;
      ...
}
 
func2 (...)
{
      int x,y,z;
      ...
      z=10;
      ...
}
 
main ()
{
      int count;
...
}
No exemplo acima as variáveis z e k são globais. Veja que func2() tem uma variável local chamada z. Quando temos então, em func2(), o comando z=10 quem recebe o valor de 10 é a variável local, não afetando o valor da variável global z.
Evite ao máximo o uso de variáveis globais. Elas ocupam memória o tempo todo (as locais só ocupam memória enquanto estão sendo usadas) e tornam o programa mais difícil de ser entendido e menos geral.
 

AUTO AVALIAÇÃO
Veja como você está. Estude o seguinte programa e aponte o valor de cada variável sempre que solicitado:
#include 
int num;
int func(int a, int b)
{
      a = (a+b)/2;  /* Qual e o valor de a apos a atribuicao? */
      num -= a;
      return a;
}
 
main()
{
      int first = 0, sec = 50;
      num = 10;
      num += func(first, sec);  /* Qual e o valor de num, first e sec */
                               /* antes e depois da atribuicao?      */
      printf("\n\nConfira! num = %d\tfirst = %d\tsec = %d",num, first, sec);
}

Passagem de parâmetros por valor e passagem por referência

Já vimos que, na linguagem C, quando chamamos uma função os parâmetros formais da função copiam os valores dos parâmetros que são passados para a função. Isto quer dizer que não são alterados os valores que os parâmetros têm fora da função. Este tipo de chamada de função é denominado chamada por valor. Isto ocorre porque são passados para a função apenas os valores dos parâmetros e não os próprios parâmetros. Veja o exemplo abaixo:
#include 
float sqr (float num);
void main ()
{
       float num,sq;
       printf ("Entre com um numero: ");
       scanf ("%f",&num);
       sq=sqr(num);
       printf ("\n\nO numero original e: %f\n",num);
       printf ("O seu quadrado vale: %f\n",sq);
}
 
float sqr (float num)
{
       num=num*num;
       return num;
}
No exemplo acima o parâmetro formal num da função sqr() sofre alterações dentro da função, mas a variável num da função main() permanece inalterada: é uma chamada por valor.
            Outro tipo de passagem de parâmetros para uma  função ocorre quando alterações nos parâmetros formais, dentro da função, alteram os valores dos parâmetros que foram passados para a função. Este tipo de chamada de função tem o nome de "chamada por referência". Este nome vem do fato de que, neste tipo de chamada, não se passa para a função os valores das variáveis, mas sim suas referências (a função usa as referências para alterar os valores das variáveis fora da função).
            O C só faz chamadas por valor. Isto é bom quando queremos usar os parâmetros formais à vontade dentro da função, sem termos que nos preocupar em estar alterando os valores dos parâmetros que foram passados para a função. Mas isto também pode ser ruim às vezes, porque podemos querer mudar os valores dos parâmetros fora da função também. O C++ tem um recurso que permite ao programador fazer chamadas por referência. Há entretanto, no C, um recurso de programação que podemos usar para simular uma chamada por referência.
Quando queremos alterar as variáveis que são passadas para uma função, nós podemos declarar seus parâmetros formais como sendo ponteiros. Os ponteiros são a "referência" que precisamos para poder alterar a variável fora da função. O único inconveniente é que, quando usarmos a função, teremos de lembrar de colocar um & na frente das variáveis que estivermos passando para a função. Veja um exemplo:  
#include 
void Swap (int *a,int *b);
void main (void)
{
      int num1,num2;
      num1=100;
      num2=200;
      Swap (&num1,&num2);
      printf ("\n\nEles agora valem %d  %d\n",num1,num2);
}
void Swap (int *a,int *b)
{
      int temp;
      temp=*a;
      *a=*b;
      *b=temp;
}
Não é muito difícil. O que está acontecendo é que passamos para a função Swap o endereço das variáveis num1 e num2. Estes endereços são copiados nos ponteiros a e b. Através do operador * estamos acessando o conteúdo apontado pelos ponteiros e modificando-o. Mas, quem é este conteúdo? Nada mais que os valores armazenados em num1 e num2, que, portanto, estão sendo modificados!
Espere um momento... será que nós já não vimos esta estória de chamar uma função com as variáveis precedidas de &? Já! É assim que nós chamamos a função scanf(). Mas porquê? Vamos pensar um pouco. A função scanf() usa chamada por referência porque ela precisa alterar as variáveis que passamos para ela! Não é para isto mesmo que ela é feita? Ela lê variáveis para nós e portanto precisa alterar seus valores. Por isto passamos para a função o endereço da variável a ser modificada!

AUTO AVALIAÇÃO
Veja como você está
Escreva uma função que receba duas variáveis inteiras e "zere" o valor das variáveis.  Use o que você aprendeu nesta página para fazer a implementação

Vetores como Argumentos de Funções

Quando vamos passar um vetor como argumento de uma função, podemos declarar a função de três maneiras equivalentes. Seja o vetor:
                int matrx [50];
e que queiramos passá-la como argumento de uma função func(). Podemos declarar func() das três maneiras seguintes:
              void func (int matrx[50]);
              void func (int matrx[]);
              void func (int *matrx);
Nos três casos, teremos dentro de func() um int* chamado matrx. Ao passarmos um vetor para uma função, na realidade estamos passando um ponteiro. Neste ponteiro é armazenado o endereço do primeiro elemento do vetor. Isto significa que  não é feita uma cópia, elemento a elemento do vetor. Isto faz com que possamos alterar o valor dos elementos do vetor dentro da função.
Um exemplo disto já foi visto quando implementamos a função StrCpy().

AUTO AVALIAÇÃO
Veja como você está.
Escreva um programa que leia um vetor de inteiros pelo teclado e o apresente na tela. Crie uma função (void levetor(int *vet, int dimensao)) para fazer a leitura do vetor.

Os Argumentos argc e argv

A função main() pode ter parâmetros formais. Mas o programador não pode escolher quais serão eles. A declaração mais completa que se pode ter para a função main() é:
                int main (int argc,char *argv[]);
Os parâmetros argc e argv dão ao programador acesso à linha de comando com a qual o programa foi chamado.
O argc (argument count) é um inteiro e possui o número de argumentos com os quais a função main() foi chamada na linha de comando. Ele é, no mínimo 1, pois o nome do programa é contado como sendo o primeiro argumento.
O argv (argument values) é um ponteiro para uma matriz de strings. Cada string desta matriz é um dos parâmetros da linha de comando. O argv[0] sempre aponta para o nome do programa (que, como já foi dito, é considerado o primeiro argumento). É para saber quantos elementos temos em argv que temos argc.
Exemplo: Escreva um programa que faça uso dos parâamentros argv e argc. O programa deverá receber da linha de comando o dia, mês e ano correntes, e imprimir a data em formato apropriado. Veja o exemplo, supondo que o executável se chame data:
data 19 04 99
O programa deverá imprimir:
19 de abril de 1999
#include
#include
void main(int argc, char *argv[])

{
int mes;
char *nomemes [] = {"Janeiro", "Fevereiro", "Março", "Abril", "Maio",
                    "Junho", "Julho", "Agosto", "Setembro", "Outubro",
                    "Novembro", "Dezembro"};
if(argc == 4) /* Testa se o numero de parametros fornecidos esta' correto
                o primeiro parametro e' o nome do programa, o segundo o dia
                o terceiro o mes e o quarto os dois ultimos algarismos do ano */
{
    mes = atoi(argv[2]);  /* argv contem strings. A string referente ao mes deve ser
                             transformada em um numero inteiro. A funcao atoi esta
                             sendo usada para isto: recebe a string e transforma no
                             inteiro equivalente */

    if (mes<1 || mes>12)  /* Testa se o mes e' valido */
        printf("Erro!\nUso: data dia mes ano, todos inteiros");
    else
      printf("\n%s de %s de 19%s", argv[1], nomemes[mes-1], argv[3]);
}

else printf("Erro!\nUso: data dia mes ano, todos inteiros");
}

Recursividade

Na linguagem C, assim como em muitas outras linguagens de programação, uma função pode chamar a si própria. Uma função assim é chamada função recursiva. Todo cuidado é pouco ao se fazer funções recursivas. A primeira coisa a se providenciar é um critério de parada. Este vai determinar quando a função deverá parar de chamar a si mesma. Isto impede que a função se chame infinitas vezes.
Uma função que calcule o fatorial de um número inteiro n é um bom exemplo de uma função recursiva:
#include 
int fat(int n)
{
      if (n) 
        return n*fat(n-1);
      else return 1;
}
 
int main()
{
      int n;
      printf("\n\nDigite um valor para n: ");
      scanf("%d", &n);
      printf("\nO fatorial de %d e' %d", n, fat(n));
      return 0;
}
Note que, enquanto n não for igual a 0, a função fat chama a si mesma, cada vez com um valor menor. n=0 é critério de parada para esta função.
Há certos algoritmos que são mais eficientes quando feitos de maneira recursiva, mas a recursividade é algo a ser evitado sempre que possível, pois, se usada incorretamente, tende a consumir muita memória e ser lenta. Lembre-se que memória é consumida cada vez que o computador faz uma chamada a uma função. Com funções recursivas a memória do computador pode se esgotar rapidamente.  

Outras Questões

Uma função, como foi dito anteriormente, é um bloco de construção muito útil. No C as funções são flexíveis. A flexibilidade dá poder, mas exige cuidado.
            Funções devem ser implementadas, quando possível, da maneira mais geral possível. Isto as torna mais fáceis de serem reutilizadas e entendidas. Evite, sempre que possível, funções que usem variáveis globais.
Se houver uma rotina que deve ser o mais veloz possível, seria bom implementá-la sem nenhuma (ou com o mínimo de) chamadas a funções, porque uma chamada a uma função consome tempo e memória.

Um outro ponto importante é que, como já sabemos um bocado a respeito de funções, quando formos ensinar uma das funções das bibliotecas do C vamos mostrar, em primeiro lugar, o seu protótipo. Quem entendeu tudo que foi ensinado nesta parte sobre funções pode retirar inúmeras informações de um protótipo (tipo de retorno, nome da função, tipo dos argumentos, passagem por valor ou passagem por referência).
Sugiro que neste ponto, o leitor leia um arquivo-cabeçalho como, por exemplo o stdio.h ou o string.h. É um bom treino. Estes arquivo podem ser encontrados no diretório apropriado do compilador que você estiver utilizando (geralmente o subdiretório include do diretório onde você instalou o compilador).