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.
- Os tipos
short,int,long,long longe as versõesunsigned. - 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
| Tipo | Também se escreve | Bytes (Linux 64 bits) | Uso típico |
|---|---|---|---|
short | short int, signed short | 2 | Raro hoje; economizar memória em listas enormes |
int | signed, signed int | 4 | O padrão para inteiros: contagens, idades, quantidades |
long | long int | 8 (no Windows e no editor do guia: 4) | Evite: o tamanho muda de sistema para sistema |
long long | long long int | 8 (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 doint". - ::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).
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áximo | Comportamento indefinido | Volta para 0 (garantido) |
| Passou do mínimo | Comportamento indefinido | Vai para o máximo (garantido) |
-fsanitize=undefined | Denuncia: signed integer overflow | Fica quieto |
| Como evitar | Escolha um tipo com folga (long long) ou confira antes de somar | Confira 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:
| Tipo | Bits | Faixa |
|---|---|---|
std::int8_t / std::uint8_t | 8 | -128 a 127 / 0 a 255 |
std::int16_t / std::uint16_t | 16 | -32 768 a 32 767 / 0 a 65 535 |
std::int32_t / std::uint32_t | 32 | cerca de ±2,1 bilhões / 0 a 4,29 bilhões |
std::int64_t / std::uint64_t | 64 | cerca 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
| Conta | Resultado | Por quê |
|---|---|---|
7 / 2 | 3 | Inteiro com inteiro: 3,5 perde o ,5. |
7 % 2 | 1 | 7 = 2 × 3 + 1. O resto mostra se um número é par (n % 2 dá 0) ou ímpar. |
-7 / 2 | -3 | O corte é sempre "em direção ao zero": -3,5 vira -3 (e não -4). |
7.0 / 2 | 3.5 | Basta um dos lados ser double para a divisão ter vírgula. |
pizzas / pessoas | 0 | 3 / 4 = 0,75, cortado para 0. O erro clássico. |
static_cast<double>(pizzas) / pessoas | 0.75 | Converter 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
| Escrita | Vale | O que é |
|---|---|---|
1'000'000 C++14 | 1000000 | O apóstrofo ' é só um separador visual, como o ponto de "1.000.000" em português. Pode ir em qualquer lugar do número. |
0xFF | 255 | Hexadecimal (base 16, dígitos 0–9 e A–F), muito usado para cores e bytes. 0x = "hexa". |
0b1010 C++14 | 10 | Binário (base 2, só 0 e 1). 0b = "binário". |
017 | 15 (!) | Um zero à esquerda quer dizer octal (base 8). Não escreva 017 pensando em 17. |
42u | 42 | Sufixo u: sem sinal (unsigned). |
42L / 42LL | 42 | Sufixos de long e long long. Use L maiúsculo: o l minúsculo parece o algarismo 1. |
8'100'000'000 | 8100000000 | Sem 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()).
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.
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.
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
| Escrita | O 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 % b | Divisão inteira (corta a fração) e resto |
1'000'000, 0xFF, 0b1010 | Separador, hexadecimal, binário |
-fsanitize=undefined | Opçã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). Useintno dia a dia elong longpara 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, prefiraint.- Overflow com sinal é comportamento indefinido;
-fsanitize=undefineddenuncia (signed integer overflow). Comunsigned, dar a volta é garantido. <cstdint>:std::int8_t…std::uint64_ttêm tamanho exato.int8_taparece como letra nocout.- 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).