Inteiros
Números inteiros são um conjunto de tipos de variaveis primitivas, indicam números sem casas decimais que podem ter valores negativos e positivos, ou limitados para apenas números positivos com o modificador unsigned.
A única diferença entre os diferentes tipos de inteiros são as regras para definição do seu tamanho e limites e algumas particularidades exclusivas dos tipos char, signed char e unsigned char que serão explicados em detalhes na seção sobre ponteiros.
Existem vários tipos de números inteiros, sendo eles (em ordem crescente de tamanho) :
charshortintlonglong long (adicionado no C99)
Regras para tamanho de inteiros
No geral o padrão do C não dá muitas garantias quanto aos tamanhos em bytes de inteiros, a única garantia real é que char é 1 byte e que os tipos “maiores” precisam atender a alguns requisitos mínimos.
Na prática a maioria dos sistemas modernos atende aos padrões conhecidos como “modelos de dados” que são os conjuntos de tamanhos de cada variável :
LP32: Utilizado pelo windows 16bits (não é mais tão moderno assim…)ILP32: Utilizado pelo windows 32bits e sistemas UNIX 32bits (Linux,MacOs e afins)LLP64: Utilizado pelo windows 64bitsLP64: Utilizado por sistemas UNIX 64bits (Linux, MacOs e afins)
Com isso é possível montar a tabela abaixo (relacionando a quantidade de bits de cada tipo) :
| Tipo | Padrão C | LP32 | ILP32 | LLP64 | LP64 |
|---|---|---|---|---|---|
char | Pelo menos 8 | 8 | 8 | 8 | 8 |
short | Pelo menos 16 | 16 | 16 | 16 | 16 |
int | Pelo menos 16 | 16 | 16 | 32 | 32 |
long | Pelo menos 32 | 32 | 32 | 32 | 64 |
long long | Pelo menos 64 | 64 | 64 | 64 | 64 |
Se atendo um pouco mais aos detalhes, o padrão do C não obriga 1 byte a ser 8bits, na verdade um byte é o menor valor endereçável da arquitetura, portanto apesar de todas as arquiteturas modernas usarem 8bits por byte, o padrão do C aceita arquiteturas que não seguem isso e expõe a definição CHAR_BIT que indica o número de bits em um byte.
Isso permitiria por exemplo bizarrices como arquiteturas que tem um byte com 64bits, o que possibilitaria que todos os tipos tivessem apenas 1 byte, logo, quanto a bytes a regra a ser seguida pelo C é a seguinte :
1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
Mas mesmo assim, a maioria das pessoas não está preocupada com arquiteturas obscuras que provavelmente nunca vão ver na vida, na prática as arquiteturas modernas e mesmo sistemas embarcados de hoje em dia em sua grande maioria usam 8bits por byte e a representação de complemento de dois para inteiros com sinal.
Limites de inteiros
Constantes relacionadas a limites de inteiros podem ser encontradas na biblioteca limits.h.
Ao assumir complemento de dois para os inteiros com sinal, podemos definir os seguintes limites para os dados :
| Tamanho em bits | Tem sinal | Limite inferior | Limite superior |
|---|---|---|---|
| 8 | Sim | -128 | 127 |
| 16 | Sim | -32768 | 32768 |
| 32 | Sim | -2147483648 | 2147483647 |
| 64 | Sim | -9223372036854775808 | 9223372036854775807 |
| 8 | Não | 0 | 255 |
| 16 | Não | 0 | 65535 |
| 32 | Não | 0 | 4294967295 |
| 64 | Não | 0 | 18446744073709551616 |
Mas e se eu te disser que você não precisa perder seu tempo decorando a tabela acima?
Todos esses valores podem ser facilmente calculados.
Para inteiros sem sinal o limite é :
\[-\frac{2^N}{2} \text{ até } \frac{2^N}{2}-1\]
Enquanto que para inteiros sem sinal, o limite é
\[0 \text{ até } 2^N-1\]
Onde N é o número de bits, logo fica fácil “descobrir” os limites de um tipo inteiro tendo uma calculadora a disposição.
Tipos de tamanho específico
Para facilitar o manuseio de inteiros, existem algumas definições específicas de tipos que estão presentes na biblioteca padrão pela inttypes.h que foi adicionada no C99.
Lá existem definições para inteiros de 8,16,32 e 64bits e suas respectivas versões com ou sem sinal. Utilizar estes tipos garante um tamanho fixo conhecido e facilita o manuseio dos mesmos além de compatibilidade maior com outros compiladores e processadores.
Eu poderia listar todos eles, mas é mais fácil entender a regra dos nomes, considere que pedaços em colchetes [], são opcionais e / são alternativas ao mesmo valor.
[u]int[_fast/_least]X_t
Xé o número de bits, podendo ser 8, 16, 32 ou 64ué o modificador para uma versão sem sinal do tipo_fasté o modificador para obter o tipo “mais eficiente para manuseio” que tenha pelo menos o tamanho especificado_leasté o modificador para obter o menor tipo que tenha “pelo menos aquele tamanho”- Na ausência de
fasteleast, o tipo tem EXATAMENTE a quantidade de bits emX.
Exemplos :
uint8_t: Tipo sem sinal com exatamente 8bitsint_fast32_t: Tipo com sinal com o tipo mais eficiente que tenha pelo menos 32bitsuint_least64_t: Inteiro sem sinal com pelo menos 64bits
Além destes tipos, existe o intmax_t que contêm o maior tipo inteiro com sinal e uintmax_t que contêm o maior tipo inteiro sem sinal.
Tipos inteiros adicionais
Além destes, existem alguns tipos inteiros adicionais presentes nas bibliotecas inttypes.h e stddef.h (incluida junto com stdlib.h).
Estes tipos são :
| Tipo | Descrição |
|---|---|
ptrdiff_t | Tipo resultante ao subtrair dois ponteiros |
size_t | Tipo que pode guardar o tamanho máximo teórico que um array pode ter |
max_align_t | Tipo que com o maior requisito de alinhamento possível (C11) |
intptr_t | Inteiro com sinal capaz de guardar qualquer ponteiro |
uintptr_t | Inteiro sem sinal capaz de guardar qualquer ponteiro |
O tipo size_t é o tipo resultante de toda expressão sizeof, e é o tipo ideal para guardar o tamanho máximo que objetos, arrays ou qualquer dado possa ter.
O tipo ptrdiff_t é utilizado para guardar diferenças entre ponteiros, e pode ser considerado como uma versão com sinal de size_t, visto que o tipo ssize_t normalmente definido e presente em sistemas POSIX não faz parte do padrão do C.
O propósito do POSIX ao utilizar ssize_t é a possibilidade de utilizar os valores negativos para indicar erros, reservando um bit do valor para erros.
max_align_t no geral é utilizado junto com o operador alignof.
Tipo _BitInt
Adicionado apenas no C23, o tipo é declarado como _BitInt(N) onde N é o número de bits que o tipo deve ter com cada valor de N sendo considerado um tipo diferente.
Os tipos _BitInt ainda podem ter o modificador unsigned e para valores com sinal, o número de N inclui o bit de sinal, de forma que _BitInt(1) não seja um tipo válido (pois não sobra nenhum bit pro valor).
A mesma regra descrita nos limites de inteiros se aplicam para calcular os limites de um valor do tipo _BitInt.
Inteiros definidos pela implementação
Desde o C99, existe a possibilidade dos compiladores terem tipos inteiros adicionais adicionados a linguagem, tipos como __uint128 e __int128 que simbolizam inteiros de 128 bits, porém o suporte e existência desses tipos depende da arquitetura e do compilador utilizado.
Overflow e underflow
Ao tratarmos de números inteiros, é comum o uso dos seguintes termos :
overflow: Em tradução literal, seria um “transbordamento”, esse termo é utilizado para indicar quando chegamos em um valor que vai além do limite que uma variável suporta.underflow: Em tradução literal, seria um “subtransbordamento”, esse termo é utilizado para indicar quando chegamos um valor abaixo do limite mínimo que uma variável suporta.
O comportamento padrão para calcularmos o valor resultante quando ocorre overflow ou underflow é :
overflow:VALOR_MAXIMO + Xse tornaráVALOR_MINIMO + (X - 1)paraX > 0underflow:VALOR_MINIMO - Xse tornaráVALOR_MAXIMO - (X - 1)paraX > 0
Para inteiros com sinal, o comportamento de overflow e underflow é indefinido, portanto qualquer checagem como i + 1 < i é transformada em false durante as otimizações, porém na prática quando um overflow ou underflow acontece e usamos complemento de dois (o que é o padrão), temos o comportamento descrito acima.
O teste abaixo demonstra um exemplo ao escrever um overflow e underflow de um inteiro com sinal de 8bits (lembre-se que pelas regras da linguagens, estamos invocando um comportamento indefinido):
#include <stdio.h>
#include <limits.h>
int main()
{
signed char test1 = SCHAR_MAX;
signed char test2 = SCHAR_MIN;
//"%hhd" é o modificador para escrever um signed char no printf
printf("%hhd %hhd\n", test1 + (signed char)1, test2 - (signed char)1);
}
Já para inteiros sem sinal, o overflow e underflow são definidos, e seguem o mesmo comportamento descrito, o que pode ser preocupante pois 0 - 1 gera o valor máximo, logo o perigo de underflow é muito maior. É por esse motivo que alguns autores aconselham evitar o uso de inteiros sem sinal, justamente pela facilidade de gerar um underflow ao realizar subtrações, no qual devemos ter muito cuidado sempre que realizamos subtrações.
Exemplo de overflow e underflow com inteiros sem sinal (neste caso, este comportamento é definido pela linguagem, garantindo que funcione em qualquer lugar):
#include <stdio.h>
#include <limits.h>
int main()
{
unsigned int test1 = UINT_MAX;
unsigned int test2 = 0;
//"%u" é o modificador para escrever um unsigned int no printf
printf("%u %u\n", test1 + 1U, test2 - 1U);
}
Aritmética Verificada
Desde o C23, a biblioteca stdckdint.h, foi adicionada, permitindo uma forma portável de realizar soma, subtração ou multiplicações checando se a operação teve sucesso sem overflow ou underflow através das macros ckd_add, ckd_sub, ckd_mul.
Todas as macros tem a seguinte sintaxe para chamada :
bool ckd_xxx(tipo1 *resultado, tipo2 a, tipo3 b);
Onde tipo1, tipo2 e tipo3 podem ser tipos diferentes, a verificação realizada é se a operação resultante com a e b pode ser guardada em resultado sem overflow/underflow ou truncamento, sendo tipo1 um ponteiro para o tipo guardando o resultado.
Todas as 3 operações funcionam com qualquer tipo inteiro que não seja char, bool, _BitInt(N), ou enum.
Lembrando que antes da existência dessa biblioteca, essas operações eram fornecidas como extensões de compiladores e, inclusive, são geralmente implementadas apontando as macros para as respectivas extensões já existentes.
Além disso a aritmética verificada geralmente é extremamente eficiente em alguns processadores, devido a presença de registradores de flags do processador que guardam se a última operação aritmética resultou em overflow/underflow, de forma que utilizar essas extensões seja absurdamente mais eficiente do que realizar a checagem por conta.
Para código escrito antes do C23, é possível implementar uma biblioteca que fornece a funcionalidade nos principais compiladores de C :
#if defined(__GNUC__) || defined(__clang__)
#define ckd_add(R, A, B) __builtin_add_overflow((A), (B), (R))
#define ckd_sub(R, A, B) __builtin_sub_overflow((A), (B), (R))
#define ckd_mul(R, A, B) __builtin_mul_overflow((A), (B), (R))
#elif defined(_MSC_VER)
//A Microsoft não fornece uma extensão de compilador "equivalente"
//mas tem uma biblioteca que realiza essas operações
//Utiliza a palavra chave _Generic adicionada no C11 para implementar a aritmética checada
#include <intsafe.h>
#define ckd_add(R, A, B) _Generic(*R, \
signed char:Int8Add, \
unsigned char:Uint8Add, \
short:ShortAdd, \
unsigned short:UShortAdd, \
int:IntAdd, \
unsigned int:UIntAdd, \
long:LongAdd, \
unsigned long: ULongAdd, \
long long:LongLongAdd, \
unsigned long long:ULongLongAdd, \
intptr_t:IntPtrAdd, \
size_t:SizeTAdd, \
ssize_t:SSizeTAdd, \
)(A,B,R)
#define ckd_sub(R, A, B) _Generic(*R, \
signed char:Int8Sub, \
unsigned char:Uint8Sub, \
short:ShortSub, \
unsigned short:UShortSub, \
int:IntSub, \
unsigned int:UIntSub, \
long:LongSub, \
unsigned long: ULongSub, \
long long:LongLongSub, \
unsigned long long:ULongLongSub, \
intptr_t:IntPtrSub, \
size_t:SizeTSub, \
ssize_t:SSizeTSub, \
)(A,B,R)
#define ckd_mul(R, A, B) _Generic(*R, \
signed char:Int8Mult, \
unsigned char:Uint8Mult, \
short:ShortMult, \
unsigned short:UShortMult, \
int:IntMult, \
unsigned int:UIntMult, \
long:LongMult, \
unsigned long: ULongMult, \
long long:LongLongMult, \
unsigned long long:ULongLongMult, \
intptr_t:IntPtrMult, \
size_t:SizeTMult, \
ssize_t:SSizeTMult, \
)(A,B,R)
#endif
Dicas para uso consciente de inteiros
No geral, aconselho utilizar signed char e unsigned char para representar bytes, int/unsigned int para inteiros genéricos onde o tamanho não é problema (todas as plataformas modernas tem geralmente um int de pelo menos 32bits, a menos que sejam processadores embarcados de 16/8 bits ou arquiteturas super específicas).
Em casos onde operações com bits ou tamanhos fixos são necessários, os tipos uintx_t e intx_t são utilizados para especificar um tamanho fixo.
O tipo size_t é importante também pois é normalmente utilizado para indicar tamanhos ou um inteiro do maior tamanho suportado nativamente pela plataforma.
Já o tipo ptrdiff_t é mais raro, mas pode ser usado de maneira similar ao ssize_t definido pelo POSIX (um tamanho de arrays/dados que também pode representar erros, utilizado geralmente em retorno de funções) ou realmente para diferenças entre ponteiros.