LINGUAGEM: Linguagem C - Capítulo 1 - Parte 6



Operadores e Expressões: bit-a-bit e especiais

Objetivos:

Em C, OPERADORES e EXPRESSÕES podem ser classificados em cinco grande categorias:
  • Atribuições
  • Aritméticos
  • Lógicos e Relacionais
  • Bit-a-Bit
  • Especiais
Na última aula, disticutimos as três primeiras categorias acima. HOJE, iremos dar prosseguimento ao estudo apresentado os OPERADORES e EXPRESSÕES:
  • Bit-a-Bit
  • Especiais
Ao final, concluiremos o assunto mostrando um quadro que resume e compara os OPERADORES da linguagem C por ordem de precedência e associatividade.

Manipulação de Bits:

OPERADORES e EXPRESSÕES de manipulação de bits ( bit-a-bit ) referem-se a testar, atribuir ou deslocar os bits efetivos em um byte ou palavra que correspondem aos tipos básicos char, int e variantes.
OBS.:
  • Operações bit-a-bit não podem ser feitas sobre float, double, long double, void ou outros tipos mais complexos.

Significados:
  • Os operadores &, | e ~ têm a mesma tabela verdade que &&, || e ! (respectivamente), com a diferença que operam sobre os bits individuais da representação de dois números (ou apenas um, no caso de ~) e não sobre expressões numéricas.
  • o operador ^ tem a mesma tabela verdade que a função xor() discutida na aula passada (novamente operando sobre bits individuais e não sobre expressões numéricas).
  • o operador >> desloca os bits de um número k casas para à esquerda fazendo com que os k bits mais à esquerda se percam e os k bits mais à direita sejam '0'
  • o operador << desloca os bits de um número k casas para à direita fazendo com que os k bits mais à direita se percam e os k bits mais à esquerda sejam '0'
Exemplo 1 (<< e >>):
Considere:
unsigned int  x = 7;  
Se pudéssemos olhar a representação do valor 7 (decimal) 'dentro' da variável x, veríamos os seguinte padrão de bits: x = 0...00000111 ( a quantidade total de bits depende de quantos bytes são utilizados para armazenar um unsigned int, quantidade que depende da máquina alvo e compilador ).

A EXPRESSÃO abaixo (deslocamento à esquerda):
x = x << 2;    
faz com que o padrão de bits representando o valor inicial 7 se transforme em x = 0...00011100. Quer dizer, os bits, todos foram deslocados para a esquerda dois bits! Assim, o valor agora reresentado na variável x é 28 !

De maneira geral, se uma variável tem o seguinte padrão de bits:
  • x = | bn-1 | bn-2 | ... | b2 | b1 | b0 |
  • onde:
    • a variável é representada por n bits
    • cada um dos bi (para 0 ≤ i ≤ n-1 ) denota um bit com valor '0' ou '1'
então:
  • x << k, resulta em:
    • x = | bn-k-1 | bn-k-2 | ... | b2 | b1 | b0 | 0 | 0 | .. | 0 |0 | 0 |
    • onde a seq. final de '0's contém k '0's
  • x >> k, resulta em:
    • x = | 0 | 0 | ... | 0 | 0 | 0 | bn-1 | bn-2 | ... | bk-1 | bk |
    • onde a seq. inicial de '0's contém k '0's

Exemplo 2 (~):
Continuando o exemplo anterior, tem-se x = 0...00011100.

A EXPRESSÃO abaixo ( complemento de 1 ):
x = ~ x;
tem o seguinte efeito na representação de x em binário: x = 1...11100011 . Ou seja, os bits que são '0' passam a ser '1', os que são '1' tornam-se '0'.

Exemplo 3 (|):
Continuando o exemplo anterior, tem-se x = 1...11100011.

A EXPRESSÃO abaixo ( OR ):
x =  x | 8 ;
transforma x em : x = 1...11101011 . Quer dizer, muda o quarto bit (da direita para a esquerda) de '0' para '1'. Isto acontece por que:
x     = 1...11100011
4     = 0...00001000
      --------------
x | 4 = 1...11101011     /* OU bit a bit */
  • | é frequentemente utilizado para LIGAR bits particulares

Exemplo 4 (&):
Continuando o exemplo anterior, tem-se x = 1...11101011.

A EXPRESSÃO abaixo ( AND ):
x =  x & 0xf...f7 ;
transforma x em : x = 1...11100011 . Quer dizer, muda o quarto bit (da direita para a esquerda) de '1' para '0'. Isto acontece por que:
x            = 1...11101011
0xf...f7     = 1...11110111
             --------------
x | 0xf...f7 = 1...11100011     /* AND bit a bit */
  • & é frequentemente utilizado para DESLIGAR bits particulares

Aplicações Típicas:
  • rotinas de SO, drivers de dispositivos
  • criptografia
  • compactação de dados

Atividade 1:

Tendo em vista os significados de | e & que são, respectivamente, similares aos de || e &&, descreva - através de um exemplo - o significado de ^ que é similar à função xor() descrita na última aula.

Atividade 2:

Uma tabela binária é uma tabela (matriz linhas x colunas ) onde cada célula armazena um dentre dois valores possíveis. Por exemplo, a Freqüência deste curso é em essência uma tabela binária uma vez que cada célula só pode conter ou '0' (presente) ou '2' (ausente).
Uma maneira simples de representar uma tabela binária é através da seguinte estrutura:
char tabela1[40][10]; 
Nesta representação são gastos : 40*10 = 400 bytes ( sizeof char = 1 byte ).

Uma maneira mais eficiente (do ponto de vista da quantidade de memória utilizada) seria utilizar um mapa de bits para armazenar a tabela binária. Tal mapa de bits é uma sequencia de bits mantidas pelos operadores bit a bit.

Para a tabela acima de 40 linhas por 10 colunas poderíamos ter a seguinte declaração:
char tabela2[ (40*10) / 8 ];
Nesta representação são gastos : (40*10)/8 = 50 bytes. A idéia aqui é, ao invés de usar um char (1 byte) para armazenar 0 ou 1, usar um bit individual dentro de um char. Vejamos como isto pode ser feito.

Atribuindo valor a uma determinada célula (linha,coluna)

Na primeira representação acima, uma dada célula pode ter seu valor modificado da seguinte maneira:
tabela1 [3][5] = 0; /* aluno 3 esteve presente */ 
 tabela1 [7][5] = 2;     /* aluno 7 faltou */

Na segunda representação acima, o mesmo processo tem de ser simulado através de uma função:
void set(unsigned x, unsigned y, int val) {
 unsigned pos = ( x* 10 + y ) / 8;
 unsigned des = ( x* 10 + y ) % 8;

 if (val)
  tabela2[pos] |=  ( 1 << des );
 else
  tabela2[pos] &= ~( 1 << des );
}

Recuperado o valor em uma determinada célula (linha,coluna)

A consultar do valor em uma dada célula é feita de maneira direta para a primeira representação (utilizado índices linha e coluna). Já para a segunda representação devemos fazer uma função de consulta.
/* retorna   0 se na pos x,y está armazenado 0
    retona 1 se na pos x,y está armazenado 1
**/
int get(unsigned x, unsigned y) {
...
}
Como exercício, escreva a função de consulta get().

Operadores Especiais:

  • Operador Condicional ? :
  • Operador sizeof
  • Operadores de Conversão de Tipo ou Casting (tipo)
  • Operador Vírgula ,
  • Operadores () e []

Serão vistos em mais detalhes em aulas futuras:
  • Operadores de ponteiros * e &
  • Operadores de estruturas . e ->

Operador Condicional ? :

Uso
EXP1 ? EXP2 : EXP3;
Significado
EXP1 é avaliada ... Se ela for verdadeira, então EXP2 é avaliada e se torna o valor da expressão como um todo. ... Se EXP1 for falsa, então EXP3 é avaliada e se torna o valor da expressão como um todo.
Por exemplo, o comando condicional da função set() da atividade 2 poderia ser reescrito assim:
tabela2[ pos ]    =  val    ?   
   tabela2[pos] |    (1 << des) :
   tabela2[pos] &  ~ (1 << des) ;

b) Operador sizeof

Uso
sizeof  ID-VAR;
  sizeof  (ESP-TIPO);
Significado
sizeof é um operador unário em tempo de compilação que retorna o tamanho, em BYTES, de uma variável ou de um especificador de tipo de dados, este último escrito entre parênteses.
Por exemplo, para tornar o código da função set() da atividade 2 mais PORTÁVEL, podemos fazer:
char tabela2 [ (40*10) / ((sizeof char)*8) + 1 ];

void set(unsigned x, unsigned y, int val) {
 unsigned pos = ( x* 10 + y ) / ((sizeof char)*8);
 unsigned des = ( x* 10 + y ) % ((sizeof char)*8);
 ...
}

c) Conversão Explícita de tipos (casting)

Uso
(ESPECIFICADOR-DE-TIPO) EXPRESSÃO;
Significado
Força - explicitamente - uma EXPRESSÃO a ser de um determinado tipo dado por ESPECIFICADOR-DE-TIPO.

Por exemplo, para deixar claro que em:
tabela2 |= ( 1 << des);
o valor 1 se trata de um char ( 1 byte ) podemos fazer de forma explícita:
tabela2 |= (  ((char) 1)  << des ); 

Além de conversões explícitas, o compilador realiza algumas conversões implícitas ...

Conversões Automáticas


Exemplo:

d) Operador vígula ,

Uso
EXP1, EXP2;
Significado
O operador vígula é usado para encadear diversas expressões. O lado esquerdo (EXP1) é sempre avaliado como void. Assim, a expressão do lado direito (EXP2) torna-se o valor de toda a expressão separada por vírgula.
Por exemplo:
int x,y;

x = ( y = 3, y+1 );
primeiro o valor 3 é atribuído a y e, em seguida, o valor 4 é atribuído a x. Os parênteses são necessários porque o operador vírgula tem a menor precedência de todos os operadores de C.

Um outro exemplo é:
int i,x;

for ( i = 0 , x = 1;  i < 10;  i++ ,  x+=2) {
 ...
}

e) Operadores () e []

  • Parênteses () são operadores que aumentam a precedência das operações dentro deles.
  • Colchetes [] realizam indexação de vetores e matrizes.

Quadro Geral de Operadores em C:

A tabela 2.8 lista a precedência de todos os operadores de C. Note que todos os operadores, exceto os operadores unários e o ?, associam da esquerda para a direita. Os operadores unários ( *, ! e ~) e o ternário ? associam da direita para a esquerda.

Ordem de Avaliação:

O padrão C ANSI (C89) não estipula que as subexpressões de uma expressão devam ser avaliadas e uma ordem específica. Assim, seu código nunca deve contar com a ordem em que as subexpressões são avaliadas. Por exemplo, a expressão:
x =  f() + g();
não garante que f() será chamada antes de g()!

Leituras Recomendadas:

Exercícios

  1. Realize por completo a atividade 1
  2. Realize por completo a atividade 2, escrevendo a função get() e testando as funções set() e get() em um programa que lê do usário posições x e y, liga os bits nestas posições e ao final imprime a tabela completa.
  3. Observe o padrão abaixo:
    • ( (0000 0001)2 << 1 ) → (0000 0010)2 = 2
    • ( (0000 0010)2 << 1 ) → (0000 0100)2 = 4
    • ( (0000 0100)2 << 1 ) → (0000 1000)2 = 8
    • ( (0000 1000)2 << 1 ) → (0001 0000)2 = 16
    • ( (0001 0000)2 << 1 ) → (0010 0000)2 = 32
    • ...
    1. desse padrão, o que se pode concluir com relação à operação de deslocamento à esquerda?
    2. e com relação à operação de deslocamento à direita?
    3. em termos numéricos o que significa x << k ? Quer dizer, se x vale 5 quanto irá valer x << k ?
    4. em termos numéricos o que significa x >> k ? Quer dizer, se x vale 35 quanto irá valer x >> k ?
  4. Para o programa abaixo, descreva como ele funciona e qual a saída gerada após a sua execução.
#include <stdio.h>

int g(float a, float b) {

    static float f;
   
    return f += a > b ? a / b : b / a , (int) f;
    
}
      
int main() {
      
    printf("%i\n", g(7,2));
    printf("%i\n", g(2,3));
     
}

 
Bibliografia e fonte:

  • [CCT] Schildt, H. (1996) C, completo e total: 3a Ed.. São Paulo, Makron.
  • LP, UFMA; Coutinho, Lucian. Linguagem de programação para ciencia da computação da ufma.http://www.deinf.ufma.br/~lrc/2009.1/LP/
  • [K&R] KERNIGHAN, B. e RITCHIE, D. (1990) C, a linguagem de programação: padrão ANSI. Rio de Janeiro: Campus.
  • DEITEL, H. M. (1999) Como programar em C. Rio de Janeiro: LTC.
  • Módulo Consultoria e Informática (1989) Linguagem C: programação e aplicações. Rio de Janeiro: LTC.