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.

Você vai aprender
  • 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::mt19937 e 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 livroNo C++Nome técnico
O livro de números embaralhadosUma 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ê abreO número de partida da fórmulasemente (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 sementestd::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") chamado semente. 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 o random_device sorteou. 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.

Por que três peças, e não uma função 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.

A mesma semente, outro compilador: números diferentes

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

ProblemaO que acontece
Semente pelo relógiotime(0) muda só uma vez por segundo. Dois programas iniciados no mesmo segundo "sorteiam" igual (você viu acima).
Faixa pequena e indefinidaRAND_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 fracaO 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 todoQualquer 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

ObjetivoFormaQuando usar
Sorteio diferente a cada execuçãostd::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ívelstd::mt19937 g(42);Testes, depuração, simulações para conferir.
Inteiro de a até bstd::uniform_int_distribution<int> d(a, b);Dados, rifas, posição numa lista. Inclui a e b.
Número com vírgulastd::uniform_real_distribution<double> d(a, b);Preços, chances, coordenadas. Inclui a, não inclui b.
Número imprevisível diretostd::random_device rd; rd();Só para a semente. Para muitos números, é lento.
Jeito antigostd::srand(std::time(0)); std::rand() % 6 + 1Evite. Só para entender código antigo.
Segurança: senhas e códigos secretos

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 "sorteio" sai sempre igual

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 dado tira 0 ou 7

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

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

std::<int> rifa(1, 100);

2. Semente fixa

Complete para criar o gerador com a semente fixa 2026:

std:: gerador(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); e dado(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() e srand(time(0)) são o jeito antigo: semente que só muda a cada segundo, faixa indefinida e resultado viciado com %. Evite.