为什么rand()+ rand()产生负数?


304

我观察到该rand()库函数在循环中仅被调用一次时,几乎总是产生正数。

for (i = 0; i < 100; i++) {
    printf("%d\n", rand());
}

但是,当我添加两个rand()呼叫时,现在生成的数字将具有更多的负数。

for (i = 0; i < 100; i++) {
    printf("%d = %d\n", rand(), (rand() + rand()));
}

有人可以解释为什么我在第二种情况下看到负数吗?

PS:我在循环之前将种子初始化为srand(time(NULL))


11
rand()不能为负...
6

293
rand()+ rand()可以顺流
maskacovnik

13
什么是RAND_MAX您的编译器?您通常可以在中找到它stdlib.h。(有趣:检查man 3 rand,它带有单行描述“坏随机数生成器”。)
usr2564301 2015年

6
做每个理智的程序员都会做的事情abs(rand()+rand())。我宁愿拥有一个积极的UB,也不愿拥有一个消极的UB!;)
Vinicius Kamakura

11
@hexa:对于UB而言,这不是什么好选择,因为添加已经发生了。您不能使UB成为定义的行为。一个理智的讲解者会避免像地狱般的UB。
对此网站来说太老实了

Answers:


542

rand()定义为返回0和之间的整数RAND_MAX

rand() + rand()

可能溢出。您观察到的结果很可能是整数溢出导致的不确定行为的结果。


4
@JakubArnold:每种语言如何分别指定溢出行为?例如,随着int的增长,Python没有任何内容(嗯,取决于可用的内存)。
对于这个网站来说太老实了

2
@Olaf这取决于语言决定如何表示有符号整数。Java没有机制来检测整数溢出(直到Java 8)并定义为可环绕,而Go仅使用2的补码表示法并将其定义为对有符号整数溢出合法。C显然支持大于2的补码。
PP

2
@EvanCarslake不,那不是普遍的行为。你说的是关于2的补码表示。但是C语言也允许其他表示形式。C语言规范说有符号整数溢出是不确定的。因此,一般而言,任何程序都不应依赖这种行为,并且需要仔细编码以免引起有符号整数溢出。但这不适用于无符号整数,因为它们会以定义良好的方式(归约模2)“环绕”。[续] ...
PP

12
这是来自C标准的与有符号整数溢出有关的报价:如果在表达式的求值过程中发生异常情况(即,如果结果没有在数学上定义或不在其类型的可表示值范围内),则行为未定义。
PP

3
@EvanCarslake稍微远离了问题,C编译器确实使用该标准,对于有符号整数,他们可以假设a + b > a他们知道这一点b > 0。他们还可以假设,如果有稍后执行的语句,a + 5那么当前值将小于INT_MAX - 5。因此,即使在不带陷阱的2的补码处理器/解释器上,程序也可能不会表现为ints是不带陷阱的2的补码。
Maciej Piechotka

90

问题是添加。rand()返回的int0...RAND_MAX。因此,如果您添加其中两个,则最多可以达到RAND_MAX * 2。如果超过INT_MAX,则加法的结果将超出有效范围int。有符号值的溢出是未定义的行为,并且可能导致键盘以外来语言与您交谈。

由于在这里添加两个随机结果没有好处,所以简单的想法就是不这样做。或者,unsigned int如果可以保存总和,则可以将每个结果强制转换为加法运算。或使用更大的类型。请注意,long不一定要比宽intlong long如果int至少为64位,则同样适用!

结论:避免添加。它不提供更多的“随机性”。如果您需要更多位,则可以连接值sum = a + b * (RAND_MAX + 1),但这也可能需要比更大的数据类型int

正如您所说的那样,是为了避免零结果:将两个rand()调用的结果相加是无法避免的,因为两个结果都可以为零。相反,您可以增加。如果是RAND_MAX == INT_MAX,则无法在中完成int。但是,(unsigned int)rand() + 1这样做的可能性很大。可能的(不是确定的),因为它确实需要UINT_MAX > INT_MAX,这在我所知道的所有实现中都是正确的(涵盖了过去30年中的许多嵌入式体系结构,DSP以及所有台式机,移动设备和服务器平台)。

警告:

尽管已经在此处添加了注释,但请注意,将两个随机值相加并不会获得均匀分布,而是像滚动两个骰子那样的三角形分布:要获得12(两个骰子)两个骰子都必须显示6。因为11已经有两个可能的变体:6 + 55 + 6,等等。

因此,从这个方面来说,添加也是不好的。

还要注意,结果rand()生成不是彼此独立的,因为它们是由伪随机数生成器生成的。另请注意,该标准未指定计算值的质量或均匀分布。


14
@badmad:那如果两个调用都返回0怎么办?
对于这个网站来说太老实了

3
@badmad:我只是想知道是否UINT_MAX > INT_MAX != false由标准保证。(听起来可能不错,但不确定是否需要)。如果是这样,您可以仅投射单个结果并递增(按此顺序!)。
对于这个网站来说太老实了

3
还有增益添加多个随机数,当你想要一个非均匀分布:stackoverflow.com/questions/30492259/...
心教堂

6
为了避免0,一个简单的“而结果为0,则重新滚动”?
奥利维尔·杜拉克

2
加上它们不仅是避免0的不好方法,而且还会导致分布不均匀。你得到一个像分布滚动骰子结果:7 6倍,可能为2或12
Barmar

36

这是对在对此答案的评论中提出的问题进行澄清的答案

我添加的原因是为了避免在代码中使用“ 0”作为随机数。rand()+ rand()是我很快想到的快速解决方案。

问题是要避免0。建议的解决方案至少有两个问题。正如其他答案所表明的那样,一个rand()+rand()可以调用未定义的行为。最好的建议是不要调用未定义的行为。另一个问题是不能保证rand()不会连续两次产生0。

以下命令拒绝零,避免未定义的行为,并且在大多数情况下,它们的速度比两次调用的速度快rand()

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);

9
rand() + 1
askvictor 2015年

3
@askvictor可能会溢出(尽管不太可能)。
Gerrit 2015年

3
@gerrit-取决于MAX_INT和RAND_MAX
askvictor 2015年

3
@gerrit,如果它们相同,我会感到惊讶,但是我想这是一个供
学究

10
如果RAND_MAX == MAX_INT,则rand()+ 1将以与rand()的值为0完全相同的概率溢出,这使该解决方案完全没有意义。如果你愿意冒险而忽略溢出的可能性,你可以如用RAND()为是,而忽略它的返回0的可能性
周华健耶扎贝克

3

在您的情况下,基本上rand()产生介于0和之间的数字。RAND_MAX2 RAND_MAX > INT_MAX

您可以使用数据类型的最大值模数,以防止溢出。这当然会破坏随机数的分布,但这rand只是获取快速随机数的一种方法。

#include <stdio.h>
#include <limits.h>

int main(void)
{
    int i=0;

    for (i=0; i<100; i++)
        printf(" %d : %d \n", rand(), ((rand() % (INT_MAX/2))+(rand() % (INT_MAX/2))));

    for (i=0; i<100; i++)
        printf(" %d : %ld \n", rand(), ((rand() % (LONG_MAX/2))+(rand() % (LONG_MAX/2))));

    return 0;
}

2

可以通过确保2 rand()之和返回的值永远不超过RAND_MAX的值来尝试一种比较棘手的方法。一种可能的方法是sum = rand()/ 2 + rand()/ 2; 这将确保即使两个rand都恰好返回32767,对于RAND_MAX值为32767的16位编译器,即使这样(32767/2 = 16383)16383 + 16383 = 32766,也不会导致负和。


1
OP希望从结果中排除0。加法也不能提供随机值的均匀分布。
对于这个网站来说太老实了

@Olaf:不能保证两次连续调用rand()都不会产生零,因此避免零的愿望并不是添加两个值的好理由。另一方面,如果要确保不发生溢出,则希望具有非均匀分布将是添加两个随机值的一个很好的理由。
超级猫

1

我添加的原因是为了避免在代码中使用“ 0”作为随机数。rand()+ rand()是我很快想到的快速解决方案。

一个简单的解决方案(可以将其称为“ Hack”),它永远不会产生零结果并且永远不会溢出:

x=(rand()/2)+1    // using divide  -or-
x=(rand()>>1)+1   // using shift which may be faster
                  // compiler optimization may use shift in both cases

这将限制您的最大值,但是如果您不关心该最大值,则此方法对您来说很好。


1
旁注:注意带符号变量的右移。它仅针对非负值进行了明确定义,对于负数则由实现定义。(幸运的是,rand()总是返回非负值)。但是,我会将优化留给编译器在这里。
对于这个网站来说太老实了

@Olaf:通常,有符号除以2的效率不如转换。除非编译器作者花了很多功夫告诉编译器rand将它为非负数,否则移位将比除以有符号整数2更有效。除法2u可能有效,但是如果a x是,int可能会导致有关从无符号进行隐式转换的警告签名。
超级猫

@supercat:请再次仔细阅读我的评论。您应该非常清楚,任何合理的编译器都将使用shift / 2(即使对于类似的东西-O0,我也已经看到了这一点,例如,没有明确请求优化)。这可能是C代码最琐碎,最成熟的优化。点是除法标准为整个整数范围定义的除法,不仅是非负值。再次:将优化留给编译器,首先编写正确且清晰的代码。对于初学者来说,这一点尤为重要。
对于这个网站来说太老实了

@Olaf:我测试过的每个编译器在右移rand()一或除以2u二时都比在使用2 时产生更有效的代码-O3。有人可能会合理地说这种优化不太重要,但是说“将这种优化留给编译器”将意味着编译器可能会执行它们。您知道实际上有任何编译器吗?
超级猫

@supercat:那您应该使用更多现代的编译器。我上次检查生成的汇编程序时,gcc刚刚生成了良好的代码。不过,尽管我很想拥有一个傻瓜,但我还是不希望您上次出现这种情况时感到烦恼。这些帖子已有几岁了,我的评论完全正确。谢谢。
对此网站来说太老实了

1

为避免0,请尝试以下操作:

int rnumb = rand()%(INT_MAX-1)+1;

您需要包含limits.h


4
这将使获得1的概率加倍。它与条件式加1基本上相同(但可能较慢),如果 rand()收益率0
太老实了此站点

是的,你是对的奥拉夫。如果rand()= 0或INT_MAX -1,则rnumb将为1。
Doni

更糟糕的是,我开始考虑它。实际上,它将对12(全部假设为RAND_MAX == INT_MAX)的适用性加倍。我确实忘记了- 1
对此网站而言太老实了

1
-1这里提供任何价值。 rand()%INT_MAX+1; 仍只会生成[1 ... INT_MAX]范围内的值。
chux-恢复莫妮卡

-2

尽管其他人都说过可能发生的溢出很可能是造成负面影响的原因,即使您使用无符号整数也是如此。真正的问题实际上是使用时间/日期功能作为种子。如果您真正熟悉此功能,您将确切知道我为什么要这样说。实际上,它是给定日期/时间以来的距离(经过的时间)。尽管使用日期/时间功能作为rand()的种子是很常见的做法,但实际上这并不是最佳选择。您应该寻找更好的替代方法,因为有关该主题的理论很多,我不可能一一列举。您在方程式中加上了溢出的可能性,这种方法从一开始就注定了失败。

那些发布rand()+ 1的用户正在使用最常用的解决方案,以确保它们不会得到负数。但是,这种方法实际上也不是最好的方法。

最好的办法是花额外的时间编写和使用适当的异常处理,并且仅在和/或最终结果为零时才添加到rand()数字中。并且,正确处理负数。rand()功能并不完美,因此需要与异常处理结合使用,以确保最终获得所需的结果。

花更多的时间和精力来研究,研究和正确实现rand()功能非常值得。只是我的两分钱。祝您一切顺利...


2
rand()没有指定要使用的种子。该标准确实指定它使用伪随机数生成器,而不是与任何时间的关系。它也没有说明发生器的质量。实际的问题显然是溢出。注意rand()+1用于避免0; rand()不返回负值。抱歉,您确实错过了这一点。这与PRNG的质量无关。...
对于这个网站来说太老实了

...在GNU / Linux下的良好做法是从中/dev/random植入种子,然后使用良好的PRNG(不确定rand()glibc 的质量),或者继续使用设备-如果没有足够的熵,则可能会阻塞应用程序。试图使应用程序中的信息熵很可能是一个漏洞,因为它可能更容易受到攻击。现在涉及到加固-不在这里
对于这个站点来说太老实了
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.