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:
00000000, 00000001, 00000010, . . . , 11111110, 11111111.
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 = 1×23 + 1×22 + 0×21 + 1×20. O conjunto de todas as sequências de k bits representa os números naturais de 0 a 2k−1.
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.
unsigned int i; for (i = 0; i < 65536; ++i) x[i] = 0;
int main( void) {
printf( "sizeof (unsigned int) = %d\n", sizeof (unsigned int));
printf( "sizeof (int) = %d\n", sizeof (int));
return 0;
}
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.
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⌋.
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.