C++ Números Aleatórios
Jogar um dado, sortear o ganhador da rifa, embaralhar as perguntas de um quiz: tudo isso precisa de números aleatórios, números que o programa não sabe de antemão. Mas um computador é uma máquina que segue instruções à risca: como ele pode "tirar a sorte"? Nesta aula você vai entender o truque e usar a biblioteca <random> do C++, peça por peça, até montar um dado de verdade. No fim, vai conhecer o jeito antigo (rand()), que ainda aparece em muitos tutoriais, e por que é melhor evitá-lo.
- O que é um gerador de números "pseudoaleatórios" e o que é a semente.
- As três peças da
<random>:std::random_device,std::mt19937e as distribuições. - Como fazer um dado de 1 a 6 com
std::uniform_int_distribution. - Como sortear números com vírgula com
std::uniform_real_distribution. - Como usar uma semente fixa para repetir os mesmos sorteios (ótimo para testar).
- O antigo
rand()/srand(time(0))e os problemas dele.
Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Quando o programa usa uma semente que muda a cada vez, os números que você vai ver no seu computador serão outros: é esse o objetivo.
A ideia: o livro de números e a página em que você abre
Imagine um livro grosso cheio de números embaralhados, página após página: 3 5 6 2 5 1 4…. Se você abre numa página qualquer e vai lendo, os números parecem não ter padrão nenhum. Mas, se outra pessoa abrir na mesma página, vai ler exatamente os mesmos números.
O gerador de números do computador funciona assim:
| No livro | No C++ | Nome técnico |
|---|---|---|
| O livro de números embaralhados | Uma fórmula que, a partir de um número, calcula o próximo, e o próximo… | gerador pseudoaleatório (std::mt19937) |
| A página em que você abre | O número de partida da fórmula | semente (em inglês, seed) |
| Alguém que abre o livro numa página "de surpresa" | Um dispositivo que dá um número imprevisível para servir de semente | std::random_device |
"Pseudo" quer dizer "falso": os números parecem aleatórios, mas são calculados. A mesma semente sempre dá a mesma sequência. Isso, que parece defeito, é muito útil: com uma semente fixa, você consegue repetir um sorteio para testar o programa. Para um jogo de verdade, você usa uma semente diferente a cada vez.
1. O dado de 1 a 6
Comece pelo programa completo. Depois, cada linha é explicada.
#include <iostream>
#include <random>
int main() {
std::random_device semente;
std::mt19937 gerador(semente());
std::uniform_int_distribution<int> dado(1, 6);
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << "\n";
}
- #include <random>
- A biblioteca de números aleatórios (C++11).
- std::random_device semente;
- Cria um
random_device("dispositivo aleatório") chamadosemente. Ele pede ao sistema operacional (ou ao próprio processador) um número realmente imprevisível, tirado de "ruído" do hardware. É lento demais para usar o tempo todo, então serve só para escolher a página do livro. - semente()
- Os parênteses pedem um número ao dispositivo. Cada vez, sai um diferente.
- std::mt19937 gerador(semente());
- Cria o gerador (o livro), chamado
gerador, aberto na página que orandom_devicesorteou. O nome estranho vem do algoritmo Mersenne Twister, criado em 1997; o 19937 vem de um número da fórmula. Ele é rápido e produz números de 0 a 4294967295. - std::uniform_int_distribution<int> dado(1, 6);
- Cria uma distribuição chamada
dado: a peça que transforma os números enormes do gerador em números inteiros (<int>) de 1 a 6, incluindo o 1 e o 6. Uniform ("uniforme") quer dizer que todos têm a mesma chance, como num dado honesto. - dado(gerador)
- Joga o dado: a distribuição pega um número do gerador e o converte para 1 a 6. Cada chamada é uma jogada nova.
$ g++ -std=c++23 -Wall -Wextra dado.cpp -o dado $ ./dado; ./dado 5 1 2 1 3 1 1 2 4 6 « cada execução, uma sequência diferente
Rodando de novo, sai outra sequência. No seu computador, os números serão outros. Repare que números repetidos (1 1) são normais: num dado de verdade também acontece.
sortear(1, 6)?
Porque cada peça faz uma coisa só, e você pode trocar uma sem mexer nas outras: o mesmo gerador serve para um dado de 6 faces, um de 20, um preço com centavos… Você cria um gerador só no começo do programa e quantas distribuições quiser.
Missão: rode algumas vezes e veja os números mudarem. Depois, transforme o dado de 6 faces num dado de 20 faces (os de RPG), trocando só os números da distribuição.
#include <iostream>
#include <random>
int main() {
std::random_device semente;
std::mt19937 gerador(semente());
std::uniform_int_distribution<int> dado(1, 6);
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << "\n";
}
#include <iostream>
#include <random>
int main() {
std::random_device semente;
std::mt19937 gerador(semente());
std::uniform_int_distribution<int> dado(1, 20);
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << "\n";
}
2. Semente fixa: repetir os mesmos sorteios
Agora troque o random_device por um número escrito no código, por exemplo 42. É como combinar com todo mundo: "abram o livro na página 42".
#include <iostream>
#include <random>
int main() {
std::mt19937 gerador(42);
std::uniform_int_distribution<int> dado(1, 6);
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << " ";
std::cout << dado(gerador) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra fixa.cpp -o fixa $ ./fixa; ./fixa 3 5 6 2 5 3 5 6 2 5 « mesma semente, mesma sequência, sempre
Quando a semente fixa é útil:
- Testar e consertar o programa. Um bug que só aparece "às vezes" é um pesadelo. Com semente fixa, ele aparece toda vez, do mesmo jeito.
- Simulações que outra pessoa precisa conferir. Basta dizer qual semente você usou.
- Jogos com "fase do dia", em que todo mundo recebe o mesmo desafio: a semente pode ser a data.
Duas variáveis-gerador com a mesma semente também andam juntas, cada uma no seu próprio livro:
#include <iostream>
#include <random>
int main() {
std::uniform_int_distribution<int> dado(1, 6);
std::mt19937 g1(7);
std::cout << dado(g1) << " " << dado(g1) << " " << dado(g1) << "\n";
std::mt19937 g2(7);
std::cout << dado(g2) << " " << dado(g2) << " " << dado(g2) << "\n";
}
$ ./dois 1 2 5 1 2 5
É por isso que você deve criar o gerador uma vez só, no começo. Se criar um gerador novo com a mesma semente toda vez que for jogar o dado (por exemplo, dentro de uma repetição, aula for), vai tirar sempre o mesmo número.
O gerador std::mt19937 é igual em todo lugar: com a semente 42, o primeiro número "cru" (gerador(), sem distribuição) é sempre 1608637542, em qualquer compilador. Já as distribuições podem ser implementadas de jeitos diferentes por cada biblioteca padrão. Então o 3 5 6 2 5 acima vale para o g++ do Linux; no editor do guia (Clang, no navegador) ou no Visual Studio, a mesma semente 42 pode dar outros números, que também se repetem a cada execução. Por isso as missões desta aula não dependem do valor sorteado.
#include <iostream>
#include <random>
int main() {
std::mt19937 gerador(42);
std::cout << gerador() << "\n";
std::cout << gerador() << "\n";
std::cout << gerador.min() << " a " << gerador.max() << "\n";
}
$ ./cru 1608637542 « o mesmo em qualquer compilador 3421126067 0 a 4294967295 « a faixa do gerador "cru"
3. Números com vírgula: uniform_real_distribution
Para sortear um preço entre R$ 10 e R$ 20, ou uma chance entre 0 e 1, use a distribuição de números reais (com vírgula):
#include <iostream>
#include <random>
int main() {
std::mt19937 gerador(42);
std::uniform_real_distribution<double> preco(10.0, 20.0);
std::uniform_real_distribution<double> zero_um(0.0, 1.0);
std::cout << preco(gerador) << "\n";
std::cout << preco(gerador) << "\n";
std::cout << zero_um(gerador) << "\n";
}
$ ./real; ./real 17.9654 11.8343 0.779691 17.9654 « semente fixa: repetiu 11.8343 0.779691
- uniform_real_distribution<double>
- Distribuição uniforme de números
double. - preco(10.0, 20.0)
- Números a partir de 10.0 e menores que 20.0. Diferente da versão de inteiros, o limite de cima não entra (na prática, a diferença é invisível).
- zero_um(gerador)
- Um número entre 0 e 1. Serve para "chances": se sair menor que
0.3, aconteceu algo que tem 30% de chance. Você vai fazer isso com um if.
Para mostrar o preço com duas casas (17.97), veja Formatar com iomanip.
4. O jeito antigo: rand() e srand(time(0))
Em livros e tutoriais antigos (e em muito código da linguagem C), você vai encontrar isto:
#include <iostream>
#include <cstdlib>
#include <ctime>
int main() {
std::srand(std::time(0));
std::cout << std::rand() % 6 + 1 << " ";
std::cout << std::rand() % 6 + 1 << " ";
std::cout << std::rand() % 6 + 1 << "\n";
}
- <cstdlib>, <ctime>
- Bibliotecas herdadas do C: a de utilidades gerais e a de tempo.
- std::time(0)
- A hora atual, em segundos contados desde 1º de janeiro de 1970.
- std::srand(...)
- seed rand: escolhe a semente do gerador antigo. Aqui, a hora atual.
- std::rand()
- Devolve um número de 0 a
RAND_MAX(no Linux, 2147483647). - % 6 + 1
- O resto da divisão por 6 dá de 0 a 5 (aula Operadores Aritméticos); somando 1, de 1 a 6.
Funciona… mais ou menos. Olhe o que acontece quando a Ana roda o programa duas vezes seguidas e, depois, espera um segundo:
$ ./velho; ./velho; sleep 1; ./velho 2 3 6 2 3 6 « no mesmo segundo, a mesma semente: o mesmo "sorteio" 2 2 6
E sem o srand, o "sorteio" é sempre igual, em todo computador Linux:
#include <iostream>
#include <cstdlib>
int main() {
std::cout << std::rand() << "\n";
std::cout << std::rand() << "\n";
std::cout << RAND_MAX << "\n";
}
$ ./semsrand; ./semsrand 1804289383 846930886 2147483647 1804289383 « a mesma coisa de novo 846930886 2147483647
Por que evitar o rand()
| Problema | O que acontece |
|---|---|
| Semente pelo relógio | time(0) muda só uma vez por segundo. Dois programas iniciados no mesmo segundo "sorteiam" igual (você viu acima). |
| Faixa pequena e indefinida | RAND_MAX pode ser só 32767 em alguns sistemas (no Windows, é). No Linux é maior, mas você não controla isso. |
O % 6 é "viciado" | Se a faixa do gerador não é divisível por 6, alguns resultados saem um pouquinho mais que outros. O uniform_int_distribution corrige isso. |
| Qualidade fraca | O padrão C++ não exige nada do algoritmo do rand(); em alguns sistemas, os números têm padrões visíveis. |
| Um gerador só, para o programa todo | Qualquer parte do programa que chame rand() muda a sequência de todas as outras. |
Resumo: reconheça o rand() quando aparecer em código antigo, mas, no seu código, use a <random>.
Formas diferentes + qual usar
| Objetivo | Forma | Quando usar |
|---|---|---|
| Sorteio diferente a cada execução | std::mt19937 g(std::random_device{}()); | Jogos, sorteios de verdade. (O std::random_device{}() cria o dispositivo e pede um número numa linha só.) |
| Sorteio repetível | std::mt19937 g(42); | Testes, depuração, simulações para conferir. |
| Inteiro de a até b | std::uniform_int_distribution<int> d(a, b); | Dados, rifas, posição numa lista. Inclui a e b. |
| Número com vírgula | std::uniform_real_distribution<double> d(a, b); | Preços, chances, coordenadas. Inclui a, não inclui b. |
| Número imprevisível direto | std::random_device rd; rd(); | Só para a semente. Para muitos números, é lento. |
| Jeito antigo | std::srand(std::time(0)); std::rand() % 6 + 1 | Evite. Só para entender código antigo. |
O mt19937 é ótimo para jogos, mas não serve para gerar senhas, códigos de recuperação ou chaves: quem vê alguns números consegue calcular os próximos. Para isso, use bibliotecas de criptografia, que pedem números diretamente ao sistema operacional.
Quando dá errado
error: no match for call to ‘(std::uniform_int_distribution<int>) ()’
std::mt19937 gerador(42);
std::uniform_int_distribution<int> dado(1, 6);
std::cout << dado() << "\n";
$ g++ -std=c++23 -Wall -Wextra errogerador.cpp -o errogerador
errogerador.cpp: In function ‘int main()’:
errogerador.cpp:7:22: error: no match for call to ‘(std::uniform_int_distribution<int>) ()’
7 | std::cout << dado() << "\n";
| ~~~~^~
…
/usr/include/c++/15/bits/uniform_int_dist.h:203:9: note: candidate expects 1 argument, 0 provided
…
/usr/include/c++/15/bits/uniform_int_dist.h:208:9: note: candidate expects 2 arguments, 0 provided
O que significa: "nenhuma combinação para chamar dado() sem nada entre os parênteses". As notas dizem que a versão que existe "espera 1 argumento, e 0 foram dados". A distribuição não sorteia sozinha: ela precisa do gerador.
Como consertar: passe o gerador: dado(gerador).
error: static assertion failed: template argument must be an integral type
std::uniform_int_distribution<double> nota(0, 10);
$ g++ -std=c++23 -Wall -Wextra errodouble.cpp -o errodouble
In file included from /usr/include/c++/15/bits/stl_algo.h:65,
from /usr/include/c++/15/bits/unicode.h:39,
from /usr/include/c++/15/format:61,
from /usr/include/c++/15/ostream:44,
from /usr/include/c++/15/iostream:43,
from errodouble.cpp:1:
/usr/include/c++/15/bits/uniform_int_dist.h: In instantiation of ‘class std::uniform_int_distribution<double>’:
errodouble.cpp:6:47: required from here
6 | std::uniform_int_distribution<double> nota(0, 10);
| ^
/usr/include/c++/15/bits/uniform_int_dist.h:90:49: error: static assertion failed: template argument must be an integral type
90 | static_assert(std::is_integral<_IntType>::value,
| ^~~~~
/usr/include/c++/15/bits/uniform_int_dist.h:90:49: note: ‘std::integral_constant<bool, false>::value’ evaluates to false
O que significa: o erro aparece dentro de um arquivo da biblioteca (uniform_int_dist.h), mas a culpa é sua: a linha required from here aponta a sua linha 6. "Afirmação estática falhou: o tipo precisa ser inteiro". A distribuição _int_ só aceita tipos inteiros, e recebeu double.
Como consertar: para números com vírgula, use std::uniform_real_distribution<double>. Para inteiros, uniform_int_distribution<int>.
O que significa: o gerador está com semente fixa (std::mt19937 gerador(42);), ou sem semente nenhuma (std::mt19937 gerador; usa uma semente padrão, sempre a mesma), ou você está criando um gerador novo a cada jogada.
Como consertar: crie um gerador no começo, com std::random_device como semente, e use-o para todas as jogadas.
O que significa: com o jeito antigo, a conta ficou errada: rand() % 6 dá de 0 a 5, e rand() % 7 dá de 0 a 6.
Como consertar: use std::uniform_int_distribution<int> dado(1, 6);, em que você escreve os limites diretamente, sem conta.
Referência rápida
| Escrita | O que faz |
|---|---|
#include <random> | Traz geradores e distribuições |
std::random_device rd; | Cria a fonte de números imprevisíveis |
rd() | Pede um número imprevisível (use como semente) |
std::mt19937 g(rd()); | Gerador com semente imprevisível |
std::mt19937 g(42); | Gerador com semente fixa (sequência repetível) |
std::uniform_int_distribution<int> d(1, 6); | Inteiros de 1 a 6, todos com a mesma chance |
std::uniform_real_distribution<double> d(0.0, 1.0); | Reais de 0 até (sem incluir) 1 |
d(g) | Sorteia um número |
std::srand(std::time(0)); std::rand() | Jeito antigo (C); evite |
A <random> tem outras distribuições, por exemplo std::normal_distribution (a "curva do sino", para simular alturas ou notas em torno de uma média) e std::bernoulli_distribution (sim ou não, com uma chance). E std::shuffle (de <algorithm>) embaralha uma lista inteira com um gerador; você vai precisar de listas, que chegam em std::vector.
Exercícios
1. A distribuição
Complete para criar uma distribuição de inteiros de 1 a 100:
2. Semente fixa
Complete para criar o gerador com a semente fixa 2026:
3. Para que serve o std::random_device num programa típico?
4. O programa usa std::mt19937 g(42); e é executado duas vezes no mesmo computador. Os números sorteados…
5. Por que std::srand(std::time(0)) pode dar o mesmo sorteio em dois programas?
Missão final: a rifa da escola
Missão: a escola da Ana vai sortear um número da rifa (de 1 a 100) e um desconto entre 5% e 15% (com vírgula) na cantina. Para poderem conferir o sorteio depois, use um gerador com a semente fixa 2026. Mostre Número sorteado: seguido do número e Desconto: seguido do desconto e de %. Os valores exatos dependem do compilador; a missão confere só o formato.
#include <iostream>
#include <random>
int main() {
// 1. crie o gerador com a semente 2026
// 2. crie as duas distribuições
// 3. sorteie e mostre
}
#include <iostream>
#include <random>
int main() {
std::mt19937 gerador(2026);
std::uniform_int_distribution<int> rifa(1, 100);
std::uniform_real_distribution<double> desconto(5.0, 15.0);
std::cout << "Número sorteado: " << rifa(gerador) << "\n";
std::cout << "Desconto: " << desconto(gerador) << "%\n";
}
Resumo
- O computador calcula números que parecem aleatórios a partir de uma semente. Mesma semente, mesma sequência.
- Três peças:
std::random_device(semente imprevisível) →std::mt19937(gerador, crie um só) → distribuição (a faixa que você quer). std::uniform_int_distribution<int> dado(1, 6);edado(gerador): um dado honesto, 1 e 6 incluídos.std::uniform_real_distribution<double>: números com vírgula; o limite de cima não entra.- Semente fixa (
std::mt19937 g(42);) repete os sorteios: ótimo para testar. O valor exato pode mudar de um compilador para outro. rand()esrand(time(0))são o jeito antigo: semente que só muda a cada segundo, faixa indefinida e resultado viciado com%. Evite.