Menu

Случайные числа в C: rand, srand и число в заданном диапазоне

Как генерировать случайные числа в C с помощью rand() и RAND_MAX, почему srand(time(NULL)) вызывают ровно один раз, отображение в диапазон через % и небольшое смещение, которое оно вносит, случайные дробные числа и воспроизводимые последовательности.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Случайные числа в C берутся из двух функций в <stdlib.h>: rand(), которая выдаёт следующее значение, и srand(), которая задаёт точку отсчёта. По-настоящему случайными они не являются - это псевдослучайная последовательность, детерминированно вычисленная из зерна, - что является ограничением для криптографии и достоинством для тестирования.

rand() и RAND_MAX

rand() возвращает int где-то между 0 и RAND_MAX включительно. RAND_MAX - это макрос, который гарантированно не меньше 32767; на Linux и macOS он равен 2147483647.

Запустите это дважды. Числа оба раза будут одинаковыми - и это не баг.

Задание зерна через srand

Без вызова srand последовательность ведёт себя так, будто вы вызвали srand(1). Одно зерно - одна и та же последовательность, каждый запуск. Чтобы числа менялись от запуска к запуску, задайте зерно чем-то изменчивым - по традиции текущим временем:

time(NULL) из <time.h> возвращает число секунд с начала 1970 года, поэтому каждый запуск получает своё зерно. Приведение к unsigned int убирает предупреждение о сужении time_t.

Три правила про зерно, и все три нарушают:

Задавайте зерно ровно один раз, в начале main. Вызов srand перед каждым rand() - классический антипаттерн: внутри цикла, который отрабатывает меньше чем за секунду, time(NULL) на каждой итерации возвращает одно и то же значение, поэтому вы переинициализируете генератор тем же числом, и rand() каждый раз возвращает одно и то же первое значение. На выходе - столбик одинаковых «случайных» чисел.

Не переинициализируйте генератор, чтобы «улучшить» случайность. Качество генератора берётся из продвижения его внутреннего состояния; сброс этого состояния выбрасывает последовательность.

У time(NULL) разрешение в одну секунду. Две программы, запущенные в одну и ту же секунду, получат одинаковую последовательность. Для игры это нормально и неприемлемо везде, где важна независимость.

Число в диапазоне

Стандартная идиома использует оператор остатка:

rand() % n            /* от 0 до n-1 */
rand() % n + min      /* от min до min+n-1 */

Чтобы получить значения от min до max включительно, количество возможных значений равно max - min + 1:

Именно в + 1 живут ошибки смещения на единицу. rand() % 6 даёт значения от 0 до 5, поэтому бросок кубика - это rand() % 6 + 1. Написав rand() % 7 + 1, чтобы «включить 6», вы получите семигранный кубик.

Честное замечание о смещении остатка

rand() % n не идеально равномерен, если только n не делит RAND_MAX + 1 нацело.

Представьте это на маленьких числах. Если бы RAND_MAX был равен 9 - то есть rand() возвращал бы значения от 0 до 9, десять равновероятных вариантов, - то rand() % 3 отображает 0, 3, 6, 9 в 0; 1, 4, 7 в 1; и 2, 5, 8 в 2. Результат 0 получается четырьмя способами из десяти, результаты 1 и 2 - тремя каждый. Ноль на 33% вероятнее.

Тот же перекос существует и с настоящим RAND_MAX, просто он несравнимо меньше: лишними оказываются первые (RAND_MAX + 1) % n исходов, каждый из которых получает один дополнительный шанс примерно из 2,1 миллиарда. Для броска кубика, перетасованной колоды или симуляции это неизмеримо - берите % и идите дальше.

Когда это всё же важно - статистические расчёты, всё связанное с безопасностью, - отбрасывайте лишние значения, а не подмешивайте их:

Цикл выбрасывает тот небольшой диапазон значений, который вызвал бы перекос, и тянет заново. Завершается он быстро - отбрасываемый кусочек составляет исчезающе малую долю целого.

Для по-настоящему чувствительной к безопасности случайности rand() не годится ни при какой аккуратности: используйте arc4random_buf на macOS и BSD, getrandom() на Linux или BCryptGenRandom на Windows.

Случайные дробные числа

Разделите на RAND_MAX, чтобы попасть в [0.0, 1.0], а затем масштабируйте:

Приведение в (double) rand() обязательно. Без него rand() / RAND_MAX - это целочисленное деление, которое почти всегда даёт 0, а 1 - с шансом один на два миллиарда, когда выпадет максимум. Баг выглядит как «все мои случайные дробные числа нулевые». Почему так - см. приведение типов.

Воспроизводимые последовательности

Фиксированное зерно даёт одну и ту же последовательность при каждом запуске - ровно то, что нужно для теста, отладочного сеанса или игры с кодами уровней, которыми можно делиться:

Зерно 42 даёт одни и те же пять чисел при каждом использовании - и в этом запуске, и в любом другом с той же библиотекой. Именно из-за этой воспроизводимости симуляция должна позволять выбирать зерно: обычно запускаемся от часов, а при воспроизведении бага передаём фиксированное значение.

Одна оговорка: последовательность для данного зерна не переносима. Разные библиотеки C используют разные генераторы, поэтому зерно 42 на glibc и зерно 42 на Windows дадут разные числа. Воспроизводимо на одной машине, но не между машинами.

Игра в кости

Всё вместе - зерно один раз, вспомогательная функция для диапазона и массив, подсчитывающий результаты:

Гистограмма должна иметь пик на 7 и сходить на нет к 2 и 12 - семёрку можно составить шестью способами, а 2 и 12 - лишь одним каждую. Генератор, дающий здесь ровное распределение, был бы сломан.

Две смежные страницы: стандартная библиотека разбирает остальное содержимое <stdlib.h>, а математические функции описывают <math.h>, который понадобится вам сразу же, как только случайные значения пойдут в настоящие вычисления.

Часто задаваемые вопросы

Как сгенерировать случайное число в C?

Подключите <stdlib.h>, один раз в начале main задайте зерно через srand((unsigned) time(NULL)) (нужен <time.h>), а затем вызывайте rand() для каждого значения. rand() возвращает int от 0 до RAND_MAX включительно.

Как получить случайное число между двумя значениями в C?

Используйте rand() % (max - min + 1) + min. Для броска кубика от 1 до 6 это rand() % 6 + 1. % n отображает результат в 0..n-1, а прибавление min сдвигает окно - главное, чтобы количество значений включало обе границы, за это и отвечает + 1.

Почему моя программа на C каждый раз печатает одни и те же случайные числа?

Потому что вы не вызвали srand. Без зерна rand() ведёт себя так, будто зерно равно 1, поэтому каждый запуск даёт одну и ту же последовательность. Вызовите srand((unsigned) time(NULL)) один раз при старте программы - именно один раз, а не перед каждым вызовом rand(), от этого станет только хуже.

Что такое смещение остатка (modulo bias)?

rand() % n идеально равномерен, только если n нацело делит RAND_MAX + 1. Иначе первые несколько значений встречаются на всём диапазоне на один раз чаще и оказываются чуть более вероятными. При RAND_MAX, равном 2147483647, и небольшом n перекос далеко ниже того, что заметит игра или симуляция, но для криптографии или статистики используйте цикл с отбраковкой или полноценный генератор.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ