A memória de qualquer computador é uma seqüência de bytes. Cada byte tem um endereço, que dá sua posição na memória. Cada byte consiste em 8 bits (= dígitos binários). Portanto, um byte tem 256 possíveis valores:
00000000, 00000001, 00000010, . . . , 11111110, 11111111.
Como é possível representar um número natural 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?
Na linguagem C, números naturais — 0, 1, 2, 3, etc. — 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;
Em alguns computadores, um unsigned int é armazenado em 2 bytes consecutivos. Em muitos outros, é armazenado em 4 bytes consecutivos. Ou seja, sizeof (unsigned int) vale 4 em muitos computadores. [O operador sizeof dá o número de bytes de um objeto.]
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 .
[Nos computadores reais temos 216 = 65 536 ou até 232 = 4 294 967 296 valores diferentes.] A correspondência entre os padrões de 4 bits e os números naturais é dada pela tabela abaixo:
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 |
É fácil entender a tabela: 13 corresponde à seqüência de bits 1101 porque
13 = 1×23 + 1×22 + 0×21 + 1×20.
Você deve imaginar que a tabela é cíclica: o fim é emendado com o começo, ou seja, depois do padrão 1111 vem, novamente, o padrão 0000. As operações aritméticas seguem o ciclo. [Um matemático diria que todas as operações aritméticas são executadas módulo 16.] Por exemplo,
9+6 vale 15, 9+7 vale 0, 9+8 vale 1 e 14×2 vale 12.
Nos três últimos exemplos ocorre overflow (= transbordo). Isso não constitui um erro e o computador continua trabalhando normalmente.
unsigned int i; for (i = 0; i < 65536; i++) x[i] = 0;
Há dois sabores de números inteiros: o positivo e o negativo. Os inteiros positivos podem ser representados em notação binária exatamente como os números naturais. Como é possível representar os inteiros negativos? Queremos uma representação que permita executar operações aritméticas de forma simples e mecânica.
Na linguagem C, os inteiros são conhecidos como "inteiros com sinal". Uma variável inteira i é declarada assim:
int i;
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 usual manda usar esses 16 valores para representar os seguintes números:
–8, –7, –6, –5, –4, –3, –2, –1, +0, +1, +2, +3, +4, +5, +6, +7 .
(Poderíamos representar o intervalo que vai de –7 a +8, ou o intervalo que vai de –6 a +9, ou algo ainda mais bizarro; mas isso não se faz normalmente.) Para associar esse intervalo com os padrões de 4 bits, usa-se a tabela abaixo, que segue a convenção conhecida como two's-complement:
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 padrões de bits que começam com 1 representam números negativos. Para descobrir o padrão de bits de um inteiro negativo faça o seguinte: tome o padrão de bits do correspondente número 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, ou seja, depois do padrão 0111 vem o padrão 1000. As operações aritméticas seguem esse ciclo. Para somar 7 com 3, por exemplo, localize 7 na tabela e dê 3 passos à frente, sem esquecer que a tabela é cíclica. Assim,
7+1 vale –8 (e não +8, como você gostaria),
7+3 vale –6 (e não +10, como você esperava),
6×2 vale –4 (e não +12, como querem os matemáticos).
Nas três últimas somas ocorre overflow (= transbordo). Isso não constitui um erro e o computador continua trabalhando normalmente.
Nos computadores reais, os valores de int vão usualmente de –231 a 231–1, ou seja, de –2 147 483 648 a 2 147 483 647. Os valores mínimo e máximo no seu computador estão registrados nas constantes INT_MIN e INT_MAX definidas no arquivo limits.h.
A possibilidade de overflow sugere que em trechos de código como
while (...) { ... i = i + 1; }
o programador deveria interromper o programa se i for igual a INT_MAX antes do incremento de i. Isso não se faz, entretanto, pois em geral outras partes do código garantem indiretamente que i ≤ INT_MAX.
Há uma outra representação possível para números inteiros: um inteiro como –123, por exemplo, é representado pela seqüência de caracteres
'-' '1' '2' '3'
Na linguagem C, esta cadeia de caracteres (= string) é representada por "-123". Se estivermos usando a tabela ISO 8859-1, a seqüência de caracteres será
45 49 50 51
Como cada caracter é armazenado em um byte e cada byte tem 8 bits, o resultado é a seqüência de quatro bytes
00101101 00110001 00110010 00110011
Para estabelecer o contraste entre essa representação e as anteriores, observe que a representação two's-complement de –123 é
11111111 10000101
(confira!) supondo que cada int é armazenado em 2 bytes. A representação de inteiros por strings 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 strings é muito usado em arquivos. Se um arquivo usa essa maneira de representar inteiros, dizemos que ele está em "modo texto" (text file); se usa a representação binária ou two's-complement, dizemos que está em "modo binário" (binary file).
A função atoi (abreviatura de alphanumeric to integer) converte representação por string em representação "two's-complement". Por exemplo,
atoi ("9876") vale 9876
e atoi ("-9876") vale -9876 . [Pergunta irrelevante: quanto vale atoi ("C3PO")?] A função atoi faz parte da biblioteca stdlib, e portanto seu protótipo está no arquivo-interface stdlib.h.