Projeto de Algoritmos

Números naturais e números inteiros

A memória de qualquer computador é uma sequência de bytes.  Cada byte consiste em 8 bits (= dígitos binários) e portanto tem 256 possíveis valores:

000000000000000100000010,  . . . ,  1111111011111111.

Como é possível representar um número natural  (0, 1, 2, 3 etc.)  em um byte ou em alguns bytes consecutivos? Como é possível representar um número inteiro, positivo ou negativo, em um byte ou em alguns bytes consecutivos? Como essas representações podem ser manipuladas para realizar operações aritméticas sobre os números?

O ponto de partida da representação é a notação binária: cada sequência de bits representa o resultado da soma das potências de 2 que correspondem aos bits 1. Por exemplo, a sequência 1101 representa o número 13, pois   13 = 23 + 22 + 21 + 20.   O conjunto de todas as sequências de k bits representa os números naturais de 0 a 2k−1.

Representação de números naturais

Na linguagem C, números naturais são conhecidos como "inteiros sem sinal".  Esse é um dos tipos-de-dados básicos da linguagem C.  Um variável n desse tipo é declarada assim:

   unsigned int n;

Um unsigned int é armazenado em s bytes consecutivos, sendo s o valor da expressão sizeof (unsigned int). Portanto, um unsigned int é representado por 8s bits e assim pode assumir 28s valores, a saber, 0,1,…,28s−1.  [O número 28s−1 está registrado na constante UINT_MAX, definida no arquivo limits.h.]  Em alguns computadores, s vale 2 e portanto um unsigned int tem 216 valores, de 0 a 65535. Em outros computadores, s vale 4 e assim um unsigned int tem 232 valores, que representam os números 0 a 4294967295.

Números naturais maiores que  28s−1  são representados módulo 28s.  Se s = 2, por exemplo, os números 65536 e 65537 são representados por 0 e 1 repectivamente. Em geral, o programador ignora esse efeito pois trabalha com números muito menores que 28s.

Para simplificar nossos exemplos, vamos imaginar que cada byte tem apenas 2 bits e não os 8 bits usuais. Vamos imaginar também que cada inteiro é armazenado em 2 bytes. Portanto, cada inteiro em nosso computador imaginário terá um total de 4 bits. Uma variável inteira sem sinal — tal como n no exemplo acima — poderá assumir 24 = 16 valores diferentes:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 .

A correspondência entre esses números e os padrões de 4 bits é dada em notação binária:
valor bits 
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001
10 1010
11 1011
12 1100
13 1101
14 1110
15 1111

Você deve imaginar que a tabela é cíclica: o fim é emendado com o começo.  Assim, por exemplo, os números 16 e 17 são representados por 0 e 1 respectivamente.

Exercícios

  1. Suponha que nosso computador representa cada inteiro em 2 bytes e que cada byte tem 8 bits.  Qual o efeito do seguinte fragmento de código?
       unsigned int i;
       for (i = 0; i < 65536; ++i) x[i] = 0;
    
  2. Suponha que precisamos contar o número de ocorrências de um certo fenômeno.  Sabemos de antemão que o fenômeno não ocorre mais que 65535 vezes.  Suponha que nosso computador representa cada inteiro em 2 bytes e que cada byte tem 8 bits.  Podemos usar uma variável do tipo unsigned int para contar as ocorrências do fenômeno?  Que fazer se o fenômeno pode ocorrer mais vezes? Proponha uma solução que use listas encadeadas para representar um contador em base 100 (ou base 256, ou base 65536). 
  3. Projeto de Programação.  Escreva um programa que receba um número inteiro positivo n e imprima as potências  n2, n3, n4, n5 etc.  O programa só deve parar quando não for capaz de armazenar uma potência em um unsigned int.
  4. Verificação dos tamanhos.  Compile e execute o seguinte programa:
       int main( void) {
          printf( "sizeof (unsigned int) = %d\n", sizeof (unsigned int));        
          printf( "sizeof (int) = %d\n", sizeof (int));        
          return 0;
       }
    

Representação de números inteiros

Números inteiros (positivos e negativos) são conhecidos em C como "inteiros com sinal". Para criar uma variável i deste tipo, basta dizer

   int i;

Cada int é armazenado em s bytes consecutivos, sendo s o valor da expressão sizeof (int) (igual ao valor de sizeof (unsigned int)).  Assim, cada int é representado por uma sequência de 8s bits e portanto tem 28s possíveis valores. O conjunto desses valores é

−28s−1, . . . , −1, 0, 1, . . . , 28s−1−1 .

Se s = 2, por exemplo, o conjunto de valores dos ints vai de −215 a 215−1, ou seja, de −32768 a 32767.  Se s = 4, o conjunto de valores dos ints vai de −231 a 231−1, ou seja, de −2147483648 a 2147483647.   [Os números −28s−1 e 28s−1−1 estão registrados nas constantes INT_MIN e INT_MAX, definidas no arquivo limits.h.]

Cada sequência de 8s bits que começa com 0 representa, em notação binária, um int não negativo. Cada sequência de 8s bits que começa com 1 representa o inteiro negativo k−28s, sendo k o valor da sequência em notação binária. Esta maneira de representar os inteiros negativos é conhecida como complemento-de-dois (ou two's-complement).

Números inteiros fora do intervalo −28s−1. . 28s−1−1 são representados módulo 28s (ou seja, um número inteiro j é representado pelo único i no intervalo −28s−1. . 28s−1−1 tal que a diferença j − i é um múltiplo inteiro de 28s).  As atribuições entre ints e unsigned ints também são feitas módulo 28s.   Se s = 2, por exemplo, os números −32769 e 32768 são representados por 32767 e −32768 respectivamente. Depois do fragmento de código

unsigned int n;  int i;
n = -1;  i = 65535;

o valor de n será 65535 e o valor de i será −1.

Para simplificar os exemplos, vamos imaginar, mais uma vez, que em nosso computador cada byte tem apenas 2 bits e não os 8 bits usuais. Portanto, cada inteiro em nosso computador imaginário terá um total de 4 bits. Uma variável inteira tal como i poderá assumir 24 = 16 valores diferentes.  A convenção complemento-de-dois usa esses 16 valores para representar os números

−8, −7, −6, −5, −4, −3, −2, −1, +0, +1, +2, +3, +4, +5, +6, +7 .

A tabela abaixo associa esse intervalo com os padrões de 4 bits:
valor bits 
−8 1000
−7 1001
−6 1010
−5 1011
−4 1100
−3 1101
−2 1110
−1 1111
+0 0000
+1 0001
+2 0010
+3 0011
+4 0100
+5 0101
+6 0110
+7 0111

Os inteiros positivos são representados em notação binária. Um inteiro negativo -k é representado pelo complemento de k relativo a 24[Outra maneira de dizer a mesma coisa: para descobrir o padrão de bits de um inteiro negativo, tome o padrão de bits do correspondente inteiro positivo, inverta todos os bits (ou seja, troque 0 por 1 e 1 por 0) e some 1 (em binário) ao resultado.]
            0
       -1      +1
    -2            +2
  -3                +3
                      
 -4                  +4
                      
  -5                +5
    -6            +6
       -7      +7
           -8

Você deve imaginar que a tabela é cíclica: o fim é emendado com o começo. As operações aritméticas seguem esse ciclo. Para somar 7 com 3, por exemplo, localize 7 na tabela e dê 3 passos no sentido horário. Assim,

7+3  vale  −6  (e não +10, como você gostaria),
7+1  vale  −8  (e não +8, como você esperava),
6×2  vale  −4  (e não +12, como querem os matemáticos).

Em todas estas somas ocorre overflow (= transbordamento). Isso não constitui um erro e o computador continua trabalhando normalmente.

Exercícios

  1. Suponha, como fizemos acima, que cada número inteiro é representado em nosso computador por 4 bits. Imagine agora que (ao contrário da convenção complemento-de-dois) cada inteiro negativo entre −7 e −1 é representado da seguinte maneira: primeiro, o valor absoluto do número é representado em binário, da maneira usual; depois, o bit mais significativo (ou seja, o que está mais à esquerda) é ligado (ou seja, assume valor 1).  Por exemplo, −5 é representado por  1101 .  Discuta as desvantagens desse sistema de representação de números negativos.

Aritmética int

As operações aritméticas envolvendo ints, unsigned ints, chars e unsigned chars são executadas como se seus operandos fossem do tipo int e produzem resultados do tipo int.  Em particular, os cálculos são feitos módulo 28s.  Por exemplo, depois do fragmento de código

unsigned int m, n;
int i;
m = 2; n = 3;
i = 32767;

o valor da expressão m-n é −1 e o valor da expressão i+1 é −32768.

Se o resultado de uma operação precisa ser reduzido módulo 28s para ficar no intervalo −28s−1. . 28s−1−1, dizemos que ocorreu um overflow aritmético. Em geral, o programador ignora a possibilidade de overflow pois trabalha com números de valor absoluto pequeno.

O resultado das divisões é truncado. A expressão 9/2, por exemplo, tem valor ⌊9/2⌋  (ou seja, o piso de 9/2).  No caso de números negativos, a divisão não obedece o operador piso:  na maioria dos computadores, a expressão -9/2 tem valor −⌊9/2⌋, diferente de ⌊-9/2⌋.

Representação de inteiros por sequências de caracteres

Números inteiros também podem ser representados por sequências de caracteres[Prefiro não dizer "por strings" para não me ver obrigado a acrescentar o caractere nulo ao final de cada sequência.]  O inteiro  −123,  por exemplo, pode ser representado pela sequência de caracteres

'-'    '1'    '2'    '3'

Na linguagem C, esta sequência de caracteres é representada por  "-123".  Se estivermos usando a tabela ISO 8859-1, a sequência de caracteres será

45    49    50    51

Como cada caractere é armazenado em um byte e cada byte tem 8 bits, o resultado é a sequência de quatro bytes

00101101 00110001 00110010 00110011

Para estabelecer o contraste entre essa representação e as anteriores, observe que a representação complemento-de-dois de −123 é

11111111 10000101

(confira!) supondo que cada int é armazenado em 2 bytes.

A representação de inteiros por sequências de caracteres não é muito econômica (consome muitos bytes). Além disso, fazer operações aritméticas diretamente sobre essa representação é uma loucura!  Por tudo isso, a representação não é usada na memória do computador.   Entretanto, a representação de inteiros por sequências de caracteres é muito usado em arquivos.  Se um arquivo usa esta maneira de representar inteiros, dizemos que ele está em "modo texto" (text file);  se usa a representação binária ou complemento-de-dois, dizemos que está em "modo binário" (binary file).

A função  atoi  (abreviatura de alphanumeric to integer) converte representação por sequência de caracteres em representação complemento-de-dois.  Por exemplo,

atoi( "9876")   vale   9876

e   atoi( "-9876")   vale   -9876 .  A função atoi faz parte da biblioteca stdlib, e portanto seu protótipo está no arquivo-interface stdlib.h.

 

 


URL of this site: www.ime.usp.br/~pf/algoritmos/
Last modified: Tue Apr 23 07:49:06 BRT 2013
Paulo Feofiloff
IME-USP

Valid HTML 4.01 Transitional    Valid CSS!