我正在尝试用骰子制作游戏,并且我需要在其中包含随机数(以模拟骰子的侧面。我知道如何使其介于 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 次相同的随机数。 有没有命令可以做到这一点?
您的测试应用程序最根本的问题是您调用srand一次,然后调用rand一次并退出。
srand
rand
函数的重点是用随机种子srand初始化 伪随机数序列。
这意味着如果您将 相同的值 传递给srand两个不同的应用程序(具有相同的srand/rand实现),那么 您将在两个应用程序中获得完全相同的 值rand()序列。
rand()
但是,在您的示例应用程序中,伪随机序列仅包含一个元素 - 从种子生成的伪随机序列的第一个元素等于当前1 sec精度时间。那么你期望在输出中看到什么?
1 sec
显然,当您碰巧在同一秒运行应用程序时-您使用相同的种子值-因此您的结果当然是相同的(正如 Martin York 在对该问题的评论中已经提到的那样)。
实际上,您应该调用srand(seed)一次,然后调用rand() 多次 并分析该序列-它应该看起来是随机的。
srand(seed)
修正 1 - 示例代码:
好的我明白了。显然口头描述是不够的(可能是语言障碍之类的...... :))。
srand()/rand()/time()基于问题中使用的相同函数的老式 C 代码示例:
srand()/rand()/time()
#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 已经很明显了。这些对于教育目的很好,有时也可以说明这一点,但对于任何严肃的用途,它们大多是无用的。
time
修正案 2 - 详细说明:
重要的是要理解,到目前为止,还 没有 C 或 C++ 标准特性(库函数或类)最终确定地产生实际随机数据(即由标准保证实际上是随机的)。解决这个问题的唯一标准特性是std::random_device,遗憾的是它仍然不能保证实际随机性。
根据应用程序的性质,您应该首先确定您是否真的需要真正随机(不可预测)的数据。 当您确实需要真正的随机性时, 值得注意的情况是信息安全——例如生成对称密钥、非对称私钥、盐值、安全令牌等。
然而,安全级随机数是一个独立的行业,值得单独写一篇文章。我在我的这个答案中简要讨论了它们。
为什么不直接使用 random_device?
这个问题其实是一个很好的问题。
答案是 - 当然你可以像你在你的例子中写的那样使用std::random_device 。它是完全合法和正确的使用std::random_device- 任何发行版都可以在它之上使用,就像任何其他随机引擎一样。如果您不需要像std::mt19937或任何其他的伪随机数生成器 (PRNG) - 请不要使用它。而已。
std::random_device
许多人重复的口头禅 - “std::random_device 只是用于播种 blah-blah-blah”是一个随机的 BS(双关语),与std::random_device. 当然std::random_device 可以用作 PRNG 种子 - 就像任何其他随机信息来源一样。
话虽如此 - 你是否真的应该使用std::random_device而不是一个好的 PRNG 完全取决于你的应用程序需求 - 下面写了一些细节。
您应该将任何 PRNG 视为一个数学函数,它采用有限大小的位输入序列并产生具有某些(通常是均匀的)分布的非常长的输出数字序列。如果您将相同的输入位传递给相同的 PRNG 两次 - 您将获得相同的输出序列。就像您使用相同的值x计算std::sin(x)两次一样 - 您将得到完全相同的正弦值返回。这就是为什么如果您需要避免每次都重复相同的 PRNG 输出数字序列 - 其输入位(种子)每次都必须不同。显然,由于 PRNG 操作只需要一些计算 - 它通常是本地的且快速的 - 没有系统调用,不涉及外部设备,在等待某事时没有阻塞,没有抛出异常 - 即时结果和高生成率的数字很容易扩展CPU 性能提高。
x
std::sin(x)
std::random_device另一方面是首次尝试在 C++ 标准库中引入实际的随机数生成器。
引用 C++ 标准(ISO/IEC 14882-2017):
29.6.6 类 random_device random_device 统一随机位生成器产生不确定的随机数。 如果实施限制阻止生成不确定的随机数,则实施可以使用随机数引擎。
29.6.6 类 random_device
^^^ 这句话很有趣,因为上面的(1)和(2)完全相互矛盾。std::random_device要么产生不确定的随机数,要么它的实现阻止它 - 两者不可能同时为真。但是“if”和“may”这个词只出现在(2)中——所以对上面引用的唯一可能的非矛盾理解是(2)中的“if”永远不会实现,每个实现只会产生不确定的随机数——即符合(1)。
让我们假设符合标准的std::random_device简单地产生随机和独立位的均匀分布序列。如果我们非常乐观,我们甚至可以希望获得加密安全的随机数——即使 C++ 标准不保证甚至承诺这样的事情。好消息是现代实现实际上提供了它——典型的/dev/urandomUNIX 实现和 Win32 Crypto API 实现都应该足够安全。无论如何,如果没有加密安全,它就std::random_device不是很有用的工具。
/dev/urandom
特别是因为根据 C++ 标准:
结果类型运算符()(); 6 返回:一个非确定性随机值,均匀分布在min()*和max()之间,包括两者。 如何生成这些值是*实现定义的。
结果类型运算符()();
6 返回:一个非确定性随机值,均匀分布在min()*和max()之间,包括两者。 如何生成这些值是*实现定义的。
^^^ 因此,如果我们真的需要 - 我们可能会在某种程度上将应用程序的可移植性限制为仅产生加密安全输出的那些实现std::random_device::operator()()- 因为这是为每个特定实现单独定义和记录的(这就是实现定义的意思顺便说一句)。当然,如果我们不需要像安全随机数这样的严格要求,我们不应该限制可移植性。
std::random_device::operator()()
显然,如果没有外部信息源(外部随机源),就无法产生均匀分布和独立随机位(AKA 真随机数)的非确定性序列——比如一些传感器信号噪声或一些外部事件的精确测量时间——任何外部和固有的不规则. (外部是指信息来自外部媒体 - 但传感器本身可能集成到 CPU/SoC)。然后通常过滤任何此类外部随机性源以去除可检测的规律性以确保符合均匀分布和独立位序列输出的要求。所有这些都极大地限制了产生的数据速率,并在等待新的外部数据时产生故障和/或阻塞的可能性。
因此,现在让我们权衡 PRNG 序列与真随机数在不同类型应用中的优缺点。
最后一点:
如果您不需要安全随机数并且不想依赖实施的质量甚至标准合规性,std::random_device那么单独使用它来生成 PRNG 种子也是一个坏主意。您应该在混合中加入更多随机性 - 例如将std::random_device输出与最大可用精度的当前时间(微秒/纳秒/任何可用的)结合起来,如果可用,也添加一些其他外部传感器的读数(例如原始陀螺仪传感器读数或音频麦克风读数或原始图像传感器读数 - 任何外部的和嘈杂的)。
例如。而不是使用这个:
std::mt19937::result_type seed = std::random_device()(); std::mt19937 gen(seed);
最好使用这样的东西:
std::mt19937::result_type seed = std::random_device()() ^ std::chrono::duration_cast<std::chrono::seconds>( std::chrono::system_clock::now().time_since_epoch() ).count() ^ std::chrono::duration_cast<std::chrono::microseconds>( std::chrono::high_resolution_clock::now().time_since_epoch() ).count() /* ^ more_external_random_stuff */ ; std::mt19937 gen(seed);
您还可以初始化std::mt19937::state_size(=624) 32 位数字的完整状态种子序列:
std::mt19937::state_size
std::random_device rd; std::array< std::uint32_t, std::mt19937::state_size > seed_array; for( auto it = seed_array.begin(); it != seed_array.end(); ++it ) { // read from std::random_device *it = rd(); // mix with a C++ equivalent of time(NULL) - UNIX time in seconds *it ^= std::chrono::duration_cast<std::chrono::seconds>( std::chrono::system_clock::now().time_since_epoch() ).count(); // mix with a high precision time in microseconds *it ^= std::chrono::duration_cast<std::chrono::microseconds>( std::chrono::high_resolution_clock::now().time_since_epoch() ).count(); //*it ^= more_external_random_stuff; } std::seed_seq sseq( seed_array.cbegin(), seed_array.cend() ); std::mt19937 gen(sseq);
在大多数情况下,伪随机数生成器就足够了——例如用于科学模拟或游戏。在某些情况下,甚至需要一致定义的伪随机序列——例如,在游戏中,您可以选择在运行时生成完全相同的地图,以避免在安装包中存储大量数据。
最初的问题和反复出现的大量相同/相似的问题(甚至许多被误导的“答案”)表明,首先重要的是区分随机数和伪随机数,并了解什么是伪随机数序列首先并意识到伪随机数生成器的使用方式与使用真随机数生成器的方式不同。
直观地 ,当您请求随机数时 - 返回的结果不应依赖于先前返回的值,不应依赖于之前是否有人请求过任何东西,也不应依赖于什么时候、通过什么过程、在什么计算机上、来自什么生成器和在它被要求的星系。毕竟,这就是 “随机” 这个词的含义——不可预测且独立于任何事物——否则它不再是随机的,对吧?有了这种直觉,在网络上搜索一些魔法咒语以在任何可能的情况下获得这样的随机数是很自然的。
^^^这种直观的期望在涉及伪随机数生成器的所有情况下都是 非常错误和有害 的——尽管对于真正的随机数是合理的。
虽然存在“随机数”的有意义的概念(有点) - 没有“伪随机数”这样的东西。伪随机数生成器实际上产生伪随机数 序列 。
伪随机序列实际上总是 确定性 的(由其算法和初始参数预先确定)——即实际上没有任何随机性。
当专家谈论 PRNG 的质量时,他们实际上谈论的是生成序列(及其显着的子序列)的统计特性。例如,如果您通过轮流使用它们来组合两个高质量的 PRNG - 您可能会产生不好的结果序列 - 尽管它们分别生成好的序列(这两个好的序列可能只是相互关联,因此组合不好)。
具体来说rand()/srand(s)一对函数提供了一个单一的每进程非线程安全(!)伪随机数序列,使用实现定义的算法生成。函数rand()产生范围内的值[0, RAND_MAX]。
srand(s)
[0, RAND_MAX]
引用 C11 标准 (ISO/IEC 9899:2011):
该srand函数使用该参数作为一个新的伪随机数序列的种子,这些伪随机数将由后续调用返回rand。如果 srand然后以相同的种子值调用,则应重复伪随机数序列。如果rand在进行任何调用之前调用srand,则应生成与srand第一次调用时相同的序列,种子值为 1。
许多人合理地期望这rand()将产生一系列半独立均匀分布的数字,范围0为RAND_MAX。好吧,它当然应该(否则它没用),但不幸的是,不仅标准不需要这样做 - 甚至还有明确的免责声明指出 “无法保证产生的随机序列的质量” 。在某些历史案例rand/srand实施中确实质量很差。即使在现代实现中它很可能已经足够好 - 但信任被打破并且不容易恢复。除了它的非线程安全特性之外,它在多线程应用程序中的安全使用也变得棘手和有限(仍然可能——您可以只从一个专用线程中使用它们)。
0
RAND_MAX
新的类模板std::mersenne_twister_engine<>(及其便利的 typedefs - std::mt19937/std::mt19937_64具有良好的模板参数组合)提供了在 C11 标准中定义的 每个对象的 伪随机数生成器。使用相同的模板参数和相同的初始化参数,不同的对象将在使用符合 C11 标准库的任何应用程序中的任何计算机上生成完全相同的每个对象输出序列。此类的优势在于其可预测的高质量输出序列和跨实现的完全一致性。
std::mt19937
std::mt19937_64
在 C++11 标准中还定义了更多 PRNG 引擎 - std::linear_congruential_engine<>(历史上在某些 C 标准库实现中用作公平质量srand/rand算法)和std::subtract_with_carry_engine<>。它们还生成完全定义的参数相关的每个对象输出序列。
srand/rand
上面过时的 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; }