Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Arquivos da Categoria: Operadores

Uma aprofundada em operadores e lógica em C

OK.
Vamos dar uma pequena pausa para aprofudar uma teoria que é necessária para seguirmos em frente: os operadores.

Na nossa última “aula”, vimos um pouco sobre os operadores, em especial operadores matemáticos, lógicos e relacionais. Mas na verdade o C é composto por uma enorme gama de operadores, capazes de realizar muitas atividades. Aprender bem como usar operadores é algo importantíssimo para construir bons programas em C. Portanto, vamos dar uma pausa e reforçar essa teoria antes de seguirmos em frente.
A primeira coisa a entender é que temos vários tipos de operadores em C, que podemos dividir  em “grupos” para facilitar a compreenção:

  • Operadores de atribuição – são operadores usados para atribuir-se valores a variáveis;
  • Operadores aritméticos – com ele realizamos operações matemáticas simples usando dois valores, os operandos;
  • Operadores relacionais – comparam os valores de dois operandos, verificando se eles representam verdades ou falsidades lógicas naquele momento;
  • Operadores lógicos – comparam os valores de dois operandos em termos lógicos, ou seja, comparações baseadas na lógica booleana (E, OU e NÃO);
  • Operadores de bits – Permitem realizar operações diretamente nos bits de um determinado valor (em geral, inteiro);
  • Operadores compostos – são operadores complexos que podem combinar as funções de dois operadores do tipos acima;
  • Operadores especiais -são operadores usados no desenvolvimento da aplicação;

Além disso, existe uma ordem de prioridade dos operadores que iremos mostrar no final desse post. Você não precisa decorar essa tabela: você pode utilizar esse post como referência ou então facilmente pegar uma “cola” dessa tabela na Internet em vários sites de referência.
Bem, vamos começar então com o Operador de Atribuição.

Operador de atribuição – =

O operador de atribuição = serve para indicar ao C que desejamos armazenar (ou atribuir) a uma determinada variável um valor. Já vimos nos programas anteriores vários exemplos de atribuições, em especial quando inicializamos variáveis como fizemos no programa de Entrada de Dados. Uma coisa, porém, que é importante dizer é que o C permite fazer-se múltiplas atribuições, desde que os tipos sejam compatíveis. Por exemplo, o trecho de código abaixo:

int a,c;
float b,d;
a = b = c = d = 0;

É válido, uma vez que b e d, ainda que sejam do tipo float, podem receber valores inteiros (por causa do autocast – lembra que vimos quando mostramos o programa de Entrada de Dados?). Basicamente, esse comando faz a seguinte salada na atribuição:

  • Atribui 0 a d – como d é float, dá o autocast do 0 (que nesse caso é tratado como inteiro) para float;
  • Em seguida, atribui a c o valor de dd está inicializado como 0.0 (0 no tipo float). Ao receber o valor para c, o sistema faz o autocast do valor, truncando-o para 0 (inteiro);
  • Após isso, atribui a b o valor de c – repete-se o caso da atribuição para d;
  • E por fim, atribui a a o valor de b – repetindo o que aconteceu quando atribui-se a c o valor de d;

Importante notar que os autocasts são sempre executados no momento da atribuição do valor a uma variável. Por isso que, se dividimos dois inteiros e precisamos de um valor float, mesmo atribuindo o resultado, precisamos forçar o cast de um dos inteiros para float antes: se usarmos a divisão normalmente, ele irá tratá-la como divisão entre dois inteiros e dará um resultado inteiro, que será convertido depois da operação em float. Ao forçar um deles como float, indicamos que precisamos de um resultado em ponto flutuante e, dando o autocast no outro operando, o programa realizará uma divisão em ponto flutuante e retornará um valor float.
Aproveitando que entramos no assunto typecast, existe uma prioridade nos autocasts: isso é feito com o objetivo de impedir que os resultados percam valor (por exemplo, ao converter-se um valor de um tipo para um outro tipo cujo tamanho seja menor em bits e, portanto, capaz de representar uma gama inferior de valores). Tenha isso em mente ao recorrer ao autocast: prefira, se possível, fazer você mesmo o typecast, pois isso irá garantir que você sabe qual será o tipo resultante.
Dito isso, vamos para o próximo conjunto de operadores.

Operadores aritméticos

Já falamos sobre eles antes: são operadores que realizam contas matemáticas simples entre dois operandos (valores), “devolvendo” um deles como valor da expressão aritmética (em C, uma expressão aritmética tem o mesmo significado que na matemática, e o termo expressão é extrapolado a partir daí como a representação de uma operação lógico-matemática que será resolvida em um determinado momento). Esse valor pode ser usado em uma atribuição ou em qualquer lugar onde exija-se um valor. Isso é interessante pois pode-se executar operações matemáticas, por exemplo, antes de operações lógicas, obviamente respeitando as prioridades determinadas pelo C (veremos abaixo as prioridades e como alterar a ordem de execução das operações).
Basicamente, as operações matemáticas são:

Operador

*
/
%

+
Operação

Multiplicação
Divisão
Resto da Divisão Inteira (mod)
Subtração
Soma

Prioridade

1
1
1
2
2

Na coluna Prioridade, fazemos uma referência a como o C prioriza as operações aritméticas entre si. No caso, primeiro o C executa divisões, multiplicações e resto de divisões conforme apareçam da esquerda para direita (como é feito na matemática). Em seguida, o C executa somas e subtrações (também da esquerda para a direita, conforme a matemática). Existem formas de alterar a prioridade da execução de fórmulas complexas, que veremos mais abaixo. Mas, basicamente, essas são as operações matemáticas possíveis de serem feitas com o C. Uma ressalva: diferentemente de outras linguagens de programação, C não possui um operador matemático para potenciação.
Em C, a divisão é palco de uma controversa. Normalmente em C, é feita divisão inteira entre valores se ambos forem do tipo int, mesmo que a atribuição do resultado seja feita para uma variável de valor flutuante. Isso deve-se ao fato de que o C considera dois momentos: (1) quando a operação de divisão é feita e (2) quando o valor resultante é atribuido à variável de ponto flutuante. Porém, se no momento da execução da divisão, um dos valores for de tipo flutuante (seja por ser uma varíavel float ou por um cast), o C irá pegar o termo restante, forçar o cast (autocasting) do mesmo para um tipo flutuante e retornar o resultado da operação com um valor de tipo flutuante. Isso é algo a manter-se em mente.
O operando % (resto da divisão inteira) é um caso a parte: ele vai transformar ambos os termos em inteiros, uma vez que o conceito de resto só existe em matemática quando falamos de número inteiros (mesmo nos casos de dízimas periódicas ou não periódicas, a matemática parte do princípio que, cedo ou tarde podemos descobrir o “último dígito” de um valor). Essa conversão é feita truncando-se o valor decimal do número com tipo flutuante, tornando-o inteiro. Porém, isso pode provocar inexatidão em resultados, podendo afetar os resultados finais do programa. Para isso, o C oferece uma biblioteca especializada em funções matemáticas, math.h, sobre a qual ainda iremos falar de maneira mais aprofundada.
Bem, dito isso, vamos seguir falando dos operadores. No caso, vamos falar dos operadores relacionais.

Operadores relacionais

O nome meio que diz tudo: operadores relacionais envolvem a relação entre dois valores específicos de maneira lógica, comparando-os entre si e vendo se a relação em questão é verdadeira ou não. Basicamente eles devolvem 1 caso a relação seja verdadeira, e 0 caso a mesma seja falsa. (Lembrando que, como vimos na nossa última “aula” não existe no C um tipo booleano, então ele adota 0, “” (string vazia) ou o símbolo null como convenções para falso e quaisquer outros valores como verdadeiro). Para ter uma noção do funcionamento do mesmo, rode o seguinte programa (retirado do material do Curso de C da UFMG):

#include <stdio.h>
int main()
{
    int i, j;

    printf("\nEntre com dois numeros inteiros: ");
    scanf("%d%d", &i, &j);

    printf("\n%d == %d é %d\n", i, j, i==j);
    printf("\n%d != %d é %d\n", i, j, i!=j);
    printf("\n%d <= %d é %d\n", i, j, i<=j);
    printf("\n%d >= %d é %d\n", i, j, i>=j);
    printf("\n%d < %d é %d\n", i, j, i<j);
    printf("\n%d > %d é %d\n", i, j, i>j);
    return(0);
}

Não existe muito a ser dito sobre novos comandos como fizemos em outros casos. O importante aqui é estudar o comportamento dos operadores relacionais, que estamos listando abaixo, já indicando também sua prioridade entre si:

Operador


>= 

<=
== 
!=
Operação

Maior do que 
Maior ou igual a 
Menor do que 
Menor ou igual a 
Igual a 
Diferente de

Prioridade

1
1
1
1
2
2

Aqui voltaremos a enfatizar:

NÃO EXISTE TIPO BOOLEANO EM C! EM C, QUALQUER VALOR DIFERENTE DE 0 OU DA STRING VAZIA “” OU DE NULL É CONSIDERADO VERDADEIRO!
Além disso, vamos reenfatizar outra coisa: nunca confunda o operador relacional de igualdade (==) com o de atribuição (=). Isso irá provocar erros de lógica de aplicação que são de difícil depuração.
Pois bem, esses três primeiros tópicos, falando de operadores de atribuição, aritméticos e relacionais são apenas uma revisão do que vimos anteriormente. A partir de agora iremos ver operadores que vimos por alto ou não vimos anteriormente, a começar pelos…

Operadores lógicos

Operadores lógicos são basicamente operadores que realizam comparações. De certa forma, são um subgrupo dos operadores relacionais, mas que trabalham apenas com valores segundo a lógica booleana. De uma forma rápida, podemos dizer que eles podem comparar se dois valores são verdadeiros ao mesmo tempo (E/AND), se pelo menos um entre dois valores pode ser considerado verdadeiro (OU/OR) e se um determinado valor é falso naquele momento (NÃO/NOT). A vantagem de usar-se operadores lógicos é que, combinando-os com os operadores relacionais, podemos construir condições complexas, em especial em situações de controle de fluxo. Por exemplo, vamos relembrar um trecho de código de nossa última “aula”:
      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! 😛 \n”);
          if (maximoTentativas>limiteTentativas)
             printf(“%d tentativas? Tá querendo demais também! 😛 \n”,maximoTentativas);
        } while (maximoTentativas<1 || maximoTentativas>limiteTentativas);

Relembrando: o do…while executa enquanto a condição indicada no final do bloco de código for considerada verdadeira. No caso, nossa intenção foi garantir que o usuário não entrasse com nenhum número negativo e nem fosse além de um limite de tentativas estipulado previamente. Para isso, utilizamos duas condições relacionais (maximoTentativas<1 e maximoTentativas>limiteTentativas), cada uma atuando em uma das situações que determinamos. Para “unirmos” as duas situações, usamos o operador lógico OU (|| em C) que indica que, enquanto pelo menos uma dessas condições relacionais for verdadeira, a condição lógica será verdadeira e, portanto, o programa permanecerá executando esse bloco de código.
C implementa os principais operadores relacionais, que são o E (AND), OU (OR), e NÃO (NOT), conforme representado abaixo:

Operador

!
&&
||
Operação

NÃO Lógico
E Lógico
OU Lógico

Prioridade

1
2
3

O programa abaixo, também retirado do material do Curso de C da UFMG poderá ilustrar melhor o comportamento de E, OU e NÃO:

#include <stdio.h> <br />int main()<br />{<br />   int i, j;<br />   printf("informe dois números(cada um sendo 0 ou 1): ");<br />   scanf("%d%d", &i, &j);<br />   printf("%d AND %d é %d\n", i, j, i && j);<br />   printf("%d OR %d é %d\n", i, j, i || j);<br />   printf("NOT %d é %d\n", i, !i);<br />}<br />

Na verdade, com o tempo esse conceito ficará bem arraigado na cabeça. Então vamos para os próximos operadores.
Antes, um parênteses: em vários momentos, e em especial quando falamos dos tipos de dados em C, mencionamos que em C, as strings de caracteres possuem um comportamento diferenciado em relação ao que vemos em outras linguagens de programação. Por isso, você NÃO DEVE utilizar os operadores lógicos e relacionais apresentados anteriormente com strings. Isso irá provocar certamente resultados espúrios. Para comparações envolvendo strings, a biblioteca padrão string.h oferece uma série de funções úteis, sobre as quais falaremos no futuro.

Operadores de bit (bitwise)

O C oferece um conjunto de operadores lógicos voltados exclusivamente para o “acesso direto” aos bits. Mais exatamente, para realizar operações lógicas com bits. Chamamos esses operadores de operadores de bit, bit-a-bit ou bitwise, dependendo do autor do livro.
C foi uma linguagem originalmente projetada para facilitar o desenvolvimento de aplicações em baixo nível, como sistemas operacionais, firmware (software de baixo nível para dispositivos eletrônicos, como celulares e players de DVD), compiladores, com o objetivo de substituir o Assembler e oferecer um mínimo de portabilidade de código, ainda que mantendo os benefícios do Assembler de acesso direto ao hardware. Como o software de um appliance, o firmware, se comunica com o hardware por pulsos eletrônicos interpretados pela CPU como bits 0s e 1s específicos em determinados locais de memória, é interessante que C possua comandos capazes de trabalhar com esses bits, que chamamos em programação de flags. Para isso, utiliza-se os operadores de bit.
Os operadores de bit lembram um pouco os operadores aritméticos e um pouco os operadores lógicos, pois eles irão realizar a operação lógica em cada bit de um determinado número, usando outro, e retornando um terceiro. Por exemplo, imaginemos que precisamos obter de um conjunto de flags (normalmente representados por um int) se o 5° bit dessa conjunto de flags está ativo. Uma forma é aplicar o bitwise AND (E bit-a-bit) contra o número 8 (representado 00001000). Esse valor (que alguns autores chamam de bitmask – máscara de bits) atua de tal forma que zera todos os outros bits do int em questão: lembrando que ambos os bit tem que ser 1 – verdadeiro – para que o bit na saída seja 1. Como os demais bits no bitmask são 0, o valor de tais bits é zerado. Se o bit na flag estiver zerado, o resultado será 0, caso contrário o valor de saída será 8.
Existem também operadores de movimentação de bits: algumas operações matemáticas e lógicas são resolvidas facilmente quando utilizamos essa movimentação de bits, “empurrando” os bits à esquerda ou direita, com isso aumentando e diminuindo seus valores. Como exemplo: uma forma “rápida e suja” de fazer-se a potência de 2 em um número é “empurrando” seus bits uma casa para a direita. Como em numeração binária um bit é sempre uma potência de 2 acima do bit à direita ao seu, ao empurrar-se os bits à direita temos o efeito da potência de 2. (PS: esse método não faz as checagens matemáticas de casos especiais como potências de 0 e 1 e nem verificam estouros do tamanho de variável). Ao empurrar-se os bits, o último bit na direção para a qual os bits foram “empurrados” é eliminado e o primeiro bit da direção oposta é preenchido com 0.
Os operadores de bit são:

Operador

!
<<
>>
&
^
|
Operação

NÃO por bit a bit
Deslocamento de bits à esquerda
Deslocamento de bits à direita
E bit a bit
OU Exclusivo (um e apena um bit)
OU bit a bit

Prioridade

1
2
2
3
4
5

Uma coisa a salientar de diferente entre os operadores bit-a-bit e os operadores lógicos é a existência do OU-Exclusivo (eXclusive OR, XOR) bit-a-bit. Nessa situação, ao comparar-se os dois números bit a bit, um bit na saída só será um se UM E APENAS UM dos bits comparados for 1. Caso contrário (ambos 0 ou ambos 1) o bit será zero.
Bem, esse é um tema complexo e pouco útil para nós. Vamos falar agora de…

Operadores Compostos

Operadores Compostos englobam operadores que atuam ao mesmo tempo como dois tipos diferentes de operadores, em geral uma operação bit-a-bit ou aritmética e uma atribuição. Além desses, existe o operador de atribuição condicional ?.
Vamos englobar em três subgrupos os operadores compostos: os de atribuição “aditiva”, os de incremento e decremento em um e o de atribuição condicional.
Os de atribuição “aditiva” são operadores que realizam uma determinada operação envolvendo um parâmetro com uma variável e atribuem à mesma variável o resultado da operação. Por exemplo, a expressão a+=4 é equivalente em termos de programação a a=a+4. Esses operadores existem para ambientes onde o espaço para a leitura do código fonte é pequeno e determinadas redundâncias de digitação comprometeriam a compilação. Alguns autores consideram que o uso desse tipo de operador torna o programa mais legível, enquanto outros defendem que esse tipo de operador provoca a ofuscação do código (ou seja, torna a leitura do código complexa). A lista dos operandos compostos segue abaixo:

Operador

/=
*=
%=
+=
-=
<<
>>
&
^
|
Operação

Divisão com atribuição
Multiplicação com atribuição
Resto da divisão inteira com atribuição
Soma com atribuição
Subtração com atribuição
Deslocamento de bits à esquerda com atribuição
Deslocamento de bits à direita com atribuição
E bit a bit com atribuição
OU Exclusivo (um e apena um bit) com atribuição
OU bit a bit com atribuição

Prioridade

1
1
1
2
2
3
3
4
4
4

Esse é um subtipo complexo, e o próximo também demanda atenção, que são os operadores de incremento e decremento em um. Na verdade, esses operadores são indicados por ++ e e indicam que a variável onde eles estão sendo usados terá seu valor somado ou subtraído em 1. Porém, existe um comportamento que deve ser levado em conta ao usar-se tais operadores, que é o de prefixação e pós-fixação. Para usar-se os operadores desse tipo, você pode colocar o operador antes ou depois da varíavel. A posição onde o colocar, porém, irá afetar o comportamento sobre qual será o valor realmente usado.
Ao prefixar o operador antes da variável (ou seja, colocar ++ ou antes da variável), o valor será modificando antes de qualquer uso. Por exemplo, se você usar a=++b, e b for 4, acontecerá o seguinte: (1) b será incrementada em 1, para 5 e (2) a receberá o novo valor de b, 5. Ao pós-fixar o operador, acontecerá o contrário: o valor será usada para qualquer outro fim antes de ser modificado. Mantendo o exemplo anterior, imagine que você, ao invés de usar a=++b, utilizou a=b++. Nesse caso (1) a receberá o valor atual de b, 4 e depois é que (2) b será incrementado em 1. Essa diferença é importante de ter-se em mente, pois ela pode provocar bugs (em especial quando utiliza-se esse operador em conjutnto com comparações relacionais), mas é uma ferramenta muito poderosa.
Por exemplo: nas “brincadeiras” da nossa última “aula” sugeri:

  • 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;

A forma para fazer isso é lembrar que o for, na comparação, exige um “booleano” verdadeiro (ou seja, qualquer valor diferente de 0). Portanto, se usarmos o operador de decremento em 1 pós-fixado, podemos fazer com que o número de tentativas vá se reduzindo até que, na última tentativa, ele chegue na condição em 0 e, sendo “falso”, saia do laço. Portanto, basta substituir a linha:

for (i=1; i<=maximoTentativas;i++)

no programa da nossa última “aula” por:

for (i=1; maximoTentativas–; i++)

e teremos feito a mágica. Na realidade, se não usarmos a variável i para exibir o número da tentativa, podemos simplesmente reduzir o comando para:

for (; maximoTentativas–;)

e o código terá o mesmo comportamento, sem precisar de uma variável de controle.

Mas, ATENÇÃO: isso só funcionará bem se você usar o decremento pós-fixado. Se tentar prefixar o decremento, ele irá, na prática, tirar uma tentativa do usuário, ao ponto de, se o usuário pedir apenas uma tentativa o sistema nem pedir o número: no caso, ele irá subtrair 1 de maximoTentativas (que será 1), reduzindo-o para 0 e tornando a condição “falsa”.
Bem, aqui podemos dizer que terminamos de falar do incremento e decremento de um. Vamos falar, para acabar com os atributos compostos, da atribuição condicional.
Chamamos de atribuição condicional um operador que funciona como o seguinte tipo de lógica:

if (condicao)
   var=x;
else
   var=y;

O C oferece um operador que permite-nos fazer esse tipo de condição diretamente na atribuição, o operador ?. Esse é um operador ternário (ou seja, exige três operandos), sendo composto por <condicao>?<atr_verdadeiro>:<atr_falso>. Nesse caso, caso condicao seja verdadeira (diferente de 0), o operador irá devolver atr_verdadeiro, enquanto que, caso condicao seja falsa (igual a zero), ele irá devolver o valor de atr_falso. Por exemplo, imagine que você irá precisa do módulo de um número, ou seja, do valor sem sinal do mesmo. Ao invés de construir um if só para isso, você pode usar uma atribuição condicional:

modulo=(numero<0)?numero*-1:numero;

O que quer dizer que modulo irá receber o valor de numero vezes -1 caso numero seja menor que 0; caso contrário, ele irá receber o valor normal de numero.

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!