Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Arquivos Diários: 16/12/2010

Funções – Parte 2

Olá a todos!
Bem, primeiramente desculpem a demora, pois tive muitas atividades de trabalho que me “frearam” um pouco. Mas não perdi a vontade de passar o que sei de C. E vocês, ainda estão aqui aprendendo?
Bem, então vamos continuar o tópico da aula anterior: Funções.
Na aula anterior, vimos como construir uma função, porque devemos usá-las, e como passar parâmetros e receber seus retornos. Com isso, podemos dizer que sabemos construir funções. Porém, ainda não sabemos como aproveitar ao máximo as funções, uma vez que vimos regras que “amarram” a construção de funções, tornando-as complexas. Em especial a regra de criar-se a função antes do uso (ou seja, colocar o código da função antes de qualquer chamada que seja feita a ela) é muito estranha. Na aula de hoje, iremos ver como escapar dessa “amarra” de programação. Também veremos uma característica das funções em C chamada recursividade, que é a capacidade de uma função chamar a sí própria, o que torna mais simples construir-se determinados algoritmos e programas.
Bem, vamos ao que interessa:

Protótipos de Função:

Bem, vamos começar com o primeiro tópico, que é o de protótipos. No caso, teremos um programa em cada tópico. Para o nosso tópico, usaremos o programa abaixo:

#include <stdio.h>

int soma(int a, int b);
int subtracao(int a, int b);
int multiplicacao(int a, int b);
int divisao(int a, int b);

void main(void)
{
  /**
   * No GCC, esse cabecalho para main retorna o seguinte warning:
   *
   * warning: return type of ‘main’ is not ‘int’
   */

  int val1=0, val2=0, opt=0, res=0;

  do
    {
      printf(“Digite um valor:”);
      scanf(“%d”,&val1);

      printf(“Digite outro valor:”);
      scanf(“%d”,&val2);

      printf(“Escolha a operação a ser realizada…\n\n”);
      printf(“1 – Soma\n”);
      printf(“2 – Subtracao\n”);
      printf(“3 – Multiplicacao\n”);
      printf(“4 – Divisao”);
      printf(“0 – Sair do Programa\n\nDigite os operadores e a operacao separados por espaco:”);

      scanf(“%d”,&opt);

     switch(opt)
      {
      case 1:
        res=soma(val1,val2);
        break;
      case 2:
        res=subtracao(val1,val2);
        break;
      case 3:
        res=multiplicacao(val1,val2);
        break;
      case 4:
        res=divisao(val1,val2);
        break;
      case 0:
        break;
      default:
        printf(“Opcao invalida\n”);
        continue;
      }

      if (opt!=0) printf (“O resultado da sua operacao para %d e %d e %d\n\n”,val1,val2,res);
    } while (opt!=0);

  /**
   * No GCC, a linha abaixo provoca o seguinte warning:
   *
   * warning: ‘return’ with a value, in function returning void
   */

  return(0);
}

int soma(int a, int b)
{
  return a+b;
}

int subtracao(int a, int b)
{
  return a-b;
}

int multiplicacao(int a, int b)
{
  return a*b;
}

int divisao(int a, int b)
{
  return a/b;
}

Primeira coisa: perceba que usamos novamente um outro tipo de protótipo para main(), void main(void). Como dissemos nos comentários do programa, o uso desse protótipo não é padrão e irá retornar alertas (Warnings) pelo compilador. Se preferir fazer um programapedante” (ou seja, sem erros ou alertas), você pode trocar o protótipo do main() de volta para o velho e bom int main(int argc, char** argv). De qualquer forma, apenas fizemos isso para demonstrar o que acontece quando tenta-se utilizar um desses protótipos não-padrão.

Agora, vamos a algo importante antes de entrarmos no nosso tópico. Olhe o código em verde:

    switch(opt)
    {

      case 1:

        res=soma(val1,val2);

        break;

      case 2:

        res=subtracao(val1,val2);

        break;

      case 3:

        res=multiplicacao(val1,val2);

        break;

      case 4:

        res=divisao(val1,val2);

        break;

      case 0:

        break;

      default:

        printf(“Opcao invalida\n”);

        continue;

    }

Esse comando de controle de fluxo, o switch, é muito usado como substituto para cadeias monstruosas de if…elseif…else, em especial quando existem códigos que serão usados em uma ou mais opções. O switch…case compara o valor da variável dada como entrada (no nosso caso, opt) com o valor inserido em cada uma das linhas case. Caso o valor da variável em questão seja igual ao valor de um dos case, o programa irá seguir a execução desse ponto até o final do bloco switch…case ou até encontrar um comando break, o que acontecer primeiro. No caso de nenhum dos case case com o valor da variável a ser comparada, nada será feito, a não ser que exista uma cláusula default estipulada no bloco. Nesse caso, o programa irá continuar a execução a partir desse ponto, valendo as mesmas regras para os demais case. No nosso caso, por exemplo, se opt for igual a 7, o default será executado e exibirá na tela Opcao Invalida, e retornará ao início do laço do…while. Caso, por exemplo, opt estivesse em 2, o resultado da função subtracao(val1,val2) seria associado à variavel res e em seguida o switch…case seria interrompido.
Bem, agora que falamos desse comando de controle de fluxo que “passou batido” até agora, vamos falar sobre o nosso tópico atual.
Como vimos na aula anterior, as funções devem, na teoria, vir antes de serem usadas por um programa. Na realidade, isso não é bem verdade. O que o compilador normalmente precisa é saber como trabalhar com uma função, ou seja, os valores que ele precisa passar para a mesma como parâmetros e o tipo de retorno da mesma. O código em si não precisa sequer ser descrito como parte do seu programa, podendo estar (o código em si) em qualquer outro lugar. (lembram das bibliotecas, como stdio.h, string.h e stdlib.h? Na realidade eles são úteis para o compilador saber como usar as funções que eles “representam”. Os códigos estão armazenados em outras bibliotecas e arquivos dentro do sistema operacional ou do compilador). Como exemplo, podemos pensar em um conector para celular. Cada celular usa um conector específico e, desde que o conector siga o tipo de conexão que o celular exige, pode ser usado conectores de quaisquer marcas (vide a quantidade de carregadores “genéricos” que existem por aí) e com qualquer tipo de entrada de energia (não importa se é de tomada ou de carro, por exemplo).
No C, chamamos esses “padrões” de protótipos de função. Um protótipo de função nada mais é que um informativo que o compilador usa para saber como “chamar” uma função. A idéia é que os protótipos representam a seqüência de parâmetros e o tipo de retorno das funções a serem usadas no nosso código. Lembram-se de quando falamos que existe a idéia de “caixa preta” no código? Os protótipos são o que permitem a existência dessa “caixa preta”: o que o programador e o compilador precisa saber é o que a função precisa de entrada e o que ela devolve como resultado. O programador não precisa como saber como a função foi construída (imaginando que não tenha sido ele que a construiu) e o compilador só precisa saber se as chamadas de função são “encaixáveis” corretamente ao código.
Quando o programa é compilado, as funções que não fazem parte do programa e que estão em biblotecas são “encaixadas” ao programa de várias formas em um passo chamado de linkedição (em alguns livros mais atuais, usa-se o termo ligação). Um binário sem ser linkeditado é chamado ocasionalmente de código-objeto e, embora não seja útil para ser executado, eles são muito úteis (veremos no futuro compilação de programas com múltiplos fontes), inclusive podendo conter funções que possam ser ligadas a posteriori a outros programas (nessa situação, o código-objeto é chamado também de biblioteca). Uma vez que o ou os programas-fontes sejam compilados e linkeditados, o binário executável está pronto.
Voltando ao assunto, é por meio dos protótipos que o compilador sabe como “encaixar” cada função nas partes onde as funções são chamadas (na realidade, são colocados endereços para onde o programa vai e segue a execução). Além disso, por meio dos protótipos que o compilador, até certo ponto, consegue “perceber” se o código está corretamente construído, pois ele tem todas as informações da entrada de dados (parâmetros) e da saída (retorno).
Para o protótipo da função, a construção é igual ao do nome da função no início da mesma (que alguns chamam de cabeçalhos), com a diferença do ; no fim do protótipo. Na realidade, para o protótipo, você não precisa colocar nenhum nome de variável. uma vez que nesse momento, o que ele precisa saber é quais os tipos de parâmetro a serem recebidos, e não seu nome. No caso, embora tenhamos usado:

int soma(int a, int b);

para uma maior legibilidade do código, poderíamos usar simplesmente:

int soma(int, int);

que seria tão útil quanto o protótipo que o colocamos. De qualquer forma, aconselho que mantenha as declarações com “nomes de variáveis” como uma boa prática, para aumentar a legibilidade sobre quais são os parâmetros a serem passadas.
Não existe o que falar mais: uma vez que o protótipo tenha sido colocado de alguma forma à disposição, o compilador pode buscar funções em qualquer lugar, seja dentro do código objeto equivalente ao fonte compilado, em um arquivo de biblioteca ou no próprio sistema operacional e “encaixá-las” ou “ligá-las” ao programa do usuário.
De resto, existe pouco o que falar desse programa, pois ele não tem mistérios quanto ao que cada função faz. Para as “brincadeiras”, sugerimos:

  1. Tente remover os protótipos (comentando-os, por exemplo) e compilar os programas. Alguns compiladores irão dar alertas mas irão compilar o seu fonte, enquanto outros simplesmente se recusarão a compilar o fonte;
  2. Para comprovar que o “nome de variável” no protótipo não faz diferença, modifique o “nome de variável” de alguma das funções no protótipo, mas não na função;
  3. Para entender bem a idéia dos protótipos de função, uma boa forma é ler a documentação da biblioteca-padrão do C. Existem muitas funções interessantes nela, em especial em bibliotecas como stdlib.h, string.h, stdio.h, time.h e math.h. Nesse link você encontra a documentação completa das bibliotecas-padrão (em inglês). Procure ler com calma e tentar entender o que cada função faz. Obviamente você não compreendará tudo no presente momento, pois muitas funções lidam com conceitos avançados que ainda falaremos, mas com calma você verá algumas funções interessantes. Se possível, tente construir seus próprios programas e funções a partir dos códigos que mencionamos no momento;

Bem, com isso terminamos a parte de protótipos de função. Vamos a um tópico mais interessante: recursividade.

Recursividade – Chamando a si próprio:

Existem certos algoritmos (formas de descrever-se algo para um computador) que são mais facilmente representáveis quando eles usam de algum modo a si próprio. Dois exemplos clássicos são os cálculos de Fatorial e do número Fibonacci. Para relembrar, um número N fatorial (representado N!) é representado pela multiplicação de todos os inteiros até N (sendo que os fatoriais de 0 e 1 são definidos como 1). No caso, esse é o algoritmo que iremos ver em C, pois pe um exercício clássico de programação recursiva.

#include <stdio.h>

unsigned int fatorial (unsigned int a)
{
  if ((a==0) || (a==1))
    return 1;
  else
    return a*fatorial(a-1);
}

int main(void)
{
  unsigned int numeroFatorial=0;

  printf(“Digite o numero ao qual deseja-se obter fatorial (apenas positivos): “);
  scanf(“%d”, &numeroFatorial);
 
  printf(“%d! = %d\n”,numeroFatorial,fatorial(numeroFatorial));

  return(0);
}

Esse código é bem básico e tem pouco mistérios. O importante é atentar ao código da função fatorial:

unsigned int fatorial (unsigned int a)
{

  if ((a==0) || (a==1))
    return 1;

  else

    return a*fatorial(a-1);

}

A primeira coisa que ele define é que, caso o valor passado na execução da função seja 0 ou 1, o valor a ser devolvido pela execução é 1. Caso contrário, ele irá devolver o valor passado vezes o valor devolvido pela execução da mesma função com um valor igual ao valor passado-1.
Como funcionaria então, por exemplo, para o fatorial 5? Vejamos em um teste de mesa:

  • main começa executando fatorial(5);
  • Como 5 não é igual a 0 ou 1, ele deveria retornar 5*o resultado de fatorial(5-1), ou seja, fatorial(4). Como não sabe o valor de fatorial(4), ele executa fatorial(4);
  • Como 4 também não é igual a 0 ou 1, ele deveria retornar 4*o resultado de fatorial(4-1), ou seja, fatorial(3). Como não sabe o valor de fatorial(3), ele executa fatorial(3);
  • Como 3 também não é igual a 0 ou 1, ele deveria retornar 3*o resultado de fatorial(3-1), ou seja, fatorial(2). Como não sabe o valor de fatorial(2), ele executa fatorial(2);
  • Como 2 também não é igual a 0 ou 1, ele deveria retornar 2*o resultado de fatorial(2-1), ou seja, fatorial(1). Como não sabe o valor de fatorial(1), ele executa fatorial(1);
  • Como 1 é igual a 1, a função fatorial retorna 1;
  • Agora ele volta para a execução de fatorial(2), pois obteve o valor de fatorial(1), que ele precisava. Ele faz 2*fatorial(1), ou seja, 2*1, retornando 2;
  • Em seguida, retoma a execução de fatorial(3), pois obteve o valor de fatorial(2), que ele precisava. Ele faz 3*fatorial(2), ou seja, 3*2, retornando 6;
  • Em seguida, retoma a execução de fatorial(4), pois obteve o valor de fatorial(3), que ele precisava. Ele faz 4*fatorial(3), ou seja, 4*6, retornando 24;
  • Por fim, retoma a execução de fatorial(5), pois obteve o valor de fatorial(4), que ele precisava. Ele faz 5*fatorial(4), ou seja, 5*24, retornando 120 para main;

Atenção para a questão de:

  if ((a==0) || (a==1))
    return 1;

Todo algoritmo recursivo deve ter uma situação de “escape”, caso contrário provocará um loop infinito. No caso do fatorial, é o fato que os fatoriais de 0 e 1 são definidos por padrão como 1 (no link da Wikipedia mostrado anteriormente há uma explicação dos motivos desses valores serem pré-definidos). Somando-se isso e o fato de que a chamada recursiva é sempre equivalente ao valor da chamada atual-1, o resultado é que cedo ou tarde, o valor vai ser 0 ou 1 (valores negativos são negados já na tipagem unsigned int), ou seja, a “escada” de chamadas irá ser desfeita, com cada chamada devolvendo os valores esperados pela anterior. Caso isso não ocorra, haverá um loop “infinito” que se encerrará com um estouro de memória (uma vez que cada chamada de função armazena localmente valores e portanto precisa de espaço de memória).
Bem, não existe mais o que se falar sobre recursividade. Como “brincadeiras” quanto recursividade, sugiro:

  1. Uma circunstância a ser levada em consideração ao se construir algoritmos com recursividade é sobre o uso dos operadores unários de incremento e decremento (++ e ). Para observar seu impacto, no return a*fatorial(a-1), tente substituir por return a*(a–) e verifique o que acontece. Lembre-se que os operadores unários de incremento e decremento atuam como operadores de incremento/decremento e atribuição;
  2. Edite o código da chamada recursiva e elimine a “condição de escape” da recursão. Coloque algum código que permita você visualizar os valores recebidos a cada chamada recursiva e seus impactos e analise o resultado final;
  3. Tente implementar o algoritmo para determinar-se um número Fibonacci. Lembrando que um número de Fibonacci equivale à soma de todos os números naturais antecessores a ele, predefinido que o 0° Fibonacci é 0 e o 1° Fibonacci é 1. Se você reparar bem, não é muito diferente do cálculo de um número Fatorial;

Com isso, acabamos o básico de Funções. Ainda existem tópicos a serem cobertos. Em especial, um tópico importante que estamos deixando para trás é o de tipos de passagem de parâmetro, um tópico importante que cobriremos quando falarmos de ponteiros, nosso próximo assunto.
Vamos ter algum tempo até começarmos o assunto de ponteiros. Enquanto isso, existem muitos sites e apostilas na internet com exercícios de programação em C que poderão ajudar você a fixar o conteúdo que vimos até agora. Enquanto a mim, vou ficar um tempo sem uma Internet de boa qualidade, mas prometo que, assim que voltar estarei postando o início do tópico de ponteiros, com a parte de matrizes, ponteiros e a correlação entre os dois. Esse será um tópico bastante complexo, mas que se lido com calma irá ser bem fixado.
Então, nos vemos em 2011, pessoal. Até lá, boas festas e muita programação C para todo mundo! 😛