Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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) :

  • char
  • short
  • int
  • long
  • long 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 64bits
  • LP64: 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) :

TipoPadrão CLP32ILP32LLP64LP64
charPelo menos 88888
shortPelo menos 1616161616
intPelo menos 1616163232
longPelo menos 3232323264
long longPelo menos 6464646464

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 bitsTem sinalLimite inferiorLimite superior
8Sim-128127
16Sim-3276832768
32Sim-21474836482147483647
64Sim-92233720368547758089223372036854775807
8Não0255
16Não065535
32Não04294967295
64Não018446744073709551616

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 64
  • u é 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 fast e least, o tipo tem EXATAMENTE a quantidade de bits em X.

Exemplos :

  • uint8_t : Tipo sem sinal com exatamente 8bits
  • int_fast32_t: Tipo com sinal com o tipo mais eficiente que tenha pelo menos 32bits
  • uint_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 :

TipoDescrição
ptrdiff_tTipo resultante ao subtrair dois ponteiros
size_tTipo que pode guardar o tamanho máximo teórico que um array pode ter
max_align_tTipo que com o maior requisito de alinhamento possível (C11)
intptr_tInteiro com sinal capaz de guardar qualquer ponteiro
uintptr_tInteiro 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 + X se tornará VALOR_MINIMO + (X - 1) para X > 0
  • underflow: VALOR_MINIMO - X se tornará VALOR_MAXIMO - (X - 1) para X > 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.