C++ Inteiros

Números inteiros (sem vírgula) estão em todo programa: quantidade em estoque, idade, pontos de um jogo, número de alunos. O C++ tem uma família de tipos inteiros, de tamanhos diferentes, com e sem sinal de menos. Nesta aula você vai conhecer cada um, descobrir os limites reais de cada caixa e ver o que acontece quando um número não cabe: às vezes ele "dá a volta", às vezes o resultado é imprevisível. Também vai entender por que 7 / 2 dá 3.

Você vai aprender
  • Os tipos short, int, long, long long e as versões unsigned.
  • Os limites reais de cada um com std::numeric_limits.
  • O que é overflow (estouro): comportamento indefinido nos tipos com sinal, "volta ao zero" nos unsigned, e como flagrá-lo com -fsanitize=undefined.
  • Os tipos de tamanho fixo de <cstdint>: std::int8_t … std::uint64_t.
  • A divisão inteira (7 / 2), o resto (%) e a divisão por zero.
  • Como escrever números: 1'000'000, 0xFF, 0b1010, e a pegadinha do zero à esquerda.

Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana (usuária ana, computador notebook, Linux 64 bits), na pasta ~/cpp, com o comando padrão do guia: g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: o hodômetro do carro

O hodômetro (o contador de quilômetros) de um carro antigo tem um número fixo de rodinhas, por exemplo 5. Ele conta de 00000 até 99999. Rodando mais 1 km depois de 99999, ele não mostra 100000: não existe a sexta rodinha. Ele volta para 00000.

Os inteiros do computador são assim: cada tipo tem um número fixo de "rodinhas" (bits), e por isso um maior valor possível. Mais rodinhas, números maiores, mais memória gasta. Escolher o tipo inteiro é escolher quantas rodinhas a sua caixa tem.


1. A família dos inteiros

TipoTambém se escreveBytes (Linux 64 bits)Uso típico
shortshort int, signed short2Raro hoje; economizar memória em listas enormes
intsigned, signed int4O padrão para inteiros: contagens, idades, quantidades
longlong int8 (no Windows e no editor do guia: 4)Evite: o tamanho muda de sistema para sistema
long longlong long int8 (garantido: pelo menos 8)Números que passam de 2 bilhões

Todos esses são com sinal (signed): guardam negativos e positivos. As palavras short ("curto") e long ("longo") modificam o int, e por isso o int pode ser omitido: long long e long long int são o mesmo tipo. O tamanho de cada um foi medido com sizeof na aula Tipos de Dados.


2. Os limites: std::numeric_limits

Para saber o menor e o maior valor de um tipo, não precisa decorar: pergunte ao C++. O cabeçalho <limits> traz o std::numeric_limits ("limites numéricos"):

#include <iostream>
#include <limits>

int main() {
    std::cout << "short:     " << std::numeric_limits<short>::min()
              << " a " << std::numeric_limits<short>::max() << "\n";
    std::cout << "int:       " << std::numeric_limits<int>::min()
              << " a " << std::numeric_limits<int>::max() << "\n";
    std::cout << "long:      " << std::numeric_limits<long>::min()
              << " a " << std::numeric_limits<long>::max() << "\n";
    std::cout << "long long: " << std::numeric_limits<long long>::min()
              << " a " << std::numeric_limits<long long>::max() << "\n";
}
#include <limits>
Traz o std::numeric_limits.
std::numeric_limits<int>
"Os limites do tipo int". O tipo vai entre < >; esse jeito de passar um tipo é um template (aula Templates de Classe). Por enquanto, leia como "a ficha técnica do int".
::min() e ::max()
Perguntas feitas à ficha: o menor e o maior valor. O :: quer dizer "de dentro de".
$ g++ -std=c++23 -Wall -Wextra limites.cpp -o limites
$ ./limites
short:     -32768 a 32767
int:       -2147483648 a 2147483647
long:      -9223372036854775808 a 9223372036854775807
long long: -9223372036854775808 a 9223372036854775807

Em palavras: um int vai de cerca de menos 2,1 bilhões a mais 2,1 bilhões; um long long, até cerca de 9,2 quintilhões (9 seguido de 18 algarismos). Repare que o lado negativo tem um número a mais (-32768 × 32767): o zero ocupa uma vaga do lado positivo.

Quando o valor não cabe na caixa

A população do mundo (uns 8,1 bilhões) não cabe num int:

#include <iostream>

int main() {
    int populacao = 8'100'000'000;
    std::cout << "População mundial: " << populacao << "\n";
}
$ g++ -std=c++23 -Wall -Wextra populacao.cpp -o populacao
populacao.cpp: In function ‘int main()’:
populacao.cpp:4:21: warning: overflow in conversion from ‘long int’ to ‘int’ changes value from ‘8100000000’ to ‘-489934592’ [-Woverflow]
    4 |     int populacao = 8'100'000'000;
      |                     ^~~~~~~~~~~~~
$ ./populacao
População mundial: -489934592

Overflow in conversion from ‘long int’ to ‘int’ changes value = "estouro na conversão de long para int: muda o valor de 8100000000 para -489934592". O número escrito (8'100'000'000) é grande demais para ser int, então o próprio C++ o tratou como long; ao guardá-lo na caixa de int, as "rodinhas" que sobraram foram perdidas, e o resultado nem é positivo. Os apóstrofos em 8'100'000'000 são só separadores para facilitar a leitura (seção 7).

Missão: troque o tipo da variável para um que caiba 8,1 bilhões, rode e confira População mundial: 8100000000, sem avisos. (O Clang do editor avisa com outras palavras: implicit conversion from 'long long' to 'int' changes value. Lá aparece long long, e não long, porque no editor o long tem só 4 bytes.)

#include <iostream>

int main() {
    int populacao = 8'100'000'000;
    std::cout << "População mundial: " << populacao << "\n";
}
#include <iostream>

int main() {
    long long populacao = 8'100'000'000;
    std::cout << "População mundial: " << populacao << "\n";
}

3. unsigned: só positivos

Cada tipo inteiro tem uma versão sem sinal: unsigned short, unsigned int (ou só unsigned), unsigned long, unsigned long long. Ela não guarda negativos e, em troca, vai duas vezes mais longe nos positivos:

$ ./limites-unsigned          « o mesmo programa, com os tipos unsigned
unsigned short:     0 a 65535
unsigned int:       0 a 4294967295
unsigned long long: 0 a 18446744073709551615

Parece ótimo para um estoque, que nunca é negativo. Mas veja o que acontece quando a Ana vende uma peça a mais do que tinha:

#include <iostream>

int main() {
    unsigned int estoque = 10;
    unsigned int vendidos = 11;
    unsigned int sobrou = estoque - vendidos;
    std::cout << "Sobrou: " << sobrou << "\n";
}
$ g++ -std=c++23 -Wall -Wextra estoque.cpp -o estoque
$ ./estoque
Sobrou: 4294967295             « 10 - 11 deu mais de 4 bilhões

Como o unsigned não tem o -1, ele "deu a volta" para trás, como o hodômetro rodando de ré: de 0 pulou para o maior valor possível. Nenhum erro, nenhum aviso: só um estoque de 4 bilhões de peças. O mesmo acontece se você puser um negativo direto:

$ cat negativo.cpp
#include <iostream>

int main() {
    unsigned int a = -1;
    std::cout << a << "\n";
}
$ g++ -std=c++23 -Wall -Wextra negativo.cpp -o negativo
$ ./negativo
4294967295
$ cat negativo2.cpp
#include <iostream>

int main() {
    unsigned int b{-1};
    std::cout << b << "\n";
}
$ g++ -std=c++23 -Wall -Wextra negativo2.cpp -o negativo2
negativo2.cpp: In function ‘int main()’:
negativo2.cpp:4:20: error: narrowing conversion of ‘-1’ from ‘int’ to ‘unsigned int’ [-Wnarrowing]
    4 |     unsigned int b{-1};
      |                    ^~

Com =, silêncio. Com chaves, o compilador barra: -1 não cabe num unsigned (o mesmo narrowing da aula Variáveis).

Na dúvida, use int

"Nunca é negativo" não é motivo suficiente para usar unsigned: uma subtração errada vira um número gigante em vez de um negativo fácil de perceber. As diretrizes da comunidade C++ (as C++ Core Guidelines) recomendam int (ou long long) para quantidades e contas, e unsigned só quando você quer mesmo a "volta ao zero" ou trabalha com bits. Você ainda vai encontrar unsigned na biblioteca: o .size() de um std::string devolve um tipo sem sinal (aula Funções de String).


4. Overflow: quando a conta passa do limite

Overflow ("transbordar") é quando o resultado de uma conta não cabe no tipo. O que acontece depende de o tipo ter sinal ou não, e a diferença é importante.

Com sinal (int, long long…): comportamento indefinido

#include <iostream>
#include <limits>

int main() {
    int pontos = std::numeric_limits<int>::max();
    std::cout << "Antes: " << pontos << "\n";
    pontos = pontos + 1;
    std::cout << "Depois: " << pontos << "\n";
}
$ g++ -std=c++23 -Wall -Wextra overflow.cpp -o overflow
$ ./overflow
Antes: 2147483647
Depois: -2147483648            « o maior positivo + 1 virou o menor negativo

Parece que deu a volta, como o hodômetro. Mas a norma do C++ diz outra coisa: estourar um inteiro com sinal é comportamento indefinido (undefined behavior, "UB"). Quer dizer que a linguagem não promete nenhum resultado. Desta vez, com estas opções, saiu -2147483648; com otimização ligada, o compilador tem o direito de supor que o estouro nunca acontece e gerar um programa que faz qualquer outra coisa. A aula Comportamento Indefinido mostra exemplos disso.

Como achar um overflow que não dá erro? Com o sanitizer de comportamento indefinido: a opção -fsanitize=undefined ("sanitizar o indefinido") faz o g++ colocar fiscais no programa, que denunciam o problema na hora em que ele acontece:

$ g++ -std=c++23 -Wall -Wextra -fsanitize=undefined overflow.cpp -o overflow
$ ./overflow
Antes: 2147483647
overflow.cpp:7:12: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
Depois: -2147483648
$ echo $?
0
overflow.cpp:7:12
Arquivo, linha e coluna da conta culpada: pontos + 1, na linha 7.
runtime error
"Erro em tempo de execução": achado com o programa rodando, e não pelo compilador.
signed integer overflow
"Estouro de inteiro com sinal".
2147483647 + 1 cannot be represented in type 'int'
"2147483647 + 1 não pode ser representado no tipo int": a conta exata, com os valores daquele momento.

Por padrão, o sanitizer avisa e deixa o programa continuar (por isso o código de saída foi 0). Ele deixa o programa mais lento, então é ferramenta de teste, não de entrega. Mais em Sanitizers.

Quando a conta é feita só com números escritos no código, o próprio compilador percebe:

$ g++ -std=c++23 -Wall -Wextra literal-overflow.cpp -o literal-overflow     « int x = 2147483647 + 1;
literal-overflow.cpp: In function ‘int main()’:
literal-overflow.cpp:4:24: warning: integer overflow in expression of type ‘int’ results in ‘-2147483648’ [-Woverflow]
    4 |     int x = 2147483647 + 1;
      |             ~~~~~~~~~~~^~~

Sem sinal (unsigned): dá a volta, e isso é garantido

#include <iostream>
#include <limits>

int main() {
    unsigned int contador = std::numeric_limits<unsigned int>::max();
    std::cout << "Antes: " << contador << "\n";
    contador = contador + 1;
    std::cout << "Depois: " << contador << "\n";
}
$ g++ -std=c++23 -Wall -Wextra -fsanitize=undefined volta.cpp -o volta
$ ./volta
Antes: 4294967295
Depois: 0                      « o sanitizer ficou quieto: não é erro da linguagem

Para unsigned, a norma define o hodômetro: passou do máximo, volta a 0; passou do 0 para trás, vai ao máximo. Por isso o sanitizer não reclamou. "Definido" não quer dizer "certo para o seu programa": o estoque de 4 bilhões da seção 3 continua sendo um bug.

Com sinal (int…)Sem sinal (unsigned…)
Passou do máximoComportamento indefinidoVolta para 0 (garantido)
Passou do mínimoComportamento indefinidoVai para o máximo (garantido)
-fsanitize=undefinedDenuncia: signed integer overflowFica quieto
Como evitarEscolha um tipo com folga (long long) ou confira antes de somarConfira antes de subtrair

5. Tipos de tamanho fixo: <cstdint> C++11

Como int e long mudam de tamanho de sistema para sistema, o cabeçalho <cstdint> traz tipos com o tamanho no nome:

TipoBitsFaixa
std::int8_t / std::uint8_t8-128 a 127 / 0 a 255
std::int16_t / std::uint16_t16-32 768 a 32 767 / 0 a 65 535
std::int32_t / std::uint32_t32cerca de ±2,1 bilhões / 0 a 4,29 bilhões
std::int64_t / std::uint64_t64cerca de ±9,2 quintilhões / 0 a 18,4 quintilhões

Leia o nome assim: u = unsigned (sem sinal), int, o número de bits e _t (de type, tipo).

#include <cstdint>
#include <iostream>

int main() {
    std::int8_t pequeno = 100;
    std::int16_t medio = 30000;
    std::int32_t normal = 2000000000;
    std::int64_t grande = 9000000000000000000;
    std::uint8_t byte = 255;
    std::uint64_t enorme = 18000000000000000000u;
    std::cout << sizeof(pequeno) << " " << sizeof(medio) << " " << sizeof(normal) << " "
              << sizeof(grande) << " " << sizeof(byte) << " " << sizeof(enorme) << "\n";
    std::cout << medio << " " << normal << " " << grande << " " << enorme << "\n";
}
$ g++ -std=c++23 -Wall -Wextra fixos.cpp -o fixos
$ ./fixos
1 2 4 8 1 8
30000 2000000000 9000000000000000000 18000000000000000000

O u no fim de 18000000000000000000u diz "este número é sem sinal": sem ele, o número seria grande demais para qualquer tipo com sinal. Repare também que a segunda linha não mostra pequeno nem byte. Há um motivo:

A pegadinha do int8_t

#include <cstdint>
#include <iostream>

int main() {
    std::int8_t idade = 65;
    std::cout << "Idade: " << idade << "\n";
    std::cout << "Idade: " << static_cast<int>(idade) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra int8.cpp -o int8
$ ./int8
Idade: A                       « 65 virou a letra A
Idade: 65

No g++ (e em praticamente todo sistema), std::int8_t é só outro nome para signed char, um tipo de caractere. E o std::cout mostra caracteres como letras: o 65 é o código da letra A (a aula Caracteres explica a tabela de códigos). Para ver o número, converta para int com static_cast<int>(...) (aula Conversão de Tipos). O mesmo vale para std::uint8_t.

Quando usar os tipos fixos? Quando o tamanho exato importa: arquivos e protocolos com formato definido, contas que precisam de exatamente 64 bits em qualquer sistema. Para o dia a dia, int e long long bastam.


6. Divisão inteira: 7 / 2 dá 3

Quando os dois lados de uma divisão são inteiros, o resultado também é inteiro: a parte depois da vírgula é jogada fora (não arredondada). O operador % ("módulo") dá o resto dessa divisão:

#include <iostream>

int main() {
    std::cout << "7 / 2 = " << 7 / 2 << "\n";
    std::cout << "7 % 2 = " << 7 % 2 << "\n";
    std::cout << "-7 / 2 = " << -7 / 2 << "\n";
    std::cout << "7.0 / 2 = " << 7.0 / 2 << "\n";
    int pizzas = 3;
    int pessoas = 4;
    std::cout << "Cada um come " << pizzas / pessoas << " pizza\n";
    std::cout << "Cada um come " << static_cast<double>(pizzas) / pessoas << " pizza\n";
}
$ g++ -std=c++23 -Wall -Wextra divisao.cpp -o divisao
$ ./divisao
7 / 2 = 3
7 % 2 = 1
-7 / 2 = -3
7.0 / 2 = 3.5
Cada um come 0 pizza
Cada um come 0.75 pizza
ContaResultadoPor quê
7 / 23Inteiro com inteiro: 3,5 perde o ,5.
7 % 217 = 2 × 3 + 1. O resto mostra se um número é par (n % 2 dá 0) ou ímpar.
-7 / 2-3O corte é sempre "em direção ao zero": -3,5 vira -3 (e não -4).
7.0 / 23.5Basta um dos lados ser double para a divisão ter vírgula.
pizzas / pessoas03 / 4 = 0,75, cortado para 0. O erro clássico.
static_cast<double>(pizzas) / pessoas0.75Converter um dos lados para double antes de dividir resolve.

Missão: as notas da Ana foram 7, 8 e 8, mas o programa diz que a média é 7. A variável já é double; o problema está na divisão. Conserte para sair Média: 7.66667.

#include <iostream>

int main() {
    int soma = 7 + 8 + 8;
    double media = soma / 3;
    std::cout << "Média: " << media << "\n";
}
#include <iostream>

int main() {
    int soma = 7 + 8 + 8;
    double media = soma / 3.0;
    std::cout << "Média: " << media << "\n";
}

Por que double media não bastou? Porque o C++ calcula o lado direito primeiro (23 / 3, inteiro com inteiro, dá 7) e só depois guarda o resultado na caixa double: o 7 vira 7.0, e a fração já se perdeu. As contas e a ordem delas são assunto de Operadores Aritméticos.

Dividir um inteiro por zero

#include <iostream>

int main() {
    int balas = 10;
    int criancas = 0;
    std::cout << "Dividindo...\n";
    std::cout << balas / criancas << "\n";
    std::cout << "Fim\n";
}
$ g++ -std=c++23 -Wall -Wextra zero.cpp -o zero
$ ./zero
Dividindo...
Exceção de ponto flutuante (imagem do núcleo gravada) ./zero
$ echo $?
136

O programa morreu na linha 7: o Fim nunca apareceu. Divisão inteira por zero também é comportamento indefinido; no Linux, o processador se recusa a fazer a conta, e o sistema mata o programa. A mensagem vem do bash, em português, e engana: diz "ponto flutuante" (números com vírgula), mas a conta era de inteiros. É só o nome histórico do sinal SIGFPE. O código de saída 136 é 128 + 8, onde 8 é o número desse sinal. "Imagem do núcleo gravada" (core dumped) quer dizer que o sistema pode ter salvo uma cópia da memória do programa para análise. Com números double, a divisão por zero não mata o programa; veja float e double. Como evitar: confira o divisor antes, com um if.


7. Como escrever números inteiros (literais)

#include <iostream>

int main() {
    int populacao = 1'000'000;
    int cor = 0xFF;
    int bits = 0b1010;
    int octal = 017;
    long long distancia = 9'460'730'472'580'800LL;
    unsigned int positivo = 42u;
    std::cout << populacao << " " << cor << " " << bits << " " << octal << "\n";
    std::cout << distancia << " " << positivo << "\n";
    std::cout << std::hex << 255 << " " << std::oct << 15 << " " << std::dec << 255 << "\n";
}
$ g++ -std=c++23 -Wall -Wextra literais.cpp -o literais
$ ./literais
1000000 255 10 15
9460730472580800 42
ff 17 255
EscritaValeO que é
1'000'000 C++141000000O apóstrofo ' é só um separador visual, como o ponto de "1.000.000" em português. Pode ir em qualquer lugar do número.
0xFF255Hexadecimal (base 16, dígitos 0–9 e A–F), muito usado para cores e bytes. 0x = "hexa".
0b1010 C++1410Binário (base 2, só 0 e 1). 0b = "binário".
01715 (!)Um zero à esquerda quer dizer octal (base 8). Não escreva 017 pensando em 17.
42u42Sufixo u: sem sinal (unsigned).
42L / 42LL42Sufixos de long e long long. Use L maiúsculo: o l minúsculo parece o algarismo 1.
8'100'000'0008100000000Sem sufixo, mas grande demais para int: o C++ escolhe sozinho o primeiro tipo em que cabe (long no Linux; long long no editor do guia).

A última linha da saída mostra o caminho inverso: std::hex, std::oct e std::dec mudam a base em que o std::cout escreve os números (hexadecimal, octal, decimal). Mais em Formatar com iomanip.


Quando dá errado

warning: overflow in conversion from ‘long int’ to ‘int’ changes value

O que significa: o número escrito não cabe no tipo da variável.

Como consertar: use um tipo maior, normalmente long long.

runtime error: signed integer overflow: … cannot be represented in type 'int'

O que significa: o sanitizer flagrou uma conta que estourou um inteiro com sinal, na linha e coluna indicadas. Sem o sanitizer, o mesmo programa daria um número errado, sem aviso nenhum.

Como consertar: use um tipo com mais folga (long long) ou confira, antes da conta, se ela cabe (comparando com std::numeric_limits<int>::max()).

Um unsigned virou um número gigante, como 4294967295

O que significa: uma subtração deu negativo numa variável sem sinal, e ela "deu a volta".

Como consertar: use int para valores que entram em contas de subtração, ou confira se a >= b antes de calcular a - b.

A divisão deu 0 (ou perdeu a parte decimal)

O que significa: os dois lados eram inteiros, então a divisão foi inteira.

Como consertar: escreva um lado com ponto (3.0) ou converta: static_cast<double>(a) / b.

Exceção de ponto flutuante (imagem do núcleo gravada), código 136

O que significa: divisão (ou %) de inteiro por zero.

Como consertar: antes de dividir, confira se o divisor é diferente de zero.

Um int8_t ou uint8_t aparece como letra (ou como um símbolo estranho)

O que significa: esses tipos são tipos de caractere por dentro, e o std::cout os mostra como letras.

Como consertar: std::cout << static_cast<int>(x).


Referência rápida

EscritaO que faz
int x = 10;Inteiro com sinal, 4 bytes (±2,1 bilhões)
long long x = 10;Inteiro com sinal, 8 bytes (±9,2 quintilhões)
unsigned int x = 10;Sem sinal: 0 a 4,29 bilhões, dá a volta
std::int64_t x = 10;Exatamente 64 bits (#include <cstdint>)
std::numeric_limits<int>::max()Maior valor do tipo (#include <limits>)
a / b e a % bDivisão inteira (corta a fração) e resto
1'000'000, 0xFF, 0b1010Separador, hexadecimal, binário
-fsanitize=undefinedOpção do g++ que denuncia overflow com sinal ao rodar

Exercícios

1. Divisão inteira

Quanto vale 17 / 5 em C++?

2. Resto

Quanto vale 17 % 5?

3. unsigned int a = 5; unsigned int b = 6; Quanto vale a - b (no Linux 64 bits)?

4. O que a norma do C++ diz sobre somar 1 ao maior int?

5. Quanto vale o literal 010?

6. Qual é o tipo mais indicado para guardar a distância em quilômetros da Terra ao Sol (cerca de 150 milhões) e somar distâncias de várias viagens?

Missão final: o cronômetro

Missão: a corrida da Ana durou 3725 segundos. Usando só divisão inteira (/) e resto (%), calcule as horas, os minutos e os segundos e mostre 1 h 2 min 5 s. Dica: uma hora tem 3600 segundos; o resto da divisão por 3600 são os segundos que sobram para os minutos.

#include <iostream>

int main() {
    int total = 3725;
    // calcule horas, minutos e segundos

    // mostre no formato: 1 h 2 min 5 s
}
#include <iostream>

int main() {
    int total = 3725;
    int horas = total / 3600;
    int minutos = total % 3600 / 60;
    int segundos = total % 60;
    std::cout << horas << " h " << minutos << " min " << segundos << " s\n";
}

Resumo

  • Família: short (2 bytes), int (4, o padrão), long (varia), long long (8). Use int no dia a dia e long long para números acima de 2 bilhões.
  • std::numeric_limits<T>::min() e ::max() dão os limites reais.
  • unsigned: só positivos, o dobro de alcance, e dá a volta (10 - 11 = 4294967295). Na dúvida, prefira int.
  • Overflow com sinal é comportamento indefinido; -fsanitize=undefined denuncia (signed integer overflow). Com unsigned, dar a volta é garantido.
  • <cstdint>: std::int8_t … std::uint64_t têm tamanho exato. int8_t aparece como letra no cout.
  • Inteiro dividido por inteiro corta a fração (7 / 2 = 3); % dá o resto. Divisão por zero mata o programa.
  • Literais: 1'000'000, 0xFF, 0b1010; cuidado com o zero à esquerda (017 = 15).