Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Arquivos da Categoria: Controle de Fluxo

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! 😛

Controle de Fluxo e Operandos Lógicos

Olá!
Vamos continuar então a programar em C.

Os dois primeiros programas, Hello World e o de Entrada de Dados, tinham como ponto comum o fato de serem, vamos dizer assim, diretos. Eles eram executados, realizavam cada instrução em sequencia e se encerravam.
Isso em si não é ruim: muitos programas simples e interessantes podem ser feitos assim. Porém, é perceptível que eles não possuem nenhuma “inteligência”: eles não conseguem executar instruções dependendo das entradas que lhe são oferecidas. Isso se deve ao fato de eles não possuírem nenhum controle de fluxo. Em programação, esse termo é adotado para definir situações onde o programa pode mudar a sequencia de execução do programa conforme condições estipuladas no momento do desenvolvimento da aplicação. Por exemplo: no programa de Entrada de Dados, poderíamos estipular valores limite para primeiro e segundo que não os limites do tipo int.
Pois bem, veremos agora um programa com um pouco mais de “inteligência”, pois iremos falar mais sobre os comandos de controle de fluxo em C e sobre os operadores lógicos, além de questões sobre o tipo booleano (ou melhor, sobre a ausência de um tipo booleano) em C.
No caso, vamos escrever um outro programa comum, o “adivinhe o número”. Mas vamos adicionar alguma “inteligência” a ele e tornar ele um pouco mais desafiador:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/* Definindo o número limite para a pessoa tentar descobrir */

#define LIMITE 10
#define ENCERRA printf(“Pressione qualquer tecla para continuar!\n”); while(!getchar());

int main (int argc, char** argv)
{
  const int
limiteTentativas=(LIMITE/2)-1;
  int numeroPensado=0, numeroDoUsuario=0, maximoTentativas=0,i;
 
  srand(time(NULL)); // Serve para modificar a tabela de números pseudo-aleatórios
  numeroPensado=rand()%LIMITE+1;

  printf(“OK! Pensei em um número entre 1 e %d e te desafio a achar ele!\n”, LIMITE);
  do
    {
      printf(“Quantas tentativas você acha que precisa para descobrir ele? “);
      scanf(“%d”,&maximoTentativas);

      if (maximoTentativas<1)
         printf(“Você precisa tentar ao menos uma vez! :P\n”);

      if (maximoTentativas>limiteTentativas)
         printf(“%d tentativas? Tá querendo demais também! :P\n”,maximoTentativas);
    } while (maximoTentativas<1 || maximoTentativas>limiteTentativas);

  for (i=1; i<=maximoTentativas;i++)
    {
      printf(“Vamos lá! %da. tentativa! “,i);
     
      scanf(“%d”,&numeroDoUsuario);

      if (numeroDoUsuario==numeroPensado)
      {
         printf(“Parabéns! Você acertou!\n”);
         ENCERRA
         return(0);
      }
         else
         {
           printf(“Não pensei em %d. “, numeroDoUsuario);
           if (numeroDoUsuario<numeroPensado)
              printf(“Pensei em um número maior.\n”);
           if (numeroDoUsuario>numeroPensado)
              printf(“Pensei em um número menor.\n”);
         }
    }

    printf(“Que pena! Eu pensei em %d! Melhor sorte da próxima vez! \n”,numeroPensado);
    ENCERRA
    return(0);
}

Como de costume, iremos ignorar algumas coisas que já vimos anteriormente. Portanto, se você ficar com alguma dúvida, dê uma relida nos posts anteriores para fixar bem o conteúdo que já lidamos. No caso, vamos falar sobre os trechos de código que estão com destaque colorido no fonte àcima.

#define e os “números mágicos”

Logo de cara incluímos duas novas bibliotecas em C: stdlib.hSTanDard Library (biblioteca padrão), que contêm uma série de funções cotidianas em C – e time.h, que contem funções para lidar com tempo. Dessas, a stdlib.h em especial é muito importante, pois uma grande quantidade de comandos C muito usados está incluída nela. No caso, estamos muito interessados nas funções relacionadas à geração de números randômicos, mas existe muito mais nela e no futuro falaremos muito sobre essa biblioteca.
Logo após, vemos dois novos comandos do pré-processador (ou macros, como são chamados tecnicamente):

#define LIMITE 10
#define ENCERRA printf(“Pressione qualquer tecla para continuar!\n”); while(!getchar());

A macro #define permite ao programador definir um símbolo que o pré-processador irá substituir por uma informação qualquer no momento da compilação. No caso, definimos dois símbolos:

  • LIMITE – um número com valor 10;
  • ENCERRA – uma sequencia de comandos C;

No caso, você pode se perguntar sobre a utilidade de definir-se símbolos. Esse sistema de símbolos permite:

  1. Criar “comandos” simples (caso do ENCERRA);
  2. Criar situações de compilação condicional (um tópico mais avançado, veremos no futuro);
  3. Evitar os chamados “números mágicos”;

“Números mágicos?! Virou Hogwarts agora?”
Na verdade não. Em programação, chamamos de números mágicos determinados números ou expressões que são colocadas no código para realizar determinadas funções. No nosso caso, precisamos limitar o maior número no qual nosso programa irá “pensar”. Poderíamos simplesmente escrever ele no nosso programa de maneira direta, mas como esse número iria se repetir várias vezes em vários trechos de código aparentemente sem relação um com o outro, com certeza a leitura e análise de erros do programa (que em programação é chamada de depuração) seria complicada. Por isso chamamos tais números de números mágicos: uma vez que ele entra, temos dificuldades para entender o que eles fazem.
No caso, ao criarmos um símbolo (LIMITE), podemos usar ele no nosso código que o próprio compilador (através do pré-processador) irá substituir todas as entradas do símbolo LIMITE pelo valor desejado (no caso, 10). Isso torna o programa mais legível e mais simples de ser modificado.
O mesmo vale para ENCERRA. Com ENCERRA, criamos um “pseudo-comando”, um símbolo que será substituído por uma série de comandos C. No caso, o comando escrito em ENCERRA ajuda alguns usuários de IDEs (em especial no Windows) a visualizarem os resultados dos seus programas.
Existe mais sobre #define que ainda iremos ver, em especial na questão dos “pseudo-comandos”, mas esse básico deve ajudar a compreender melhor as coisas até termos uma oportunidade de nos aprofundarmos nele (em especial, quando falarmos no futuro de outras macros de pré-processador, quando revisitaremos #include e #define).

Variáveis Constantes:

Após as macros temos uma declaração de variáveis: logo de cara, temos uma que é um pouco diferente do que o que vimos quando falamos dos tipos de variáveis e como as declaramos:

  const int limiteTentativas=(LIMITE/2)-1;

A principal diferença está na palavra reservada const antes da declaração do tipo int da variável limiteTentativas. O que essa palavra faz é indicar ao compilador que essa variável na verdade é uma constante do tipo desejado, portanto não poderá mais ser modificada uma vez que seja inicializada, o que acontece em seguida. No caso, a variável constante limiteTentativas tem seu valor inicializado com o resultado de (LIMITE/2)-1, ou seja, na divisão do valor do nosso símbolo LIMITE por 2, menos 1 (no caso atual, o valor final é 4).
Você deve estar se perguntando: “usar um símbolo com #define não seria mais interessante?”
Bem, nesse caso não. Perceba que o limite de tentativas é determinado baseando-se no valor de LIMITE, e o #define não realiza per se contas ou qualquer processamento: tudo o que ele faz é dizer que (1) existe um símbolo e (2) qual seu valor. Se modificarmos o valor de LIMITE, teríamos que modificar o valor desse novo #define manualmente.
Você pode pensar então: “por que não tornar a fórmula um símbolo via #define?”. A ideia parece boa, mas ela cai em um problema: quando você substituir o símbolo, você obrigaria o sistema a repetir várias vezes uma determinada conta. Podemos pensar em desktops com gigas de poder de processamento e isso aparentemente ser uma boa ideia, mas pense em um sistema embarcado e você verá que uma situação como essa iria provocar redução na velocidade do programa.
Já com a variável constante, temos uma situação onde a variável não poderá ser modificada mas ainda assim será calculada apenas uma vez, apenas exigindo uma recompilação no caso de uma mudança de LIMITE (o que iria acontecer de qualquer maneira), pois ao compilar, LIMITE seria substituído pelo seu valor.
“Eu tenho que OBRIGATORIAMENTE inicializar uma variável constante no momento em que a declaro?”, você deve estar se perguntando. Na realidade, da mesma forma que ocorre com as variáveis comuns, as variáveis constantes PODEM ser inicializadas após a declaração. O que é impedido é que uma variável constante receba OUTROS VALORES após a inicialização. Como constantes são normalmente usadas para delimitar de alguma forma o comportamento do programa (no nosso caso, utilizaremos limiteTentativas para delimitar o máximo de tentativas que uma pessoa terá de adivinhar o número “pensado” pelo computador), é uma boa prática inicializá-la antes de qualquer entrada de dados.
Bem, acho que fechamos aqui a questão das variáveis constantes, à exceção de uma coisa: por que logo abaixo vem uma segunda linha de declarações int?
Quando você coloca várias declarações e/ou inicializações de variáveis em uma mesma linha, o compilador irá entender que todas elas tem o mesmo comportamento. Se colocássemos as int em questão junto com a declaração const int de limiteTentativas, essas variáveis também seriam declaradas constantes, o que não é o comportamento desejado. Nesse caso, a regra é clara: declarações de constantes devem ser separadas das declarações de variáveis comuns.
Bem, agora que vimos o bastante sobre constantes para entendermos o programa, sigamos em frente.

rand(), srand() e o gerador de números pseudo-aleatórios:

Vamos dar uma olhada nesse bloco de código:

srand(time(NULL)); // Serve para modificar a tabela de números pseudo-aleatórios
numeroPensado=rand()%LIMITE+1;

A primeira linha utiliza duas funções: a primeira é srand(), da stdlib.h, que recebe um long int como entrada. Essa função serve para modificar a tabela de números pseudo-aleatórios do sistema, usando o long int de entrada como parâmetro para essa modificação. No caso, usamos a função time(NULL), da biblioteca time.h, que devolve o horário do sistema atual em UNIX TimeStamp (o número de segundos passados desde as 0:00 do dia 01/01/1970 até o momento atual). Na verdade, ela retorna o epoch time (outra forma pela qual pode ser chamado o UNIX TimeStamp) para qualquer data passada de maneira correta (não entraremos nesse detalhe aqui).
Por que chamamos os números do sistema de pseudo-aleatórios?
Porque o computador não consegue, pela própria natureza lógica, gerar um padrão 100% aleatório. O que pode ser feito é utilizar-se algoritmos que gerem números, considerando-se a probabilidade de sua ocorrência em sequencia, que se aproximem da aleatoriedade e montar uma tabela com ela. O problema é que, ao se “pegar” um número nessa tabela é que, caso ela não seja “inicializada” de maneiras diferentes entre cada execução, o valor deixará de ser aleatório, ainda que continue sendo não-determinístico (ou seja, você não consegue, a partir das sequencias de números, determinar com precisão as fórmulas matemáticas que as geraram). Para evitar esse problema, utilizamos um comando, o srand(), que irá modificar a semente da tabela de números pseudo-aleatórios, o que irá garantir que, a cada execução, como o Timestamp será diferente, a semente da tabela será diferente e, por sua vez, a saída da mesma será diferente (se quiser saber mais sobre a idéia de geração de números pseudo-aleatórios, dê uma olhada nesse artigo da Wikipedia). De qualquer modo, sabemos que o srand() irá garantir que os números “aleatórios” tenham esse comportamento o mais próximo possível da realidade.
Na linha seguinte, utilizamos rand() para obter um número aleatório. Porém, esse comando não nos permite definir qual o valor máximo a ser obtido, normalmente obtendo qualquer valor entre 0 e uma definição chamada RAND_MAX (normalmente o limite de um unsigned int). A opção padrão é obter o resto da divisão inteira entre o maior número que desejamos e o valor retornado. Nesse caso, obtemos um número que vai de 0 ao limite estipulado menos 1. No nosso caso, queremos um número entre 1 e o LIMITE estipulado, por isso adicionamos 1 ao valor obtido (lembre-se da prioridade de execução da operação de resto em relação à soma).
Bem, acho que já falamos demais sobre rand(), até porque não tem como aprofundar nesse caso sem entrar uma teoria extremamente complexa e totalmente fora do nosso escopo atual (e muito provavelmente futuro). Tudo que é preciso saber é que precisaos usar srand() antes de rand() para garantir o comportamento aleatório e que precisamos utilizar a operação resto para “limitar” o valor de rand().

Laço de repetição condicional – do {…} while e operações relacionais e lógicas:

Em seguida, vemos que ele irá apresentar o desafio ao usuário e entrará no bloco de código abaixo:

  do
    {
      printf(“Quantas tentativas você acha que precisa para descobrir ele? “);
      scanf(“%d”,&maximoTentativas);

      if (maximoTentativas<1)
         printf(“Você precisa tentar ao menos uma vez! :P\n”);
      if (maximoTentativas>limiteTentativas)
         printf(“%d tentativas? Tá querendo demais também! :P\n”,maximoTentativas);
    } while (maximoTentativas<1 || maximoTentativas>limiteTentativas);

No nosso caso, nos focaremos nos comandos em ciano. Eles representam um tipo de controle de fluxo que chamamos de repetição (ou iteração, em alguns lugares) condicional. O nome pomposo apenas quer dizer que o laço em questão vai ser executado enquanto uma determinada condição for cumprida. No nosso caso, esse comando funciona assim:

  do
    {
       <…>
    } while (<condicao>);

O que fazemos aqui é indicar que, enquanto a condicao estipulada for verdadeira, o programa continuará no laço.

Mas como construir uma condição lógica? Para isso, C nos fornece os operadores lógicos, que comparam valores e retornam “verdadeiro” ou “falso” conforme a situação.
Aqui cabe um parenteses antes de entrarmos nos operadores: o C não possui um tipo booleano (ou seja, verdadeiro ou falso) específico. Para o C, um valor 0, NULL (que é definido por padrão como 0) ou “” (string vazia) como falso e qualquer outro valor como verdadeiro. Isso é importante pois existe um bug em C muito comum que iremos discutir adiante.
Bem, dito isso, os operadores lógicos em C padrão são: 

Operador  

>= 

<= 
== 
!=
&&
||
!
Ação  
Maior do que
Maior ou igual a
Menor do que 
Menor ou igual a 
Igual a 
Diferente de
E lógico
OU lógico
NÃO lógico

(fonte: curso de C da UFMG)
Os operadores lógicos são escritos de maneira similar aos matemáticos, sempre comparando um valor a outro. No caso, por exemplo, do comando acima, temos três expressões:

  • primeiro, uma comparação maximoTentativas<1;
  • depois, uma comparação maximoTentativas>limiteTentativas;
  • e por fim, uma operação lógica ou entre os dois;

Como isso é resolvido no C? Primeiro, o sistema irá analisar se o maximoTentativas<1. Caso seja, ele nem irá fazer a segunda comparação maximoTentativas>limiteTentativas, pois o fato de a comparação lógica ser OU vai fazer o programa aceitar como verdadeira a condição e continuará no laço (chama-se a isso short-circuit logical analysis – análise lógica de curto-circuito, e parte do princípio que uma vez que tenhamos garantido que uma condição é sabidamente verdadeira ou falsa, não há necessidade de continuar-se processando a lógica em questão). Caso contrário, ele irá tentar a segunda comparação, maximoTentativas>limiteTentativas, para verificar se ela é verdadeira ou falsa e, portanto, a condição o será.
Parece meio confuso, mas é só pensar com calma que você irá entender. Procure imaginar valores para maximoTentativas e pense em qual será a resposta (lembre-se: no caso atual, limiteTentativas é 4).
O que acontecerá se o valor não fizer nenhuma das duas condições estipuladas ser verdadeira? Simples: a expressão como um todo será falsa (0) e o laço do…while irá se encerrar normalmente, com o programa seguindo adiante. Ou seja, somente quanto o usuário entrar com um número máximo de tentativas maior que 1 e menor ou igual que o limite de tentativas estipulado (no caso, 4), o laço será interrompido.
Temos uns ifs engraçadinhos dentro desse laço, mas não falaremos sobre ele agora. Vamos então seguir em frente.
Para fecharmos esse tópico, existe uma versão do do…while chamada simplesmente while. Ela é representada assim:

while(<condicao>)
{
    <…>
}

e sua única diferença em relação ao seu “irmão” é que, caso ao chegar na entrada do laço a condição for satisfeita, o sistema sequer irá entrar no bloco de código em questão. O do…while irá executar ao menos uma vez, uma vez que a condição é testada no momento da saída do laço, diferentemente do while, que é executada quando da entrada no laço.

Iterações – o comando for:

Após nosso usuário ter determinado quantas tentativas ele quer ter para acertar o número mágico, vamos então dar a chance a ele.

  for (i=1; i<=maximoTentativas;i++)
    {
      printf(“Vamos lá! %da. tentativa! “,i);
     
      scanf(“%d”,&numeroDoUsuario);

      if (numeroDoUsuario==numeroPensado)
      {
         printf(“Parabéns! Você acertou!\n”);
         ENCERRA
         return(0);
      }
         else
         {
           printf(“Não pensei em %d. “, numeroDoUsuario);
           if (numeroDoUsuario<numeroPensado)
              printf(“Pensei em um número maior.\n”);
           if (numeroDoUsuario>numeroPensado)
              printf(“Pensei em um número menor.\n”);
         }
    }

Aqui temos um número delimitado de tentativas por máximo de tentativas. Desse modo, podemos utilizar um outro comando de laço, o for, que é um outro caso de laço de repetição ou iteração. Diferentemente do do…while, porém, o for é uma iteração não-condicional. Na verdade, ele é condicional, mas ele é mais usado em situações na qual se espera que ele se execute um determinado número de vezes (pode-se usar ele até como um while diferente, mas não é boa prática e não falaremos sobre isso aqui).
O for tem como estrutura a seguinte:
  for (<inicializacao>; <condicao>; <iteracao>)
    {
       <…>
    }

No caso, ele é um pouquinho complexo, portanto vamos dar uma olhada no seu comportamento:

  • no momento em que o programa chega no for, a primeira coisa que é feita é executar os comandos em <inicializacao>. No nosso caso, ele inicializa a variável i em 1. Essa inicialização pode ser feita como desejado, mas restringindo-se a um comando (ou bloco de código);
  • Em seguida, o bloco de código do for será executado;
  • Ao terminar de executar-se o bloco de código do for, ele executa o comando que está em <iteracao>. No nosso caso, utilizamos um operador matemático ++, que é utilizado em C para adicionar-se um ao valor da variável ao qual ela sucede. Na verdade ele se comporta de uma maneira mais complexa, mas para o momento basta entender que i++ seria o equivalente a i=i+1;
  • Depois de executar a <iteracao>, o sistema irá verificar se a <condicao> colocada é verdadeira. Caso o seja, ele irá interromper o laço for da mesma forma que o while, caso contrário ele entrará e executará uma nova iteração. No nosso caso, testamos se i ainda é menor ou igual ao número de limiteTentativas. Lembrando que C não possui tipo booleano e que basta que o valor seja 0 ou “” para ser considerado falso e, portanto, o laço for seja interrompido;

Bem, acho que isso deve ter deixado claro como o for funciona. O primeiro printf do programa deve deixar claro que você vai passando por várias iterações até acertar o “número mágico” “pensado” pelo nosso programa (o do rand() do início do programa). Mas como o programa saberá quando fomos bem-sucedidos?
Para isso existe o nosso próximo comando.

Execução condicional – o comando if:

OK… Paramos falando sobre como o nosso programa saberá que fomos bem sucedido. Veja que temos um scanf lendo o que o personagem entrou naquela tentativa. Precisamos de um comando para decidir fomos bem sucedidos ou não em acertar a descoberta do número “mágico”  “pensado” pelo programa. Quem cuida disso é comando if do bloco abaixo:

      if (numeroDoUsuario==numeroPensado)
      {
         printf(“Parabéns! Você acertou!\n”);
         ENCERRA
         return(0);
      }
      else
      {
         printf(“Não pensei em %d. “, numeroDoUsuario);
         if (numeroDoUsuario<numeroPensado)
            printf(“Pensei em um número maior.\n”);
         if (numeroDoUsuario>numeroPensado)
            printf(“Pensei em um número menor.\n”);
      }

O if é uma estrutura importante de programação, pois ele executa blocos de código caso a condição determinada seja verdadeira:

      if (<condicao>)
      {
        <…>
      }
      else if (<outra_condicao>)
      {
        <…>
      }
      else
      {
        <…>
      }

O if irá executar o bloco de código abaixo da instrução caso condicao seja verdadeira. Caso contrário ele pode:

  1. Testar outras condições. Para isso, utiliza-se else if, com uma nova condição e um bloco de código adequado;
  2. Executar um código para exceção. Para isso, utiliza-se else e o bloco de código adequado;
  3. Não fazer nada;
Agora, vamos tentar ler esse trecho de código. A primeira coisa que ele testa é se numeroDoUsuario==numeroPensado, ou seja, se o número que o usuário entrou é igual ao número “pensado” pelo sistema. Caso seja, ele indica que foi bem sucedido e encerra o mesmo (perceba que temos um return(0) dentro do if. Isso é permitido pelo C). Perceba que o operador de relação de igualdade é ==. NUNCA CONFUNDA COM O OPERADOR DE ATRIBUIÇÃO, =. Esse é um bug (falha de programação) muito comum que é provocado por falta de atenção na programação. No caso de colocar =, o valor de numeroDoUsuario seria substituído pelo de numeroPensado. A não ser que numeroPensado fosse 0 (o que é impossível, devido à lógica do programa) o programa acusaria verdadeiro independentemente do valor realmente ser (lembre-se, qualquer valor diferente de 0 ou “” ou NULL é considerado pelo C como verdadeiro em relações lógicas).
Bem, dito isso, vamos continuar a analisar nosso programa. Caso a condição acima não seja verdadeira, ele irá ignorar o bloco abaixo de if e irá ver que temos um else com um bloco de código. Ele então entrará nesse bloco de código. A primeira coisa que ele irá fazer é dizer que não pensou no numeroDoUsuario e testar se numeroDoUsuario<numeroPensado. Caso seja verdadeiro, ele irá imprimir na tela uma mensagem dizendo que pensou em um número maior que o que o usuário entrou. Caso contrário, irá verificar se numeroDoUsuario>numeroPensado. Se verdadeiro (provavelmente será, se tudo mais deu errado), irá imprimir que pensou em um número menor que o que o usuário pensou.
Os ifs dessas linhas possuem uma característica interessante que é o fato de terem comandos diretamente abaixo deles, e não dentro de blocos de código. Na realidade isso deve-se ao fato que if, while, for e afins, todos eles possuem uma característica de executar na realidade apenas um comando. A diferença é que, para o compilador C, um bloco de código (lembre-se: blocos de código são comandos isolados pelos colchetes {}) são considerados comandos isolados. Portanto, um bloco de código == um comando.
Lendo com calma o código você irá compreender muito bem o mesmo. Leia e releia o códigoo e faça exercícios mentais para entender o código do if.

Quando tudo dá errado – considerações:

OK… Mas e caso o usuário não acerte mesmo depois de ter tentado o número de vezes que ele desejou (maximoTentativas).
Isso ocorre quando a variável i, após a iteração do for, ficar com um valor maior que maximoTentativas, tornando a condição  i<=maximoTentativas falsa e saindo do laço for (ao atingir o limite de iterações estipulado). Para encerrar o programa, nós fazemos o sistema imprimir uma mensagem dizendo qual o número no qual ele pensou e saindo do mesmo.
Antes de irmos para as “brincadeiras” finais, algumas considerações:

  • Os operadores relacionais que mostramos anteriormente só servem para valores numéricos ou para o tipo char. Em especial para strings, a biblioteca string.h traz funções que nos permite testar igualdade entre textos. Veremos eles melhor no futuro;
  • Ainda não esgotamos o assunto controle de fluxo. Em especial falaremos ainda sobre um tipo de execução condicional para múltiplos valores e sobre como “quebrar” laços como o for e o while;
  • É possível aninhar ifs ccom múltiplos else if e afins. Porém, tome cuidado ao usá-los: além de tornar o código de difícil leitura, você pode ter problemas dependendo do compilador (normalmente o C exige no mínimo 8 “níveis” de ifs aninhados, mas esse valor não é obrigatório);

Bem, dito isso, vamos fazer algumas sugestões para “brincadeiras” com o nosso código:

  • Comente ou remova o srand() e veja o comportamento do gerador de números pseudo-aleatórios;
  • Tente reescrever o código para tornar maximoTentativas uma constante. Lembre-se que não é necessário inicializar o valor imediatamente, podendo ser inicializado em um momento futuro;
  • No caso de quando o usuário erra o número “pensado”, você pode ter percebido que os dois ifs são redundantes. Tente reescrever o código usando apenas um if;
  • Da mesma forma que existe o operador ++, existe o operador , que subtrai um da variável que o antecede. Considerando isso e o funcionamento do laço for, tente reconstruir o laço para não ter uma condição no sentido exato da palavra. Dica: lembre-se que C trata 0 como falso;
  • Para ter uma idéia do problema que pode ocorrer quando se confunde o operador de igualdade com o de atribuição, modifique o código removendo um dos sinais de igual de if (numeroDoUsuario==numeroPensado) e veja o que acontece com o programa. Para maior clareza, coloque um printf que apresente o número “pensado” antes do usuário entrar o seu número e digite outro completamente diferente. Coloque também um printf mostrando o numeroDoUsuario após o bug;
  • Para entender a questão das definições, altere o valor de LIMITE e veja como o programa se comporta. Como dica, o cálculo do limiteTentativas baseia-se na idéia de buscar-se o número sempre indo na metade do que é válido. Por exemplo: computador escolhe 6. Na primeira interação, tento 5, e ele me fala que pensou um maior. Tento 7 (metade do bloco “acima de cinco”) e ele me diz que pensou um menor. Tento 6 e acerto;

Bem, semana que vem iremos reeforçar a teoria dos operadores (lista completa e exemplos) e daremos uma terminada na questão dos controles de fluxo. Até lá, divirtam-se!