如何在 C 中生成随机数?

新手上路,请多包涵

我正在尝试用骰子制作游戏,并且我需要在其中包含随机数(以模拟骰子的侧面。我知道如何使其介于 1 和 6 之间)。使用

#include <cstdlib>
#include <ctime>
#include <iostream>

using namespace std;

int main()
{
    srand((unsigned)time(0));
    int i;
    i = (rand()%6)+1;
    cout << i << "\n";
}

不能很好地工作,因为当我运行程序几次时,我得到的输出如下:

 6
1
1
1
1
1
2
2
2
2
5
2

所以我想要一个每次都会生成 不同 随机数的命令,而不是连续 5 次相同的随机数。有没有命令可以做到这一点?

原文由 Predictability 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 908
2 个回答

您的测试应用程序最根本的问题是您调用 srand 一次,然后调用 rand 一次并退出。

srand 函数的重点是用随机种子初始化 伪随机数序列

这意味着如果您在两个不同的应用程序 中将相同的值 传递给 srand (具有相同的 srand / rand 实现),那么 您将得到完全相同的序列 rand() 在这两个应用程序之后读取的值。

但是,在您的示例应用程序中,伪随机序列仅包含一个元素 - 从种子生成的伪随机序列的第一个元素等于 1 sec 精度的当前时间。你期望在输出上看到什么?

显然,当您碰巧在同一秒运行应用程序时-您使用相同的种子值-因此您的结果当然是相同的(正如 Martin York 在对该问题的评论中已经提到的那样)。

实际上,您应该调用 srand(seed) 一次,然后 多次 调用 rand() 并分析该序列 - 它应该看起来是随机的。

修正 1 - 示例代码:

好的我明白了。显然口头描述是不够的(可能是语言障碍之类的…… :))。

基于问题中使用的相同 srand()/rand()/time() 函数的老式 C 代码示例:

 #include <stdlib.h>
#include <time.h>
#include <stdio.h>

int main(void)
{
    unsigned long j;
    srand( (unsigned)time(NULL) );

    for( j = 0; j < 100500; ++j )
    {
        int n;

        /* skip rand() readings that would make n%6 non-uniformly distributed
          (assuming rand() itself is uniformly distributed from 0 to RAND_MAX) */
        while( ( n = rand() ) > RAND_MAX - (RAND_MAX-5)%6 )
        { /* bad value retrieved so get next one */ }

        printf( "%d,\t%d\n", n, n % 6 + 1 );
    }

    return 0;
}

^^^ 单次运行的程序应该看起来是随机的。

请注意,我不建议在生产中使用 rand / srand 函数,原因如下,我绝对不建议使用函数 time 作为一个随机种子,因为 IMO 已经很明显了。这些对于教育目的很好,有时也可以说明这一点,但对于任何严肃的用途,它们大多是无用的。

修正案 2 - 详细说明:

重要的是要理解,到目前为止,还 没有 C 或 C++ 标准特性(库函数或类)最终确定地产生实际随机数据(即由标准保证实际上是随机的)。解决这个问题的唯一标准特性是 std::random_device ,遗憾的是它仍然不能保证实际随机性。

根据应用程序的性质,您应该首先确定您是否真的需要真正随机(不可预测的)数据。 当你确实需要真正的随机性时, 值得注意的情况是信息安全——例如生成对称密钥、非对称私钥、盐值、安全令牌等。

然而,安全级随机数是一个独立的行业,值得单独写一篇文章。我在我的 这个答案 中简要讨论了它们。

在大多数情况下 ,伪随机数生成器 就足够了——例如用于科学模拟或游戏。在某些情况下,甚至需要一致定义的伪随机序列——例如,在游戏中,您可以选择在运行时生成完全相同的地图,以避免在安装包中存储大量数据。

最初的问题和重复出现的大量相同/相似的问题(甚至许多被误导的“答案”)表明,首先重要的是区分随机数和伪随机数,并了解什么是伪随机数序列首先并要意识到伪随机数生成器的使用方式与使用真随机数生成器的方式不同。

_直观地_,当您请求随机数时 - 返回的结果不应依赖于先前返回的值,也不应依赖于是否有人之前请求过任何内容,也不应依赖于何时、通过什么过程、在什么计算机上、来自什么生成器和在它被要求的星系。毕竟,这就是 “随机” 这个词的含义——不可预测且独立于任何事物——否则它不再是随机的,对吧?有了这种直觉,在网络上搜索一些魔法咒语以在任何可能的情况下获得这样的随机数是很自然的。

^^^ 这种直观的期望在涉及 伪随机数生成器 的所有情况下都是 非常错误和有害 的——尽管对于真正的随机数是合理的。

虽然存在“随机数”的有意义的概念(有点) - 没有“伪随机数”这样的东西。 伪随机数生成器 实际上产生伪随机数 序列

伪随机序列实际上总是 确定性 的(由它的算法和初始参数决定)——也就是说,它实际上没有任何随机性。

当专家谈论 PRNG 的质量时,他们实际上谈论的是生成序列(及其显着的子序列)的统计特性。例如,如果您通过轮流使用它们来组合两个高质量的 PRNG - 您可能会产生不好的结果序列 - 尽管它们分别生成好的序列(这两个好的序列可能只是相互关联,因此组合不好)。

具体来说 rand() / srand(s) 函数对提供了一个单一的每进程非线程安全(!)伪随机数序列,使用实现定义的算法生成。函数 rand() 产生范围内的值 [0, RAND_MAX]

引用 C11 标准 (ISO/IEC 9899:2011):

srand 函数使用参数作为新的伪随机数序列的种子,这些新序列将由后续调用 rand 返回。如果用相同的种子值调用 srand ,则应重复伪随机数序列。如果在调用 rand 之前调用 srand ,则应生成与第一次调用 srand 时相同的序列,种子值为 1。

许多人合理地期望 rand() 会在 0RAND_MAX 范围内产生一系列半独立均匀分布的数字。好吧,它当然应该(否则它没用),但不幸的是,不仅标准不需要这样做 - 甚至还有明确的免责声明指出 “无法保证产生的随机序列的质量” 。在某些历史案例 rand / srand 实施的质量确实很差。即使在现代实现中它很可能已经足够好 - 但信任被打破并且不容易恢复。除了它的非线程安全特性之外,它在多线程应用程序中的安全使用也变得棘手和有限(仍然可能——您可以只从一个专用线程中使用它们)。

新类模板 std::mersenne_twister_engine<> (及其便利的 typedefs - std::mt19937 / std::mt19937_64 具有良好的模板参数组合)提供在 C++ 中定义的 每个对象的 伪随机数生成器11个标准。使用相同的模板参数和相同的初始化参数,不同的对象将在使用符合 C++11 标准库的任何应用程序中的任何计算机上生成完全相同的每个对象输出序列。此类的优势在于其可预测的高质量输出序列和跨实现的完全一致性。

在 C++11 标准中定义了更多的 PRNG 引擎 - std::linear_congruential_engine<> (历史上用作公平质量 srand/rand 在一些 C 标准库实现中的算法)和 std::subtract_with_carry_engine<> 。它们还生成完全定义的参数相关的每个对象输出序列。

上面过时的 C 代码的现代 C++11 示例替换:

 #include <iostream>
#include <chrono>
#include <random>

int main()
{
    std::random_device rd;
    // seed value is designed specifically to make initialization
    // parameters of std::mt19937 (instance of std::mersenne_twister_engine<>)
    // different across executions of application
    std::mt19937::result_type seed = rd() ^ (
            (std::mt19937::result_type)
            std::chrono::duration_cast<std::chrono::seconds>(
                std::chrono::system_clock::now().time_since_epoch()
                ).count() +
            (std::mt19937::result_type)
            std::chrono::duration_cast<std::chrono::microseconds>(
                std::chrono::high_resolution_clock::now().time_since_epoch()
                ).count() );

    std::mt19937 gen(seed);

    for( unsigned long j = 0; j < 100500; ++j )
    /* ^^^Yes. Generating single pseudo-random number makes no sense
       even if you use std::mersenne_twister_engine instead of rand()
       and even when your seed quality is much better than time(NULL) */
    {
        std::mt19937::result_type n;
        // reject readings that would make n%6 non-uniformly distributed
        while( ( n = gen() ) > std::mt19937::max() -
                                    ( std::mt19937::max() - 5 )%6 )
        { /* bad value retrieved so get next one */ }

        std::cout << n << '\t' << n % 6 + 1 << '\n';
    }

    return 0;
}

使用 std::uniform_int_distribution<> 的先前代码的版本

#include <iostream>
#include <chrono>
#include <random>

int main()
{
    std::random_device rd;
    std::mt19937::result_type seed = rd() ^ (
            (std::mt19937::result_type)
            std::chrono::duration_cast<std::chrono::seconds>(
                std::chrono::system_clock::now().time_since_epoch()
                ).count() +
            (std::mt19937::result_type)
            std::chrono::duration_cast<std::chrono::microseconds>(
                std::chrono::high_resolution_clock::now().time_since_epoch()
                ).count() );

    std::mt19937 gen(seed);
    std::uniform_int_distribution<unsigned> distrib(1, 6);

    for( unsigned long j = 0; j < 100500; ++j )
    {
        std::cout << distrib(gen) << ' ';
    }

    std::cout << '\n';
    return 0;
}

原文由 Serge Dundich 发布,翻译遵循 CC BY-SA 4.0 许可协议

一个非常有主见的答案

c++ <random> 库违反了软件工程的最佳原则之一:“简单的事情做简单、复杂、不常见的事情可能更复杂一些。”

相反,他们甚至使简单和常见的用例也变得过于复杂,只是因为他们患有文化疾病,害怕“这还不够普遍”之类的评论。

结果,现在每当您想要一个简单的随机数时,您都必须查看文档,阅读带有文本墙的堆栈溢出,美化这个糟糕的设计,而不仅仅是一个易于记忆的一两个衬里。 (Common Lisp 更实用: (random 5) 从 0..4 和 (random 1.0) 产生 0.0..1.0 之间的实数。这是最常见的用例,它是在你的指尖。如果你需要更复杂的东西,你必须找到包和库或者自己做。)

只需计算全球每个人在理解该标题及其内容上浪费时间的累积工时,看看它有多糟糕。

即使我现在在浪费时间,写这个答案,你也浪费你的时间,阅读它,只是因为他们创造了一个复杂的谜题,这与其他现代可憎的东西有着相似的精神,比如 Vulkan API。

那么,该如何应对呢?浪费一次时间,为最常见的用例编写一个头文件,然后在需要时重新使用它。

原文由 BitTickler 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题