Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

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.

5 Respostas para “Uma aprofundada em operadores e lógica em C

  1. Pingback: Matrizes e Ponteiros – Parte 1 « Aulas de C

  2. Pingback: Matrizes e Ponteiros – Parte 2 – Alocação dinâmica de memória « Aulas de C

  3. Pingback: Matrizes e Ponteiros – Parte 2 – Alocação dinâmica de memória « Aulas de C

  4. José 12/03/2014 às 18:49

    Obrigado pela ajuda

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: